Add test coverage of editor context menu
This commit is contained in:
@@ -1067,49 +1067,48 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
|
|
||||||
@pyqtSlot("QPoint")
|
@pyqtSlot("QPoint")
|
||||||
def _openContextMenu(self, pos: QPoint) -> None:
|
def _openContextMenu(self, pos: QPoint) -> None:
|
||||||
"""Triggered by right click to open the context menu. Also
|
"""Open the editor context menu at a given coordinate."""
|
||||||
triggered by the Ctrl+. shortcut.
|
|
||||||
"""
|
|
||||||
uCursor = self.textCursor()
|
uCursor = self.textCursor()
|
||||||
pCursor = self.cursorForPosition(pos)
|
pCursor = self.cursorForPosition(pos)
|
||||||
pBlock = pCursor.block()
|
pBlock = pCursor.block()
|
||||||
|
|
||||||
ctxMenu = QMenu(self)
|
ctxMenu = QMenu(self)
|
||||||
|
ctxMenu.setObjectName("ContextMenu")
|
||||||
if pBlock.userState() == GuiDocHighlighter.BLOCK_TITLE:
|
if pBlock.userState() == GuiDocHighlighter.BLOCK_TITLE:
|
||||||
aLabel = ctxMenu.addAction(self.tr("Set as Document Name"))
|
action = ctxMenu.addAction(self.tr("Set as Document Name"))
|
||||||
aLabel.triggered.connect(lambda: self._emitRenameItem(pBlock))
|
action.triggered.connect(lambda: self._emitRenameItem(pBlock))
|
||||||
|
|
||||||
# Follow
|
# Follow
|
||||||
status = self._processTag(cursor=pCursor, follow=False)
|
status = self._processTag(cursor=pCursor, follow=False)
|
||||||
if status == nwTrinary.POSITIVE:
|
if status == nwTrinary.POSITIVE:
|
||||||
aTag = ctxMenu.addAction(self.tr("Follow Tag"))
|
action = ctxMenu.addAction(self.tr("Follow Tag"))
|
||||||
aTag.triggered.connect(lambda: self._processTag(cursor=pCursor, follow=True))
|
action.triggered.connect(lambda: self._processTag(cursor=pCursor, follow=True))
|
||||||
ctxMenu.addSeparator()
|
ctxMenu.addSeparator()
|
||||||
elif status == nwTrinary.NEGATIVE:
|
elif status == nwTrinary.NEGATIVE:
|
||||||
aTag = ctxMenu.addAction(self.tr("Create Note for Tag"))
|
action = ctxMenu.addAction(self.tr("Create Note for Tag"))
|
||||||
aTag.triggered.connect(lambda: self._processTag(cursor=pCursor, create=True))
|
action.triggered.connect(lambda: self._processTag(cursor=pCursor, create=True))
|
||||||
ctxMenu.addSeparator()
|
ctxMenu.addSeparator()
|
||||||
|
|
||||||
# Cut, Copy and Paste
|
# Cut, Copy and Paste
|
||||||
if uCursor.hasSelection():
|
if uCursor.hasSelection():
|
||||||
aCut = ctxMenu.addAction(self.tr("Cut"))
|
action = ctxMenu.addAction(self.tr("Cut"))
|
||||||
aCut.triggered.connect(lambda: self.docAction(nwDocAction.CUT))
|
action.triggered.connect(lambda: self.docAction(nwDocAction.CUT))
|
||||||
aCopy = ctxMenu.addAction(self.tr("Copy"))
|
action = ctxMenu.addAction(self.tr("Copy"))
|
||||||
aCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
|
action.triggered.connect(lambda: self.docAction(nwDocAction.COPY))
|
||||||
|
|
||||||
aPaste = ctxMenu.addAction(self.tr("Paste"))
|
action = ctxMenu.addAction(self.tr("Paste"))
|
||||||
aPaste.triggered.connect(lambda: self.docAction(nwDocAction.PASTE))
|
action.triggered.connect(lambda: self.docAction(nwDocAction.PASTE))
|
||||||
ctxMenu.addSeparator()
|
ctxMenu.addSeparator()
|
||||||
|
|
||||||
# Selections
|
# Selections
|
||||||
aSAll = ctxMenu.addAction(self.tr("Select All"))
|
action = ctxMenu.addAction(self.tr("Select All"))
|
||||||
aSAll.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL))
|
action.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL))
|
||||||
aSWrd = ctxMenu.addAction(self.tr("Select Word"))
|
action = ctxMenu.addAction(self.tr("Select Word"))
|
||||||
aSWrd.triggered.connect(
|
action.triggered.connect(
|
||||||
lambda: self._makePosSelection(QTextCursor.SelectionType.WordUnderCursor, pos)
|
lambda: self._makePosSelection(QTextCursor.SelectionType.WordUnderCursor, pos)
|
||||||
)
|
)
|
||||||
aSPar = ctxMenu.addAction(self.tr("Select Paragraph"))
|
action = ctxMenu.addAction(self.tr("Select Paragraph"))
|
||||||
aSPar.triggered.connect(lambda: self._makePosSelection(
|
action.triggered.connect(lambda: self._makePosSelection(
|
||||||
QTextCursor.SelectionType.BlockUnderCursor, pos)
|
QTextCursor.SelectionType.BlockUnderCursor, pos)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1128,16 +1127,16 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
ctxMenu.addSeparator()
|
ctxMenu.addSeparator()
|
||||||
ctxMenu.addAction(self.tr("Spelling Suggestion(s)"))
|
ctxMenu.addAction(self.tr("Spelling Suggestion(s)"))
|
||||||
for option in suggest[:15]:
|
for option in suggest[:15]:
|
||||||
aFix = ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {option}")
|
action = ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {option}")
|
||||||
aFix.triggered.connect(
|
action.triggered.connect(
|
||||||
lambda _, option=option: self._correctWord(sCursor, option)
|
lambda _, option=option: self._correctWord(sCursor, option)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ctxMenu.addAction("%s %s" % (nwUnicode.U_ENDASH, self.tr("No Suggestions")))
|
ctxMenu.addAction("%s %s" % (nwUnicode.U_ENDASH, self.tr("No Suggestions")))
|
||||||
|
|
||||||
ctxMenu.addSeparator()
|
ctxMenu.addSeparator()
|
||||||
aAdd = ctxMenu.addAction(self.tr("Add Word to Dictionary"))
|
action = ctxMenu.addAction(self.tr("Add Word to Dictionary"))
|
||||||
aAdd.triggered.connect(lambda: self._addWord(word, block))
|
action.triggered.connect(lambda: self._addWord(word, block))
|
||||||
|
|
||||||
# Execute the context menu
|
# Execute the context menu
|
||||||
ctxMenu.exec_(self.viewport().mapToGlobal(pos))
|
ctxMenu.exec_(self.viewport().mapToGlobal(pos))
|
||||||
|
|||||||
@@ -20,13 +20,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from novelwriter.dialogs.editlabel import GuiEditLabel
|
||||||
|
|
||||||
from tools import C, buildTestProject
|
from tools import C, buildTestProject
|
||||||
from mocked import causeOSError
|
from mocked import causeOSError
|
||||||
|
|
||||||
from PyQt5.QtGui import QTextBlock, QTextCursor, QTextOption
|
from PyQt5.QtGui import QClipboard, QTextBlock, QTextCursor, QTextOption
|
||||||
from PyQt5.QtCore import QThreadPool, Qt
|
from PyQt5.QtCore import QThreadPool, Qt
|
||||||
from PyQt5.QtWidgets import QAction, qApp
|
from PyQt5.QtWidgets import QAction, QMenu, qApp
|
||||||
|
|
||||||
from novelwriter import CONFIG, SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemLayout, nwTrinary, nwWidget
|
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemLayout, nwTrinary, nwWidget
|
||||||
@@ -209,6 +210,148 @@ def testGuiEditor_MetaData(qtbot, nwGUI, projPath, mockRnd):
|
|||||||
# END Test testGuiEditor_MetaData
|
# END Test testGuiEditor_MetaData
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.gui
|
||||||
|
def testGuiEditor_ContextMenu(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
|
||||||
|
"""Test the editor context menu."""
|
||||||
|
monkeypatch.setattr(QMenu, "exec_", lambda *a: None)
|
||||||
|
|
||||||
|
buildTestProject(nwGUI, projPath)
|
||||||
|
assert nwGUI.openDocument(C.hSceneDoc) is True
|
||||||
|
docEditor = nwGUI.docEditor
|
||||||
|
sceneItem = SHARED.project.tree[C.hSceneDoc]
|
||||||
|
assert sceneItem is not None
|
||||||
|
|
||||||
|
def getMenuForPos(pos: int, select: bool = False) -> QMenu | None:
|
||||||
|
nonlocal docEditor
|
||||||
|
cursor = docEditor.textCursor()
|
||||||
|
cursor.setPosition(pos)
|
||||||
|
if select:
|
||||||
|
cursor.select(QTextCursor.SelectionType.WordUnderCursor)
|
||||||
|
docEditor.setTextCursor(cursor)
|
||||||
|
docEditor._openContextMenu(docEditor.cursorRect().center())
|
||||||
|
for obj in docEditor.children():
|
||||||
|
if isinstance(obj, QMenu) and obj.objectName() == "ContextMenu":
|
||||||
|
return obj
|
||||||
|
return None
|
||||||
|
|
||||||
|
docText = (
|
||||||
|
"### A Scene\n\n"
|
||||||
|
"@pov: Jane\n"
|
||||||
|
"Some text ..."
|
||||||
|
)
|
||||||
|
docEditor.setPlainText(docText)
|
||||||
|
assert docEditor.getText() == docText
|
||||||
|
|
||||||
|
# Rename Item from Heading
|
||||||
|
ctxMenu = getMenuForPos(1)
|
||||||
|
assert ctxMenu is not None
|
||||||
|
actions = [x.text() for x in ctxMenu.actions() if x.text()]
|
||||||
|
assert actions == [
|
||||||
|
"Set as Document Name", "Paste",
|
||||||
|
"Select All", "Select Word", "Select Paragraph"
|
||||||
|
]
|
||||||
|
with monkeypatch.context() as mp:
|
||||||
|
mp.setattr(GuiEditLabel, "getLabel", lambda a, text: (text, True))
|
||||||
|
assert sceneItem.itemName == "New Scene"
|
||||||
|
ctxMenu.actions()[0].trigger()
|
||||||
|
assert sceneItem.itemName == "A Scene"
|
||||||
|
ctxMenu.setObjectName("")
|
||||||
|
ctxMenu.deleteLater()
|
||||||
|
|
||||||
|
# Create Character
|
||||||
|
ctxMenu = getMenuForPos(21)
|
||||||
|
assert ctxMenu is not None
|
||||||
|
actions = [x.text() for x in ctxMenu.actions() if x.text()]
|
||||||
|
assert actions == [
|
||||||
|
"Create Note for Tag", "Paste",
|
||||||
|
"Select All", "Select Word", "Select Paragraph"
|
||||||
|
]
|
||||||
|
ctxMenu.actions()[0].trigger()
|
||||||
|
janeItem = SHARED.project.tree["0000000000010"]
|
||||||
|
assert janeItem is not None
|
||||||
|
assert janeItem.itemName == "Jane"
|
||||||
|
ctxMenu.setObjectName("")
|
||||||
|
ctxMenu.deleteLater()
|
||||||
|
|
||||||
|
# Follow Character Tag
|
||||||
|
ctxMenu = getMenuForPos(21)
|
||||||
|
assert ctxMenu is not None
|
||||||
|
actions = [x.text() for x in ctxMenu.actions() if x.text()]
|
||||||
|
assert actions == [
|
||||||
|
"Follow Tag", "Paste",
|
||||||
|
"Select All", "Select Word", "Select Paragraph"
|
||||||
|
]
|
||||||
|
ctxMenu.actions()[0].trigger()
|
||||||
|
assert nwGUI.docViewer.docHandle == "0000000000010"
|
||||||
|
ctxMenu.setObjectName("")
|
||||||
|
ctxMenu.deleteLater()
|
||||||
|
|
||||||
|
# Select Word
|
||||||
|
ctxMenu = getMenuForPos(31)
|
||||||
|
assert ctxMenu is not None
|
||||||
|
actions = [x.text() for x in ctxMenu.actions() if x.text()]
|
||||||
|
assert actions == [
|
||||||
|
"Paste", "Select All", "Select Word", "Select Paragraph"
|
||||||
|
]
|
||||||
|
ctxMenu.actions()[3].trigger()
|
||||||
|
assert docEditor.textCursor().selectedText() == "text"
|
||||||
|
ctxMenu.setObjectName("")
|
||||||
|
ctxMenu.deleteLater()
|
||||||
|
|
||||||
|
# Select Paragraph
|
||||||
|
ctxMenu = getMenuForPos(31)
|
||||||
|
assert ctxMenu is not None
|
||||||
|
actions = [x.text() for x in ctxMenu.actions() if x.text()]
|
||||||
|
assert actions == [
|
||||||
|
"Paste", "Select All", "Select Word", "Select Paragraph"
|
||||||
|
]
|
||||||
|
ctxMenu.actions()[4].trigger()
|
||||||
|
assert docEditor.textCursor().selectedText() == "Some text ..."
|
||||||
|
ctxMenu.setObjectName("")
|
||||||
|
ctxMenu.deleteLater()
|
||||||
|
|
||||||
|
# Select All
|
||||||
|
ctxMenu = getMenuForPos(31)
|
||||||
|
assert ctxMenu is not None
|
||||||
|
actions = [x.text() for x in ctxMenu.actions() if x.text()]
|
||||||
|
assert actions == [
|
||||||
|
"Paste", "Select All", "Select Word", "Select Paragraph"
|
||||||
|
]
|
||||||
|
ctxMenu.actions()[2].trigger()
|
||||||
|
assert docEditor.textCursor().selectedText() == docEditor.document().toRawText()
|
||||||
|
ctxMenu.setObjectName("")
|
||||||
|
ctxMenu.deleteLater()
|
||||||
|
|
||||||
|
# Copy Text
|
||||||
|
ctxMenu = getMenuForPos(31, True)
|
||||||
|
assert ctxMenu is not None
|
||||||
|
assert docEditor.textCursor().selectedText() == "text"
|
||||||
|
actions = [x.text() for x in ctxMenu.actions() if x.text()]
|
||||||
|
assert actions == [
|
||||||
|
"Cut", "Copy", "Paste", "Select All", "Select Word", "Select Paragraph"
|
||||||
|
]
|
||||||
|
qApp.clipboard().clear()
|
||||||
|
ctxMenu.actions()[1].trigger()
|
||||||
|
assert qApp.clipboard().text(QClipboard.Mode.Clipboard) == "text"
|
||||||
|
|
||||||
|
# Cut Text
|
||||||
|
qApp.clipboard().clear()
|
||||||
|
ctxMenu.actions()[0].trigger()
|
||||||
|
assert qApp.clipboard().text(QClipboard.Mode.Clipboard) == "text"
|
||||||
|
assert "text" not in docEditor.getText()
|
||||||
|
|
||||||
|
# Paste Text
|
||||||
|
ctxMenu.actions()[2].trigger()
|
||||||
|
assert docEditor.getText() == docText
|
||||||
|
|
||||||
|
ctxMenu.setObjectName("")
|
||||||
|
ctxMenu.deleteLater()
|
||||||
|
|
||||||
|
# qtbot.stop()
|
||||||
|
|
||||||
|
# END Test testGuiEditor_ContextMenu
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd):
|
def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd):
|
||||||
"""Test the document actions. This is not an extensive test of the
|
"""Test the document actions. This is not an extensive test of the
|
||||||
|
|||||||
Reference in New Issue
Block a user