Add context menu option to create note from tag (#1582)

This commit is contained in:
Veronica Berglyd Olsen
2023-11-06 22:48:06 +01:00
committed by GitHub
5 changed files with 90 additions and 38 deletions
+9
View File
@@ -61,6 +61,15 @@ class nwItemLayout(Enum):
# END Enum nwItemLayout
class nwTrinary(Enum):
NEGATIVE = -1
UNKNOWN = 0
POSITIVE = 1
# END Enum nwTrinary
class nwDocMode(Enum):
VIEW = 0
+44 -25
View File
@@ -51,9 +51,10 @@ from PyQt5.QtWidgets import (
)
from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass
from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
from novelwriter.common import minmax, transferCase
from novelwriter.constants import nwKeyWords, nwUnicode
from novelwriter.constants import nwKeyWords, nwLabels, nwUnicode, trConst
from novelwriter.core.item import NWItem
from novelwriter.core.index import countWords
from novelwriter.core.document import NWDocument
from novelwriter.gui.dochighlight import GuiDocHighlighter
@@ -149,17 +150,17 @@ class GuiDocEditor(QPlainTextEdit):
self.keyContext = QShortcut(self)
self.keyContext.setKey("Ctrl+.")
self.keyContext.setContext(Qt.WidgetShortcut)
self.keyContext.activated.connect(self._openSpellContext)
self.keyContext.activated.connect(self._openContextFromCursor)
self.followTag1 = QShortcut(self)
self.followTag1.setKey(Qt.Key_Return | Qt.ControlModifier)
self.followTag1.setContext(Qt.WidgetShortcut)
self.followTag1.activated.connect(self._followTag)
self.followTag1.activated.connect(self._processTag)
self.followTag2 = QShortcut(self)
self.followTag2.setKey(Qt.Key_Enter | Qt.ControlModifier)
self.followTag2.setContext(Qt.WidgetShortcut)
self.followTag2.activated.connect(self._followTag)
self.followTag2.activated.connect(self._processTag)
# Set Up Document Word Counter
self.wcTimerDoc = QTimer()
@@ -918,7 +919,7 @@ class GuiDocEditor(QPlainTextEdit):
follow tag function.
"""
if qApp.keyboardModifiers() == Qt.ControlModifier:
self._followTag(self.cursorForPosition(event.pos()))
self._processTag(self.cursorForPosition(event.pos()))
super().mouseReleaseEvent(event)
self.docFooter.updateLineCount()
return
@@ -974,10 +975,9 @@ class GuiDocEditor(QPlainTextEdit):
bPos = cursor.positionInBlock()
if bPos > 0:
show = self._completer.updateText(text, bPos)
if not self._completer.isVisible() and show:
point = self.cursorRect().bottomRight()
self._completer.move(self.viewport().mapToGlobal(point))
self._completer.show()
point = self.cursorRect().bottomRight()
self._completer.move(self.viewport().mapToGlobal(point))
self._completer.setVisible(show)
elif self._doReplace and added == 1:
self._docAutoReplace(text)
@@ -994,6 +994,7 @@ class GuiDocEditor(QPlainTextEdit):
cursor.setPosition(pos, QTextCursor.MoveMode.MoveAnchor)
cursor.setPosition(pos + length, QTextCursor.MoveMode.KeepAnchor)
cursor.insertText(text)
self._completer.hide()
return
@pyqtSlot("QPoint")
@@ -1007,9 +1008,14 @@ class GuiDocEditor(QPlainTextEdit):
ctxMenu = QMenu(self)
# Follow
if self._followTag(cursor=pCursor, loadTag=False):
status = self._processTag(cursor=pCursor, follow=False)
if status == nwTrinary.POSITIVE:
aTag = ctxMenu.addAction(self.tr("Follow Tag"))
aTag.triggered.connect(lambda: self._followTag(cursor=pCursor))
aTag.triggered.connect(lambda: self._processTag(cursor=pCursor, follow=True))
ctxMenu.addSeparator()
elif status == nwTrinary.NEGATIVE:
aTag = ctxMenu.addAction(self.tr("Create Note for Tag"))
aTag.triggered.connect(lambda: self._processTag(cursor=pCursor, create=True))
ctxMenu.addSeparator()
# Cut, Copy and Paste
@@ -1690,7 +1696,8 @@ class GuiDocEditor(QPlainTextEdit):
# Internal Functions
##
def _followTag(self, cursor: QTextCursor | None = None, loadTag: bool = True) -> bool:
def _processTag(self, cursor: QTextCursor | None = None,
follow: bool = True, create: bool = False) -> nwTrinary:
"""Activated by Ctrl+Enter. Checks that we're in a block
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
@@ -1702,41 +1709,53 @@ class GuiDocEditor(QPlainTextEdit):
block = cursor.block()
text = block.text()
if len(text) == 0:
return False
return nwTrinary.UNKNOWN
if text.startswith("@"):
if text.startswith("@") and isinstance(self._nwItem, NWItem):
isGood, tBits, tPos = SHARED.project.index.scanThis(text)
if not isGood:
return False
return nwTrinary.UNKNOWN
tag = ""
exist = False
cPos = cursor.selectionStart() - block.position()
for sTag, sPos in zip(reversed(tBits), reversed(tPos)):
tExist = SHARED.project.index.checkThese(tBits, self._nwItem)
for sTag, sPos, sExist in zip(reversed(tBits), reversed(tPos), reversed(tExist)):
if cPos >= sPos:
# The cursor is between the start of two tags
if cPos <= sPos + len(sTag):
# The cursor is inside or at the edge of the tag
tag = sTag
exist = sExist
break
if not tag or tag.startswith("@"):
# The keyword cannot be looked up, so we ignore that
return False
return nwTrinary.UNKNOWN
if loadTag:
if follow and exist:
logger.debug("Attempting to follow tag '%s'", tag)
self.loadDocumentTagRequest.emit(tag, nwDocMode.VIEW)
else:
logger.debug("Potential tag '%s'", tag)
elif create and not exist:
if SHARED.question(self.tr(
"Do you want to create a new project note for the tag '{0}'?"
).format(tag)):
itemClass = nwKeyWords.KEY_CLASS.get(tBits[0], nwItemClass.NO_CLASS)
if SHARED.mainGui.projView.createNewNote(tag, itemClass):
self._qDocument.syntaxHighlighter.rehighlightBlock(block)
else:
SHARED.error(self.tr(
"Could not create note in a root folder for '{0}'. "
"If one doesn't exist, you must create one first."
).format(trConst(nwLabels.CLASS_NAME[itemClass])))
return True
return nwTrinary.POSITIVE if exist else nwTrinary.NEGATIVE
return False
return nwTrinary.UNKNOWN
def _openSpellContext(self) -> None:
def _openContextFromCursor(self) -> None:
"""Open the spell check context menu at the cursor."""
self._openContextMenu(self.cursorRect().center())
return
+14
View File
@@ -147,6 +147,7 @@ class GuiProjectView(QWidget):
self.getSelectedHandle = self.projTree.getSelectedHandle
self.setSelectedHandle = self.projTree.setSelectedHandle
self.changedSince = self.projTree.changedSince
self.createNewNote = self.projTree.createNewNote
return
@@ -570,6 +571,19 @@ class GuiProjectTree(QTreeWidget):
self._timeChanged = 0.0
return
def createNewNote(self, tag: str, itemClass: nwItemClass | None) -> bool:
"""Create a new note. This function is used by the document
editor to create note files for unknown tags.
"""
rHandle = SHARED.project.tree.findRoot(itemClass)
if rHandle:
tHandle = SHARED.project.newFile(tag, rHandle)
if tHandle:
SHARED.project.writeNewFile(tHandle, 1, False, f"@tag: {tag}\n\n")
self.revealNewTreeItem(tHandle, wordCount=True)
return True
return False
def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None,
hLevel: int = 1, isNote: bool = False) -> bool:
"""Add new item to the tree, with a given itemType (and
+22 -12
View File
@@ -29,7 +29,7 @@ from PyQt5.QtGui import QTextBlock, QTextCursor, QTextOption
from PyQt5.QtWidgets import QAction, qApp
from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemLayout, nwWidget
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemLayout, nwTrinary, nwWidget
from novelwriter.constants import nwKeyWords, nwUnicode
from novelwriter.core.index import countWords
from novelwriter.gui.doceditor import GuiDocEditor
@@ -1041,7 +1041,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
assert nwGUI.openDocument(C.hSceneDoc) is True
# Create Scene
text = "### A Scene\n\n@char: Jane, John\n\n" + ipsumText[0] + "\n\n"
text = "### A Scene\n\n@char: Jane, John\n\n@object: Gun\n\n@:\n\n" + ipsumText[0] + "\n\n"
nwGUI.docEditor.replaceText(text)
# Create Character
@@ -1059,34 +1059,44 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
# Empty Block
nwGUI.docEditor.setCursorLine(2)
assert nwGUI.docEditor._followTag() is False
assert nwGUI.docEditor._processTag() is nwTrinary.UNKNOWN
# Not On Tag
nwGUI.docEditor.setCursorLine(1)
assert nwGUI.docEditor._followTag() is False
assert nwGUI.docEditor._processTag() is nwTrinary.UNKNOWN
# On Tag Keyword
nwGUI.docEditor.setCursorPosition(15)
assert nwGUI.docEditor._followTag() is False
# On Unknown Tag
nwGUI.docEditor.setCursorPosition(28)
assert nwGUI.docEditor._followTag() is True
assert nwGUI.docViewer._docHandle is None
assert nwGUI.docEditor._processTag() is nwTrinary.UNKNOWN
# On Known Tag, No Follow
nwGUI.docEditor.setCursorPosition(22)
assert nwGUI.docEditor._followTag(loadTag=False) is True
assert nwGUI.docEditor._processTag(follow=False) is nwTrinary.POSITIVE
assert nwGUI.docViewer._docHandle is None
# On Known Tag, Follow
nwGUI.docEditor.setCursorPosition(22)
assert nwGUI.docViewer._docHandle is None
assert nwGUI.docEditor._followTag(loadTag=True) is True
assert nwGUI.docEditor._processTag(follow=True) is nwTrinary.POSITIVE
assert nwGUI.docViewer._docHandle == cHandle
assert nwGUI.closeDocViewer() is True
assert nwGUI.docViewer._docHandle is None
# On Unknown Tag, Create It
assert "0000000000011" not in SHARED.project.tree
nwGUI.docEditor.setCursorPosition(28)
assert nwGUI.docEditor._processTag(create=True) is nwTrinary.NEGATIVE
assert "0000000000011" in SHARED.project.tree
# On Unknown Tag, Missing Root
assert "0000000000012" not in SHARED.project.tree
nwGUI.docEditor.setCursorPosition(42)
assert nwGUI.docEditor._processTag(create=True) is nwTrinary.NEGATIVE
assert "0000000000012" not in SHARED.project.tree
nwGUI.docEditor.setCursorPosition(47)
assert nwGUI.docEditor._processTag() is nwTrinary.UNKNOWN
# qtbot.stop()
# END Test testGuiEditor_Tags
+1 -1
View File
@@ -530,7 +530,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
with monkeypatch.context() as mp:
mp.setattr(QMenu, "exec_", lambda *a: None)
docEditor.setCursorPosition(errPos)
docEditor._openSpellContext()
docEditor._openContextFromCursor()
# Check Files
# ===========