Fix issue with "create note from tag" (#2215)

This commit is contained in:
Veronica Berglyd Olsen
2025-01-30 17:40:36 +01:00
committed by GitHub
7 changed files with 67 additions and 56 deletions
+4
View File
@@ -173,6 +173,10 @@ class nwKeyWords:
TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY, TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY, STORY_KEY, MENTION_KEY, OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY, STORY_KEY, MENTION_KEY,
] ]
CAN_CREATE = [
POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY,
]
# Set of Valid Keys # Set of Valid Keys
VALID_KEYS = set(ALL_KEYS) VALID_KEYS = set(ALL_KEYS)
-7
View File
@@ -68,13 +68,6 @@ class nwComment(Enum):
STORY = 7 STORY = 7
class nwTrinary(Enum):
NEGATIVE = -1
NEUTRAL = 0
POSITIVE = 1
class nwChange(Enum): class nwChange(Enum):
CREATE = 0 CREATE = 0
+5 -6
View File
@@ -29,7 +29,6 @@ from PyQt5.QtGui import QColor, QPainter, QPaintEvent
from PyQt5.QtWidgets import QAbstractButton, QWidget from PyQt5.QtWidgets import QAbstractButton, QWidget
from novelwriter import CONFIG from novelwriter import CONFIG
from novelwriter.enum import nwTrinary
from novelwriter.types import QtBlack, QtPaintAntiAlias from novelwriter.types import QtBlack, QtPaintAntiAlias
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -47,14 +46,14 @@ class StatusLED(QAbstractButton):
self._postitve = QtBlack self._postitve = QtBlack
self._negative = QtBlack self._negative = QtBlack
self._color = QtBlack self._color = QtBlack
self._state = nwTrinary.NEUTRAL self._state = None
self._bPx = CONFIG.pxInt(1) self._bPx = CONFIG.pxInt(1)
self.setFixedWidth(sW) self.setFixedWidth(sW)
self.setFixedHeight(sH) self.setFixedHeight(sH)
return return
@property @property
def state(self) -> nwTrinary: def state(self) -> bool | None:
"""The current state of the LED.""" """The current state of the LED."""
return self._state return self._state
@@ -66,11 +65,11 @@ class StatusLED(QAbstractButton):
self.setState(self._state) self.setState(self._state)
return return
def setState(self, state: nwTrinary) -> None: def setState(self, state: bool | None) -> None:
"""Set the colour state.""" """Set the colour state."""
if state == nwTrinary.POSITIVE: if state is True:
self._color = self._postitve self._color = self._postitve
elif state == nwTrinary.NEGATIVE: elif state is False:
self._color = self._negative self._color = self._negative
else: else:
self._color = self._neutral self._color = self._neutral
+30 -14
View File
@@ -34,7 +34,7 @@ from __future__ import annotations
import bisect import bisect
import logging import logging
from enum import Enum from enum import Enum, IntFlag
from time import time from time import time
from PyQt5.QtCore import ( from PyQt5.QtCore import (
@@ -57,7 +57,7 @@ from novelwriter.constants import nwConst, nwKeyWords, nwShortcode, nwUnicode
from novelwriter.core.document import NWDocument from novelwriter.core.document import NWDocument
from novelwriter.enum import ( from novelwriter.enum import (
nwChange, nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwChange, nwComment, nwDocAction, nwDocInsert, nwDocMode, nwItemClass,
nwItemType, nwTrinary nwItemType
) )
from novelwriter.extensions.configlayout import NColourLabel from novelwriter.extensions.configlayout import NColourLabel
from novelwriter.extensions.eventfilters import WheelEventFilter from novelwriter.extensions.eventfilters import WheelEventFilter
@@ -84,6 +84,13 @@ class _SelectAction(Enum):
MOVE_AFTER = 3 MOVE_AFTER = 3
class _TagAction(IntFlag):
NONE = 0b00
FOLLOW = 0b01
CREATE = 0b10
class GuiDocEditor(QPlainTextEdit): class GuiDocEditor(QPlainTextEdit):
"""Gui Widget: Main Document Editor""" """Gui Widget: Main Document Editor"""
@@ -1158,11 +1165,11 @@ class GuiDocEditor(QPlainTextEdit):
# Follow # Follow
status = self._processTag(cursor=pCursor, follow=False) status = self._processTag(cursor=pCursor, follow=False)
if status == nwTrinary.POSITIVE: if status & _TagAction.FOLLOW:
action = ctxMenu.addAction(self.tr("Follow Tag")) action = ctxMenu.addAction(self.tr("Follow Tag"))
action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, follow=True)) action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, follow=True))
ctxMenu.addSeparator() ctxMenu.addSeparator()
elif status == nwTrinary.NEGATIVE: elif status & _TagAction.CREATE:
action = ctxMenu.addAction(self.tr("Create Note for Tag")) action = ctxMenu.addAction(self.tr("Create Note for Tag"))
action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, create=True)) action.triggered.connect(qtLambda(self._processTag, cursor=pCursor, create=True))
ctxMenu.addSeparator() ctxMenu.addSeparator()
@@ -1925,8 +1932,9 @@ class GuiDocEditor(QPlainTextEdit):
self._qDocument.syntaxHighlighter.rehighlightBlock(block) self._qDocument.syntaxHighlighter.rehighlightBlock(block)
return return
def _processTag(self, cursor: QTextCursor | None = None, def _processTag(
follow: bool = True, create: bool = False) -> nwTrinary: self, cursor: QTextCursor | None = None, follow: bool = True, create: bool = False
) -> _TagAction:
"""Activated by Ctrl+Enter. Checks that we're in a block """Activated by Ctrl+Enter. Checks that we're in a block
starting with '@'. We then find the tag under the cursor and starting with '@'. We then find the tag under the cursor and
check that it is not the tag itself. If all this is fine, we check that it is not the tag itself. If all this is fine, we
@@ -1936,19 +1944,22 @@ class GuiDocEditor(QPlainTextEdit):
if cursor is None: if cursor is None:
cursor = self.textCursor() cursor = self.textCursor()
status = _TagAction.NONE
block = cursor.block() block = cursor.block()
text = block.text() text = block.text()
if len(text) == 0: if len(text) == 0:
return nwTrinary.NEUTRAL return status
if text.startswith("@") and self._docHandle: if text.startswith("@") and self._docHandle:
isGood, tBits, tPos = SHARED.project.index.scanThis(text) isGood, tBits, tPos = SHARED.project.index.scanThis(text)
if ( if (
not isGood or not tBits or tBits[0] == nwKeyWords.TAG_KEY not isGood
or tBits[0] not in nwKeyWords.VALID_KEYS or not tBits
or (key := tBits[0]) == nwKeyWords.TAG_KEY
or key not in nwKeyWords.VALID_KEYS
): ):
return nwTrinary.NEUTRAL return status
tag = "" tag = ""
exist = False exist = False
@@ -1965,7 +1976,14 @@ class GuiDocEditor(QPlainTextEdit):
if not tag or tag.startswith("@"): if not tag or tag.startswith("@"):
# The keyword cannot be looked up, so we ignore that # The keyword cannot be looked up, so we ignore that
return nwTrinary.NEUTRAL return status
if not exist and key in nwKeyWords.CAN_CREATE:
# Must only be set if we have a tag selected
status |= _TagAction.CREATE
if exist:
status |= _TagAction.FOLLOW
if follow and exist: if follow and exist:
logger.debug("Attempting to follow tag '%s'", tag) logger.debug("Attempting to follow tag '%s'", tag)
@@ -1977,9 +1995,7 @@ class GuiDocEditor(QPlainTextEdit):
itemClass = nwKeyWords.KEY_CLASS.get(tBits[0], nwItemClass.NO_CLASS) itemClass = nwKeyWords.KEY_CLASS.get(tBits[0], nwItemClass.NO_CLASS)
self.requestNewNoteCreation.emit(tag, itemClass) self.requestNewNoteCreation.emit(tag, itemClass)
return nwTrinary.POSITIVE if exist else nwTrinary.NEGATIVE return status
return nwTrinary.NEUTRAL
def _emitRenameItem(self, block: QTextBlock) -> None: def _emitRenameItem(self, block: QTextBlock) -> None:
"""Emit a signal to request an item be renamed.""" """Emit a signal to request an item be renamed."""
+6 -7
View File
@@ -34,7 +34,6 @@ from PyQt5.QtWidgets import QApplication, QLabel, QStatusBar, QWidget
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import formatTime from novelwriter.common import formatTime
from novelwriter.constants import nwConst from novelwriter.constants import nwConst
from novelwriter.enum import nwTrinary
from novelwriter.extensions.modified import NClickableLabel from novelwriter.extensions.modified import NClickableLabel
from novelwriter.extensions.statusled import StatusLED from novelwriter.extensions.statusled import StatusLED
@@ -121,8 +120,8 @@ class GuiMainStatus(QStatusBar):
self.setRefTime(-1.0) self.setRefTime(-1.0)
self.setLanguage(*SHARED.spelling.describeDict()) self.setLanguage(*SHARED.spelling.describeDict())
self.setProjectStats(0, 0) self.setProjectStats(0, 0)
self.setProjectStatus(nwTrinary.NEUTRAL) self.setProjectStatus(None)
self.setDocumentStatus(nwTrinary.NEUTRAL) self.setDocumentStatus(None)
self.updateTime() self.updateTime()
return return
@@ -152,12 +151,12 @@ class GuiMainStatus(QStatusBar):
self._refTime = refTime self._refTime = refTime
return return
def setProjectStatus(self, state: nwTrinary) -> None: def setProjectStatus(self, state: bool | None) -> None:
"""Set the project status colour icon.""" """Set the project status colour icon."""
self.projIcon.setState(state) self.projIcon.setState(state)
return return
def setDocumentStatus(self, state: nwTrinary) -> None: def setDocumentStatus(self, state: bool | None) -> None:
"""Set the document status colour icon.""" """Set the document status colour icon."""
self.docIcon.setState(state) self.docIcon.setState(state)
return return
@@ -220,13 +219,13 @@ class GuiMainStatus(QStatusBar):
@pyqtSlot(bool) @pyqtSlot(bool)
def updateProjectStatus(self, status: bool) -> None: def updateProjectStatus(self, status: bool) -> None:
"""Update the project status.""" """Update the project status."""
self.setProjectStatus(nwTrinary.NEGATIVE if status else nwTrinary.POSITIVE) self.setProjectStatus(not status)
return return
@pyqtSlot(bool) @pyqtSlot(bool)
def updateDocumentStatus(self, status: bool) -> None: def updateDocumentStatus(self, status: bool) -> None:
"""Update the document status.""" """Update the document status."""
self.setDocumentStatus(nwTrinary.NEGATIVE if status else nwTrinary.POSITIVE) self.setDocumentStatus(not status)
return return
## ##
+10 -9
View File
@@ -35,8 +35,8 @@ from novelwriter import CONFIG, SHARED
from novelwriter.common import decodeMimeHandles from novelwriter.common import decodeMimeHandles
from novelwriter.constants import nwKeyWords, nwUnicode from novelwriter.constants import nwKeyWords, nwUnicode
from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout, nwTrinary from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout
from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.doceditor import GuiDocEditor, _TagAction
from novelwriter.text.counting import standardCounter from novelwriter.text.counting import standardCounter
from novelwriter.types import ( from novelwriter.types import (
QtAlignJustify, QtAlignLeft, QtKeepAnchor, QtModCtrl, QtModNone, QtAlignJustify, QtAlignLeft, QtKeepAnchor, QtModCtrl, QtModNone,
@@ -1693,21 +1693,22 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
# Empty Block # Empty Block
docEditor.setCursorLine(2) docEditor.setCursorLine(2)
assert docEditor._processTag() is nwTrinary.NEUTRAL assert docEditor._processTag() == _TagAction.NONE
# Not On Tag # Not On Tag
docEditor.setCursorLine(1) docEditor.setCursorLine(1)
assert docEditor._processTag() is nwTrinary.NEUTRAL assert docEditor._processTag() == _TagAction.NONE
# On Tag Keyword # On Tag Keyword
docEditor.setCursorPosition(15) docEditor.setCursorPosition(15)
assert docEditor._processTag() is nwTrinary.NEUTRAL assert docEditor._processTag() == _TagAction.NONE
# On Known Tag, No Follow # On Known Tag, No Follow
docEditor.setCursorPosition(22) docEditor.setCursorPosition(22)
assert docEditor._processTag(follow=False) is nwTrinary.POSITIVE assert docEditor._processTag(follow=False) == _TagAction.FOLLOW
assert nwGUI.docViewer._docHandle is None assert nwGUI.docViewer._docHandle is None
# qtbot.stop()
# On Known Tag, Follow # On Known Tag, Follow
docEditor.setCursorPosition(22) docEditor.setCursorPosition(22)
position = docEditor.cursorRect().center() position = docEditor.cursorRect().center()
@@ -1723,13 +1724,13 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
# On Unknown Tag, Create It # On Unknown Tag, Create It
assert "0000000000011" not in SHARED.project.tree assert "0000000000011" not in SHARED.project.tree
docEditor.setCursorPosition(28) docEditor.setCursorPosition(28)
assert docEditor._processTag(create=True) is nwTrinary.NEGATIVE assert docEditor._processTag(create=True) == _TagAction.CREATE
assert "0000000000011" in SHARED.project.tree assert "0000000000011" in SHARED.project.tree
# On Unknown Tag, Missing Root # On Unknown Tag, Missing Root
assert "0000000000012" not in SHARED.project.tree assert "0000000000012" not in SHARED.project.tree
docEditor.setCursorPosition(42) docEditor.setCursorPosition(42)
assert docEditor._processTag(create=True) is nwTrinary.NEGATIVE assert docEditor._processTag(create=True) == _TagAction.CREATE
oHandle = SHARED.project.tree.findRoot(nwItemClass.OBJECT) oHandle = SHARED.project.tree.findRoot(nwItemClass.OBJECT)
assert oHandle == "0000000000012" assert oHandle == "0000000000012"
@@ -1738,7 +1739,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
assert oItem.itemParent == "0000000000012" assert oItem.itemParent == "0000000000012"
docEditor.setCursorPosition(47) docEditor.setCursorPosition(47)
assert docEditor._processTag() is nwTrinary.NEUTRAL assert docEditor._processTag() == _TagAction.NONE
# qtbot.stop() # qtbot.stop()
+12 -13
View File
@@ -25,7 +25,6 @@ import time
import pytest import pytest
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwTrinary
from tests.tools import C, buildTestProject from tests.tools import C, buildTestProject
@@ -47,20 +46,20 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
assert status._refTime == refTime assert status._refTime == refTime
# Project Status # Project Status
status.setProjectStatus(nwTrinary.NEUTRAL) status.setProjectStatus(None)
assert status.projIcon.state == nwTrinary.NEUTRAL assert status.projIcon.state is None
status.setProjectStatus(nwTrinary.NEGATIVE) status.setProjectStatus(False)
assert status.projIcon.state == nwTrinary.NEGATIVE assert status.projIcon.state is False
status.setProjectStatus(nwTrinary.POSITIVE) status.setProjectStatus(True)
assert status.projIcon.state == nwTrinary.POSITIVE assert status.projIcon.state is True
# Document Status # Document Status
status.setDocumentStatus(nwTrinary.NEUTRAL) status.setDocumentStatus(None)
assert status.docIcon.state == nwTrinary.NEUTRAL assert status.docIcon.state is None
status.setDocumentStatus(nwTrinary.NEGATIVE) status.setDocumentStatus(False)
assert status.docIcon.state == nwTrinary.NEGATIVE assert status.docIcon.state is False
status.setDocumentStatus(nwTrinary.POSITIVE) status.setDocumentStatus(True)
assert status.docIcon.state == nwTrinary.POSITIVE assert status.docIcon.state is True
# Idle Status # Idle Status
CONFIG.stopWhenIdle = False CONFIG.stopWhenIdle = False