diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 91e4b57f..fc513485 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -83,8 +83,8 @@ class GuiDocEditor(QPlainTextEdit): """Gui Widget: Main Document Editor""" MOVE_KEYS = ( - Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down, - Qt.Key_PageUp, Qt.Key_PageDown + Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down, + Qt.Key.Key_PageUp, Qt.Key.Key_PageDown ) # Custom Signals @@ -97,6 +97,8 @@ class GuiDocEditor(QPlainTextEdit): spellCheckStateChanged = pyqtSignal(bool) closeDocumentRequest = pyqtSignal() toggleFocusModeRequest = pyqtSignal() + requestProjectItemSelected = pyqtSignal(str, bool) + requestProjectItemRenamed = pyqtSignal(str, str) def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) @@ -157,28 +159,28 @@ class GuiDocEditor(QPlainTextEdit): self.docToolBar.requestDocAction.connect(self.docAction) # Context Menu - self.setContextMenuPolicy(Qt.CustomContextMenu) + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect(self._openContextMenu) # Editor Settings self.setMinimumWidth(CONFIG.pxInt(300)) self.setAutoFillBackground(True) - self.setFrameStyle(QFrame.NoFrame) + self.setFrameStyle(QFrame.Shape.NoFrame) # Custom Shortcuts self.keyContext = QShortcut(self) self.keyContext.setKey("Ctrl+.") - self.keyContext.setContext(Qt.WidgetShortcut) + self.keyContext.setContext(Qt.ShortcutContext.WidgetShortcut) 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.setKey(Qt.Key.Key_Return | Qt.KeyboardModifier.ControlModifier) + self.followTag1.setContext(Qt.ShortcutContext.WidgetShortcut) 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.setKey(Qt.Key.Key_Enter | Qt.KeyboardModifier.ControlModifier) + self.followTag2.setContext(Qt.ShortcutContext.WidgetShortcut) self.followTag2.activated.connect(self._processTag) # Set Up Document Word Counter @@ -274,14 +276,14 @@ class GuiDocEditor(QPlainTextEdit): def updateSyntaxColours(self) -> None: """Update the syntax highlighting theme.""" mainPalette = self.palette() - mainPalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack)) - mainPalette.setColor(QPalette.Base, QColor(*SHARED.theme.colBack)) - mainPalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) + mainPalette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) + mainPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack)) + mainPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) self.setPalette(mainPalette) docPalette = self.viewport().palette() - docPalette.setColor(QPalette.Base, QColor(*SHARED.theme.colBack)) - docPalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) + docPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack)) + docPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) self.viewport().setPalette(docPalette) self.docHeader.matchColours() @@ -333,25 +335,25 @@ class GuiDocEditor(QPlainTextEdit): options = QTextOption() if CONFIG.doJustify: - options.setAlignment(Qt.AlignJustify) + options.setAlignment(Qt.AlignmentFlag.AlignJustify) if CONFIG.showTabsNSpaces: - options.setFlags(options.flags() | QTextOption.ShowTabsAndSpaces) + options.setFlags(options.flags() | QTextOption.Flag.ShowTabsAndSpaces) if CONFIG.showLineEndings: - options.setFlags(options.flags() | QTextOption.ShowLineAndParagraphSeparators) + options.setFlags(options.flags() | QTextOption.Flag.ShowLineAndParagraphSeparators) self._qDocument.setDefaultTextOption(options) # Scrolling self.setCenterOnScroll(CONFIG.scrollPastEnd) if CONFIG.hideVScroll: - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) else: - self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) if CONFIG.hideHScroll: - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) else: - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) # Refresh the tab stops self.setTabStopDistance(CONFIG.getTabWidth()) @@ -370,7 +372,7 @@ class GuiDocEditor(QPlainTextEdit): return - def loadText(self, tHandle, tLine=None) -> bool: + def loadText(self, tHandle: str, tLine=None) -> bool: """Load text from a document into the editor. If we have an I/O error, we must handle this and clear the editor so that we don't risk overwriting the file if it exists. This can for instance @@ -388,7 +390,7 @@ class GuiDocEditor(QPlainTextEdit): self.clearEditor() return False - qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) + qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) self._docHandle = tHandle self._allowAutoReplace(False) @@ -441,7 +443,7 @@ class GuiDocEditor(QPlainTextEdit): """Replace the text of the current document with the provided text. This also clears undo history. """ - qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) + qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) self.setPlainText(text) self.updateDocMargins() self.setDocumentChanged(True) @@ -676,7 +678,7 @@ class GuiDocEditor(QPlainTextEdit): """ logger.debug("Running spell checker") start = time() - qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) + qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) self._qDocument.syntaxHighlighter.rehighlight() qApp.restoreOverrideCursor() logger.debug("Document highlighted in %.3f ms", 1000*(time() - start)) @@ -726,9 +728,9 @@ class GuiDocEditor(QPlainTextEdit): elif action == nwDocAction.D_QUOTE: self._wrapSelection(self._typDQuoteO, self._typDQuoteC) elif action == nwDocAction.SEL_ALL: - self._makeSelection(QTextCursor.Document) + self._makeSelection(QTextCursor.SelectionType.Document) elif action == nwDocAction.SEL_PARA: - self._makeSelection(QTextCursor.BlockUnderCursor) + self._makeSelection(QTextCursor.SelectionType.BlockUnderCursor) elif action == nwDocAction.BLOCK_H1: self._formatBlock(nwDocAction.BLOCK_H1) elif action == nwDocAction.BLOCK_H2: @@ -909,17 +911,17 @@ class GuiDocEditor(QPlainTextEdit): * We also handle automatic scrolling here. """ self._lastActive = time() - isReturn = event.key() == Qt.Key_Return - isReturn |= event.key() == Qt.Key_Enter + isReturn = event.key() == Qt.Key.Key_Return + isReturn |= event.key() == Qt.Key.Key_Enter if isReturn and self.docSearch.anyFocus(): return - elif event == QKeySequence.Redo: + elif event == QKeySequence.StandardKey.Redo: self.docAction(nwDocAction.REDO) return - elif event == QKeySequence.Undo: + elif event == QKeySequence.StandardKey.Undo: self.docAction(nwDocAction.UNDO) return - elif event == QKeySequence.SelectAll: + elif event == QKeySequence.StandardKey.SelectAll: self.docAction(nwDocAction.SEL_ALL) return @@ -928,7 +930,7 @@ class GuiDocEditor(QPlainTextEdit): super().keyPressEvent(event) nPos = self.cursorRect().topLeft().y() kMod = event.modifiers() - okMod = kMod == Qt.NoModifier or kMod == Qt.ShiftModifier + okMod = kMod in (Qt.KeyboardModifier.NoModifier, Qt.KeyboardModifier.ShiftModifier) okKey = event.key() not in self.MOVE_KEYS if nPos != cPos and okMod and okKey: mPos = CONFIG.autoScrollPos*0.01 * self.viewport().height() @@ -959,7 +961,7 @@ class GuiDocEditor(QPlainTextEdit): pressed, check if we're clicking on a tag, and trigger the follow tag function. """ - if qApp.keyboardModifiers() == Qt.ControlModifier: + if qApp.keyboardModifiers() == Qt.KeyboardModifier.ControlModifier: self._processTag(self.cursorForPosition(event.pos())) super().mouseReleaseEvent(event) self.docFooter.updateLineCount() @@ -1065,43 +1067,50 @@ class GuiDocEditor(QPlainTextEdit): @pyqtSlot("QPoint") def _openContextMenu(self, pos: QPoint) -> None: - """Triggered by right click to open the context menu. Also - triggered by the Ctrl+. shortcut. - """ + """Open the editor context menu at a given coordinate.""" uCursor = self.textCursor() pCursor = self.cursorForPosition(pos) + pBlock = pCursor.block() ctxMenu = QMenu(self) + ctxMenu.setObjectName("ContextMenu") + if pBlock.userState() == GuiDocHighlighter.BLOCK_TITLE: + action = ctxMenu.addAction(self.tr("Set as Document Name")) + action.triggered.connect(lambda: self._emitRenameItem(pBlock)) # Follow status = self._processTag(cursor=pCursor, follow=False) if status == nwTrinary.POSITIVE: - aTag = ctxMenu.addAction(self.tr("Follow Tag")) - aTag.triggered.connect(lambda: self._processTag(cursor=pCursor, follow=True)) + action = ctxMenu.addAction(self.tr("Follow Tag")) + action.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)) + action = ctxMenu.addAction(self.tr("Create Note for Tag")) + action.triggered.connect(lambda: self._processTag(cursor=pCursor, create=True)) ctxMenu.addSeparator() # Cut, Copy and Paste if uCursor.hasSelection(): - aCut = ctxMenu.addAction(self.tr("Cut")) - aCut.triggered.connect(lambda: self.docAction(nwDocAction.CUT)) - aCopy = ctxMenu.addAction(self.tr("Copy")) - aCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY)) + action = ctxMenu.addAction(self.tr("Cut")) + action.triggered.connect(lambda: self.docAction(nwDocAction.CUT)) + action = ctxMenu.addAction(self.tr("Copy")) + action.triggered.connect(lambda: self.docAction(nwDocAction.COPY)) - aPaste = ctxMenu.addAction(self.tr("Paste")) - aPaste.triggered.connect(lambda: self.docAction(nwDocAction.PASTE)) + action = ctxMenu.addAction(self.tr("Paste")) + action.triggered.connect(lambda: self.docAction(nwDocAction.PASTE)) ctxMenu.addSeparator() # Selections - aSAll = ctxMenu.addAction(self.tr("Select All")) - aSAll.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL)) - aSWrd = ctxMenu.addAction(self.tr("Select Word")) - aSWrd.triggered.connect(lambda: self._makePosSelection(QTextCursor.WordUnderCursor, pos)) - aSPar = ctxMenu.addAction(self.tr("Select Paragraph")) - aSPar.triggered.connect(lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, pos)) + action = ctxMenu.addAction(self.tr("Select All")) + action.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL)) + action = ctxMenu.addAction(self.tr("Select Word")) + action.triggered.connect( + lambda: self._makePosSelection(QTextCursor.SelectionType.WordUnderCursor, pos) + ) + action = ctxMenu.addAction(self.tr("Select Paragraph")) + action.triggered.connect(lambda: self._makePosSelection( + QTextCursor.SelectionType.BlockUnderCursor, pos) + ) # Spell Checking if SHARED.project.data.spellCheck: @@ -1111,21 +1120,23 @@ class GuiDocEditor(QPlainTextEdit): block = pCursor.block() sCursor = self.textCursor() sCursor.setPosition(block.position() + cPos) - sCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, cLen) + sCursor.movePosition( + QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor, cLen + ) if suggest: ctxMenu.addSeparator() ctxMenu.addAction(self.tr("Spelling Suggestion(s)")) for option in suggest[:15]: - aFix = ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {option}") - aFix.triggered.connect( + action = ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {option}") + action.triggered.connect( lambda _, option=option: self._correctWord(sCursor, option) ) else: ctxMenu.addAction("%s %s" % (nwUnicode.U_ENDASH, self.tr("No Suggestions"))) ctxMenu.addSeparator() - aAdd = ctxMenu.addAction(self.tr("Add Word to Dictionary")) - aAdd.triggered.connect(lambda: self._addWord(word, block)) + action = ctxMenu.addAction(self.tr("Add Word to Dictionary")) + action.triggered.connect(lambda: self._addWord(word, block)) # Execute the context menu ctxMenu.exec_(self.viewport().mapToGlobal(pos)) @@ -1316,8 +1327,8 @@ class GuiDocEditor(QPlainTextEdit): else: resIdx = 0 if doLoop else maxIdx - cursor.setPosition(resS[resIdx], QTextCursor.MoveAnchor) - cursor.setPosition(resE[resIdx], QTextCursor.KeepAnchor) + cursor.setPosition(resS[resIdx], QTextCursor.MoveMode.MoveAnchor) + cursor.setPosition(resE[resIdx], QTextCursor.MoveMode.KeepAnchor) self.setTextCursor(cursor) self.docFooter.updateLineCount() @@ -1343,9 +1354,9 @@ class GuiDocEditor(QPlainTextEdit): findOpt = QTextDocument.FindFlag(0) if self.docSearch.isCaseSense: - findOpt |= QTextDocument.FindCaseSensitively + findOpt |= QTextDocument.FindFlag.FindCaseSensitively if self.docSearch.isWholeWord: - findOpt |= QTextDocument.FindWholeWords + findOpt |= QTextDocument.FindFlag.FindWholeWords searchFor = self.docSearch.getSearchObject() cursor.setPosition(0) @@ -1363,8 +1374,8 @@ class GuiDocEditor(QPlainTextEdit): break if hasSelection: - cursor.setPosition(origA, QTextCursor.MoveAnchor) - cursor.setPosition(origB, QTextCursor.KeepAnchor) + cursor.setPosition(origA, QTextCursor.MoveMode.MoveAnchor) + cursor.setPosition(origB, QTextCursor.MoveMode.KeepAnchor) else: cursor.setPosition(origA) @@ -1475,8 +1486,8 @@ class GuiDocEditor(QPlainTextEdit): if blockS != blockE: posE = blockS.position() + blockS.length() - 1 cursor.clearSelection() - cursor.setPosition(posS, QTextCursor.MoveAnchor) - cursor.setPosition(posE, QTextCursor.KeepAnchor) + cursor.setPosition(posS, QTextCursor.MoveMode.MoveAnchor) + cursor.setPosition(posE, QTextCursor.MoveMode.KeepAnchor) self.setTextCursor(cursor) numB = 0 @@ -1578,7 +1589,9 @@ class GuiDocEditor(QPlainTextEdit): self._allowAutoReplace(False) for posC in range(posS, posE+1): cursor.setPosition(posC) - cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2) + cursor.movePosition( + QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.KeepAnchor, 2 + ) selText = cursor.selectedText() nS = len(selText) @@ -1598,12 +1611,16 @@ class GuiDocEditor(QPlainTextEdit): cursor.setPosition(posC) if pC in closeCheck: cursor.beginEditBlock() - cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1) + cursor.movePosition( + QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.KeepAnchor, 1 + ) cursor.insertText(oQuote) cursor.endEditBlock() else: cursor.beginEditBlock() - cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1) + cursor.movePosition( + QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.KeepAnchor, 1 + ) cursor.insertText(cQuote) cursor.endEditBlock() @@ -1723,7 +1740,7 @@ class GuiDocEditor(QPlainTextEdit): # Replace the block text cursor.beginEditBlock() posO = cursor.position() - cursor.select(QTextCursor.BlockUnderCursor) + cursor.select(QTextCursor.SelectionType.BlockUnderCursor) posS = cursor.selectionStart() cursor.removeSelectedText() cursor.setPosition(posS) @@ -1783,7 +1800,9 @@ class GuiDocEditor(QPlainTextEdit): cursor.beginEditBlock() cursor.clearSelection() cursor.setPosition(rS) - cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, rE-rS) + cursor.movePosition( + QTextCursor.MoveOperation.Right, QTextCursor.MoveMode.KeepAnchor, rE-rS + ) cursor.insertText(cleanText.rstrip() + "\n") cursor.endEditBlock() @@ -1852,6 +1871,13 @@ class GuiDocEditor(QPlainTextEdit): return nwTrinary.NEUTRAL + def _emitRenameItem(self, block: QTextBlock) -> None: + """Emit a signal to request an item be renamed.""" + if self._docHandle: + text = block.text().lstrip("#").lstrip("!").strip() + self.requestProjectItemRenamed.emit(self._docHandle, text) + return + def _openContextFromCursor(self) -> None: """Open the spell check context menu at the cursor.""" self._openContextMenu(self.cursorRect().center()) @@ -1943,7 +1969,9 @@ class GuiDocEditor(QPlainTextEdit): tInsert = tInsert + self._typPadChar if nDelete > 0: - cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, nDelete) + cursor.movePosition( + QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.KeepAnchor, nDelete + ) cursor.insertText(tInsert) return @@ -1994,8 +2022,8 @@ class GuiDocEditor(QPlainTextEdit): return cursor cursor.clearSelection() - cursor.setPosition(sPos, QTextCursor.MoveAnchor) - cursor.setPosition(ePos, QTextCursor.KeepAnchor) + cursor.setPosition(sPos, QTextCursor.MoveMode.MoveAnchor) + cursor.setPosition(ePos, QTextCursor.MoveMode.KeepAnchor) self.setTextCursor(cursor) @@ -2007,18 +2035,18 @@ class GuiDocEditor(QPlainTextEdit): cursor.clearSelection() cursor.select(mode) - if mode == QTextCursor.WordUnderCursor: + if mode == QTextCursor.SelectionType.WordUnderCursor: cursor = self._autoSelect() - elif mode == QTextCursor.BlockUnderCursor: + elif mode == QTextCursor.SelectionType.BlockUnderCursor: # This selection mode also selects the preceding paragraph # separator, which we want to avoid. posS = cursor.selectionStart() posE = cursor.selectionEnd() selTxt = cursor.selectedText() if selTxt.startswith(nwUnicode.U_PSEP): - cursor.setPosition(posS+1, QTextCursor.MoveAnchor) - cursor.setPosition(posE, QTextCursor.KeepAnchor) + cursor.setPosition(posS+1, QTextCursor.MoveMode.MoveAnchor) + cursor.setPosition(posE, QTextCursor.MoveMode.KeepAnchor) self.setTextCursor(cursor) @@ -2100,7 +2128,10 @@ class MetaCompleter(QMenu): def keyPressEvent(self, event: QKeyEvent) -> None: """Capture keypresses and forward most of them to the editor.""" parent = self.parent() - if event.key() in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Return, Qt.Key_Enter, Qt.Key_Escape): + if event.key() in ( + Qt.Key.Key_Up, Qt.Key.Key_Down, Qt.Key.Key_Return, + Qt.Key.Key_Enter, Qt.Key.Key_Escape + ): super().keyPressEvent(event) elif isinstance(parent, GuiDocEditor): parent.keyPressEvent(event) @@ -2254,9 +2285,9 @@ class GuiDocToolBar(QWidget): def updateTheme(self) -> None: """Initialise GUI elements that depend on specific settings.""" palette = QPalette() - palette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack)) - palette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText)) - palette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) + palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) + palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText)) + palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) self.setPalette(palette) tPx = int(0.8*SHARED.theme.fontPixelSize) @@ -2336,7 +2367,7 @@ class GuiDocEditSearch(QFrame): self.setContentsMargins(0, 0, 0, 0) self.setAutoFillBackground(True) - self.setFrameStyle(QFrame.StyledPanel | QFrame.Plain) + self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Plain) self.mainBox = QGridLayout(self) self.setLayout(self.mainBox) @@ -2355,7 +2386,7 @@ class GuiDocEditSearch(QFrame): self.replaceBox.returnPressed.connect(self._doReplace) self.searchOpt = QToolBar(self) - self.searchOpt.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.searchOpt.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.searchOpt.setIconSize(QSize(tPx, tPx)) self.searchOpt.setContentsMargins(0, 0, 0, 0) @@ -2417,7 +2448,7 @@ class GuiDocEditSearch(QFrame): bPx = self.searchBox.sizeHint().height() self.showReplace = QToolButton(self) - self.showReplace.setArrowType(Qt.RightArrow) + self.showReplace.setArrowType(Qt.ArrowType.RightArrow) self.showReplace.setCheckable(True) self.showReplace.toggled.connect(self._doToggleReplace) @@ -2431,8 +2462,8 @@ class GuiDocEditSearch(QFrame): self.replaceButton.setToolTip(self.tr("Find and replace in current document")) self.replaceButton.clicked.connect(self._doReplace) - self.mainBox.addWidget(self.searchLabel, 0, 0, 1, 2, Qt.AlignLeft) - self.mainBox.addWidget(self.searchOpt, 0, 2, 1, 3, Qt.AlignRight) + self.mainBox.addWidget(self.searchLabel, 0, 0, 1, 2, Qt.AlignmentFlag.AlignLeft) + self.mainBox.addWidget(self.searchOpt, 0, 2, 1, 3, Qt.AlignmentFlag.AlignRight) self.mainBox.addWidget(self.showReplace, 1, 0, 1, 1) self.mainBox.addWidget(self.searchBox, 1, 1, 1, 2) self.mainBox.addWidget(self.searchButton, 1, 3, 1, 1) @@ -2490,18 +2521,18 @@ class GuiDocEditSearch(QFrame): # only added in Qt 5.13. Otherwise, 5.3 and up supports # only the QRegExp class. if CONFIG.verQtValue >= 0x050d00: - rxOpt = QRegularExpression.UseUnicodePropertiesOption + rxOpt = QRegularExpression.PatternOption.UseUnicodePropertiesOption if not self.isCaseSense: - rxOpt |= QRegularExpression.CaseInsensitiveOption + rxOpt |= QRegularExpression.PatternOption.CaseInsensitiveOption regEx = QRegularExpression(text, rxOpt) self._alertSearchValid(regEx.isValid()) return regEx else: # pragma: no cover # >= 50300 to < 51300 if self.isCaseSense: - rxOpt = Qt.CaseSensitive + rxOpt = Qt.CaseSensitivity.CaseSensitive else: - rxOpt = Qt.CaseInsensitive + rxOpt = Qt.CaseSensitivity.CaseInsensitive regEx = QRegExp(text, rxOpt) self._alertSearchValid(regEx.isValid()) return regEx @@ -2637,7 +2668,7 @@ class GuiDocEditSearch(QFrame): def _doSearch(self) -> None: """Call the search action function for the document editor.""" modKey = qApp.keyboardModifiers() - if modKey == Qt.ShiftModifier: + if modKey == Qt.KeyboardModifier.ShiftModifier: self.docEditor.findNext(goBack=True) else: self.docEditor.findNext() @@ -2653,9 +2684,9 @@ class GuiDocEditSearch(QFrame): def _doToggleReplace(self, state: bool) -> None: """Toggle the show/hide of the replace box.""" if state: - self.showReplace.setArrowType(Qt.DownArrow) + self.showReplace.setArrowType(Qt.ArrowType.DownArrow) else: - self.showReplace.setArrowType(Qt.RightArrow) + self.showReplace.setArrowType(Qt.ArrowType.RightArrow) self.replaceBox.setVisible(state) self.replaceButton.setVisible(state) self.repVisible = state @@ -2708,7 +2739,7 @@ class GuiDocEditSearch(QFrame): isn't valid. Take the colour from the replace box. """ qPalette = self.replaceBox.palette() - qPalette.setColor(QPalette.Base, self.rxCol[isValid]) + qPalette.setColor(QPalette.ColorRole.Base, self.rxCol[isValid]) self.searchBox.setPalette(qPalette) return @@ -2749,7 +2780,7 @@ class GuiDocEditHeader(QWidget): self.itemTitle.setMargin(0) self.itemTitle.setContentsMargins(0, 0, 0, 0) self.itemTitle.setAutoFillBackground(True) - self.itemTitle.setAlignment(Qt.AlignHCenter | Qt.AlignTop) + self.itemTitle.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop) self.itemTitle.setFixedHeight(fPx) lblFont = self.itemTitle.font() @@ -2761,7 +2792,7 @@ class GuiDocEditHeader(QWidget): self.tbButton.setContentsMargins(0, 0, 0, 0) self.tbButton.setIconSize(iconSize) self.tbButton.setFixedSize(fPx, fPx) - self.tbButton.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.tbButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.tbButton.setVisible(False) self.tbButton.setToolTip(self.tr("Toggle Tool Bar")) self.tbButton.clicked.connect(lambda: self.toggleToolBarRequest.emit()) @@ -2770,7 +2801,7 @@ class GuiDocEditHeader(QWidget): self.searchButton.setContentsMargins(0, 0, 0, 0) self.searchButton.setIconSize(iconSize) self.searchButton.setFixedSize(fPx, fPx) - self.searchButton.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.searchButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.searchButton.setVisible(False) self.searchButton.setToolTip(self.tr("Search")) self.searchButton.clicked.connect(self.docEditor.toggleSearch) @@ -2779,7 +2810,7 @@ class GuiDocEditHeader(QWidget): self.minmaxButton.setContentsMargins(0, 0, 0, 0) self.minmaxButton.setIconSize(iconSize) self.minmaxButton.setFixedSize(fPx, fPx) - self.minmaxButton.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.minmaxButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.minmaxButton.setVisible(False) self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode")) self.minmaxButton.clicked.connect(lambda: self.docEditor.toggleFocusModeRequest.emit()) @@ -2788,7 +2819,7 @@ class GuiDocEditHeader(QWidget): self.closeButton.setContentsMargins(0, 0, 0, 0) self.closeButton.setIconSize(iconSize) self.closeButton.setFixedSize(fPx, fPx) - self.closeButton.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.closeButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.closeButton.setVisible(False) self.closeButton.setToolTip(self.tr("Close")) self.closeButton.clicked.connect(self._closeDocument) @@ -2846,9 +2877,9 @@ class GuiDocEditHeader(QWidget): theme rather than the main GUI. """ palette = QPalette() - palette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack)) - palette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText)) - palette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) + palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) + palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText)) + palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) self.setPalette(palette) self.itemTitle.setPalette(palette) @@ -2924,7 +2955,8 @@ class GuiDocEditHeader(QWidget): """Capture a click on the title and ensure that the item is selected in the project tree. """ - self.mainGui.projView.setSelectedHandle(self._docHandle, doScroll=True) + if event.button() == Qt.MouseButton.LeftButton: + self.docEditor.requestProjectItemSelected.emit(self._docHandle, True) return # END Class GuiDocEditHeader @@ -2961,11 +2993,13 @@ class GuiDocEditFooter(QWidget): self.setContentsMargins(0, 0, 0, 0) self.setAutoFillBackground(True) + alLeftTop = Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop + # Status self.statusIcon = QLabel("") self.statusIcon.setContentsMargins(0, 0, 0, 0) self.statusIcon.setFixedHeight(self.sPx) - self.statusIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.statusIcon.setAlignment(alLeftTop) self.statusText = QLabel(self.tr("Status")) self.statusText.setIndent(0) @@ -2973,14 +3007,14 @@ class GuiDocEditFooter(QWidget): self.statusText.setContentsMargins(0, 0, 0, 0) self.statusText.setAutoFillBackground(True) self.statusText.setFixedHeight(fPx) - self.statusText.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.statusText.setAlignment(alLeftTop) self.statusText.setFont(lblFont) # Lines self.linesIcon = QLabel("") self.linesIcon.setContentsMargins(0, 0, 0, 0) self.linesIcon.setFixedHeight(self.sPx) - self.linesIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.linesIcon.setAlignment(alLeftTop) self.linesText = QLabel("") self.linesText.setIndent(0) @@ -2988,14 +3022,14 @@ class GuiDocEditFooter(QWidget): self.linesText.setContentsMargins(0, 0, 0, 0) self.linesText.setAutoFillBackground(True) self.linesText.setFixedHeight(fPx) - self.linesText.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.linesText.setAlignment(alLeftTop) self.linesText.setFont(lblFont) # Words self.wordsIcon = QLabel("") self.wordsIcon.setContentsMargins(0, 0, 0, 0) self.wordsIcon.setFixedHeight(self.sPx) - self.wordsIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.wordsIcon.setAlignment(alLeftTop) self.wordsText = QLabel("") self.wordsText.setIndent(0) @@ -3003,7 +3037,7 @@ class GuiDocEditFooter(QWidget): self.wordsText.setContentsMargins(0, 0, 0, 0) self.wordsText.setAutoFillBackground(True) self.wordsText.setFixedHeight(fPx) - self.wordsText.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.wordsText.setAlignment(alLeftTop) self.wordsText.setFont(lblFont) # Assemble Layout @@ -3051,9 +3085,9 @@ class GuiDocEditFooter(QWidget): theme rather than the main GUI. """ palette = QPalette() - palette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack)) - palette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText)) - palette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) + palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) + palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText)) + palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) self.setPalette(palette) self.statusText.setPalette(palette) diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index a0e2c3b9..5df05810 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -37,8 +37,8 @@ from PyQt5.QtGui import ( QTextOption ) from PyQt5.QtWidgets import ( - QAction, qApp, QFrame, QHBoxLayout, QLabel, QMenu, QTextBrowser, - QToolButton, QWidget + QAction, QFrame, QHBoxLayout, QLabel, QMenu, QTextBrowser, QToolButton, + QWidget, qApp ) from novelwriter import CONFIG, SHARED @@ -59,6 +59,7 @@ class GuiDocViewer(QTextBrowser): documentLoaded = pyqtSignal(str) loadDocumentTagRequest = pyqtSignal(str, Enum) togglePanelVisibility = pyqtSignal() + requestProjectItemSelected = pyqtSignal(str, bool) def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) @@ -75,8 +76,8 @@ class GuiDocViewer(QTextBrowser): self.setMinimumWidth(CONFIG.pxInt(300)) self.setAutoFillBackground(True) self.setOpenExternalLinks(False) - self.setFocusPolicy(Qt.StrongFocus) - self.setFrameStyle(QFrame.NoFrame) + self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + self.setFrameStyle(QFrame.Shape.NoFrame) # Document Header and Footer self.docHeader = GuiDocViewHeader(self) @@ -92,7 +93,7 @@ class GuiDocViewer(QTextBrowser): self.installEventFilter(self.wheelEventFilter) # Context Menu - self.setContextMenuPolicy(Qt.CustomContextMenu) + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.customContextMenuRequested.connect(self._openContextMenu) self.initViewer() @@ -148,14 +149,14 @@ class GuiDocViewer(QTextBrowser): # Set the widget colours to match syntax theme mainPalette = self.palette() - mainPalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack)) - mainPalette.setColor(QPalette.Base, QColor(*SHARED.theme.colBack)) - mainPalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) + mainPalette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) + mainPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack)) + mainPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) self.setPalette(mainPalette) docPalette = self.viewport().palette() - docPalette.setColor(QPalette.Base, QColor(*SHARED.theme.colBack)) - docPalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) + docPalette.setColor(QPalette.ColorRole.Base, QColor(*SHARED.theme.colBack)) + docPalette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) self.viewport().setPalette(docPalette) self.docHeader.matchColours() @@ -165,19 +166,19 @@ class GuiDocViewer(QTextBrowser): self.document().setDocumentMargin(0) theOpt = QTextOption() if CONFIG.doJustify: - theOpt.setAlignment(Qt.AlignJustify) + theOpt.setAlignment(Qt.AlignmentFlag.AlignJustify) self.document().setDefaultTextOption(theOpt) # Scroll bars if CONFIG.hideVScroll: - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) else: - self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) if CONFIG.hideHScroll: - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) else: - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) # Refresh the tab stops self.setTabStopDistance(CONFIG.getTabWidth()) @@ -196,7 +197,7 @@ class GuiDocViewer(QTextBrowser): return False logger.debug("Generating preview for item '%s'", tHandle) - qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) + qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor)) sPos = self.verticalScrollBar().value() aDoc = ToHtml(SHARED.project) @@ -272,9 +273,9 @@ class GuiDocViewer(QTextBrowser): elif action == nwDocAction.COPY: self.copy() elif action == nwDocAction.SEL_ALL: - self._makeSelection(QTextCursor.Document) + self._makeSelection(QTextCursor.SelectionType.Document) elif action == nwDocAction.SEL_PARA: - self._makeSelection(QTextCursor.BlockUnderCursor) + self._makeSelection(QTextCursor.SelectionType.BlockUnderCursor) else: logger.debug("Unknown or unsupported document action '%s'", str(action)) return False @@ -392,13 +393,13 @@ class GuiDocViewer(QTextBrowser): mnuSelWord = QAction(self.tr("Select Word"), mnuContext) mnuSelWord.triggered.connect( - lambda: self._makePosSelection(QTextCursor.WordUnderCursor, point) + lambda: self._makePosSelection(QTextCursor.SelectionType.WordUnderCursor, point) ) mnuContext.addAction(mnuSelWord) mnuSelPara = QAction(self.tr("Select Paragraph"), mnuContext) mnuSelPara.triggered.connect( - lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, point) + lambda: self._makePosSelection(QTextCursor.SelectionType.BlockUnderCursor, point) ) mnuContext.addAction(mnuSelPara) @@ -419,9 +420,9 @@ class GuiDocViewer(QTextBrowser): def mouseReleaseEvent(self, event: QMouseEvent) -> None: """Capture mouse click events on the document.""" - if event.button() == Qt.BackButton: + if event.button() == Qt.MouseButton.BackButton: self.navBackward() - elif event.button() == Qt.ForwardButton: + elif event.button() == Qt.MouseButton.ForwardButton: self.navForward() else: super().mouseReleaseEvent(event) @@ -437,15 +438,15 @@ class GuiDocViewer(QTextBrowser): cursor.clearSelection() cursor.select(selType) - if selType == QTextCursor.BlockUnderCursor: + if selType == QTextCursor.SelectionType.BlockUnderCursor: # This selection mode also selects the preceding paragraph # separator, which we want to avoid. posS = cursor.selectionStart() posE = cursor.selectionEnd() selTxt = cursor.selectedText() if selTxt.startswith(nwUnicode.U_PSEP): - cursor.setPosition(posS+1, QTextCursor.MoveAnchor) - cursor.setPosition(posE, QTextCursor.KeepAnchor) + cursor.setPosition(posS+1, QTextCursor.MoveMode.MoveAnchor) + cursor.setPosition(posE, QTextCursor.MoveMode.KeepAnchor) self.setTextCursor(cursor) @@ -663,7 +664,7 @@ class GuiDocViewHeader(QWidget): self.docTitle.setMargin(0) self.docTitle.setContentsMargins(0, 0, 0, 0) self.docTitle.setAutoFillBackground(True) - self.docTitle.setAlignment(Qt.AlignHCenter | Qt.AlignTop) + self.docTitle.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop) self.docTitle.setFixedHeight(fPx) lblFont = self.docTitle.font() @@ -675,7 +676,7 @@ class GuiDocViewHeader(QWidget): self.backButton.setContentsMargins(0, 0, 0, 0) self.backButton.setIconSize(QSize(fPx, fPx)) self.backButton.setFixedSize(fPx, fPx) - self.backButton.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.backButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.backButton.setVisible(False) self.backButton.setToolTip(self.tr("Go Backward")) self.backButton.clicked.connect(self.docViewer.navBackward) @@ -684,7 +685,7 @@ class GuiDocViewHeader(QWidget): self.forwardButton.setContentsMargins(0, 0, 0, 0) self.forwardButton.setIconSize(QSize(fPx, fPx)) self.forwardButton.setFixedSize(fPx, fPx) - self.forwardButton.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.forwardButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.forwardButton.setVisible(False) self.forwardButton.setToolTip(self.tr("Go Forward")) self.forwardButton.clicked.connect(self.docViewer.navForward) @@ -693,7 +694,7 @@ class GuiDocViewHeader(QWidget): self.refreshButton.setContentsMargins(0, 0, 0, 0) self.refreshButton.setIconSize(QSize(fPx, fPx)) self.refreshButton.setFixedSize(fPx, fPx) - self.refreshButton.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.refreshButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.refreshButton.setVisible(False) self.refreshButton.setToolTip(self.tr("Reload")) self.refreshButton.clicked.connect(self._refreshDocument) @@ -702,7 +703,7 @@ class GuiDocViewHeader(QWidget): self.closeButton.setContentsMargins(0, 0, 0, 0) self.closeButton.setIconSize(QSize(fPx, fPx)) self.closeButton.setFixedSize(fPx, fPx) - self.closeButton.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.closeButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.closeButton.setVisible(False) self.closeButton.setToolTip(self.tr("Close")) self.closeButton.clicked.connect(self._closeDocument) @@ -761,9 +762,9 @@ class GuiDocViewHeader(QWidget): theme rather than the main GUI. """ palette = QPalette() - palette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack)) - palette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText)) - palette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) + palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) + palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText)) + palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) self.setPalette(palette) self.docTitle.setPalette(palette) return @@ -834,7 +835,8 @@ class GuiDocViewHeader(QWidget): """Capture a click on the title and ensure that the item is selected in the project tree. """ - self.mainGui.projView.setSelectedHandle(self._docHandle, doScroll=True) + if event.button() == Qt.MouseButton.LeftButton: + self.docViewer.requestProjectItemSelected.emit(self._docHandle, True) return # END Class GuiDocViewHeader @@ -868,7 +870,7 @@ class GuiDocViewFooter(QWidget): # Show/Hide Details self.showHide = QToolButton(self) - self.showHide.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.showHide.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.showHide.setIconSize(QSize(fPx, fPx)) self.showHide.setFixedSize(QSize(fPx, fPx)) self.showHide.clicked.connect(lambda: self.docViewer.togglePanelVisibility.emit()) @@ -878,7 +880,7 @@ class GuiDocViewFooter(QWidget): self.showComments = QToolButton(self) self.showComments.setCheckable(True) self.showComments.setChecked(CONFIG.viewComments) - self.showComments.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.showComments.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.showComments.setIconSize(QSize(fPx, fPx)) self.showComments.setFixedSize(QSize(fPx, fPx)) self.showComments.toggled.connect(self._doToggleComments) @@ -888,7 +890,7 @@ class GuiDocViewFooter(QWidget): self.showSynopsis = QToolButton(self) self.showSynopsis.setCheckable(True) self.showSynopsis.setChecked(CONFIG.viewSynopsis) - self.showSynopsis.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.showSynopsis.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) self.showSynopsis.setIconSize(QSize(fPx, fPx)) self.showSynopsis.setFixedSize(QSize(fPx, fPx)) self.showSynopsis.toggled.connect(self._doToggleSynopsis) @@ -902,7 +904,7 @@ class GuiDocViewFooter(QWidget): self.lblComments.setContentsMargins(0, 0, 0, 0) self.lblComments.setAutoFillBackground(True) self.lblComments.setFixedHeight(fPx) - self.lblComments.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.lblComments.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) self.lblSynopsis = QLabel(self.tr("Synopsis")) self.lblSynopsis.setBuddy(self.showSynopsis) @@ -911,7 +913,7 @@ class GuiDocViewFooter(QWidget): self.lblSynopsis.setContentsMargins(0, 0, 0, 0) self.lblSynopsis.setAutoFillBackground(True) self.lblSynopsis.setFixedHeight(fPx) - self.lblSynopsis.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.lblSynopsis.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) lblFont = self.font() lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) @@ -977,9 +979,9 @@ class GuiDocViewFooter(QWidget): theme rather than the main GUI. """ palette = QPalette() - palette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack)) - palette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText)) - palette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) + palette.setColor(QPalette.ColorRole.Window, QColor(*SHARED.theme.colBack)) + palette.setColor(QPalette.ColorRole.WindowText, QColor(*SHARED.theme.colText)) + palette.setColor(QPalette.ColorRole.Text, QColor(*SHARED.theme.colText)) self.setPalette(palette) self.lblComments.setPalette(palette) self.lblSynopsis.setPalette(palette) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 4b261afd..607e694a 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -138,7 +138,6 @@ class GuiProjectView(QWidget): self.emptyTrash = self.projTree.emptyTrash self.requestDeleteItem = self.projTree.requestDeleteItem self.getSelectedHandle = self.projTree.getSelectedHandle - self.setSelectedHandle = self.projTree.setSelectedHandle self.changedSince = self.projTree.changedSince self.createNewNote = self.projTree.createNewNote @@ -191,17 +190,26 @@ class GuiProjectView(QWidget): """Check if the project tree has focus.""" return self.projTree.hasFocus() - def renameTreeItem(self, tHandle: str | None = None) -> bool: + ## + # Public Slots + ## + + @pyqtSlot(str, str) + def renameTreeItem(self, tHandle: str | None = None, name: str = "") -> None: """External request to rename an item or the currently selected item. This is triggered by the global menu or keyboard shortcut. """ if tHandle is None: tHandle = self.projTree.getSelectedHandle() - return self.projTree.renameTreeItem(tHandle) if tHandle else False + if tHandle: + self.projTree.renameTreeItem(tHandle, name=name) + return - ## - # Public Slots - ## + @pyqtSlot(str, bool) + def setSelectedHandle(self, tHandle: str, doScroll: bool = False) -> None: + """Select an item and optionally scroll it into view.""" + self.projTree.setSelectedHandle(tHandle, doScroll=doScroll) + return @pyqtSlot(str) def updateItemValues(self, tHandle: str) -> None: @@ -761,19 +769,16 @@ class GuiProjectTree(QTreeWidget): self.setCurrentItem(tItem.child(0)) return - def renameTreeItem(self, tHandle: str) -> bool: + def renameTreeItem(self, tHandle: str, name: str = "") -> None: """Open a dialog to edit the label of an item.""" tItem = SHARED.project.tree[tHandle] - if tItem is None: - return False - - newLabel, dlgOk = GuiEditLabel.getLabel(self, text=tItem.itemName) - if dlgOk: - tItem.setName(newLabel) - self.setTreeItemValues(tHandle) - self._alertTreeChange(tHandle, flush=False) - - return True + if tItem: + newLabel, dlgOk = GuiEditLabel.getLabel(self, text=name or tItem.itemName) + if dlgOk: + tItem.setName(newLabel) + self.setTreeItemValues(tHandle) + self._alertTreeChange(tHandle, flush=False) + return def saveTreeOrder(self) -> None: """Build a list of the items in the project tree and send them diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 635e9b8f..defb0fe5 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -284,10 +284,13 @@ class GuiMain(QMainWindow): self.docEditor.spellCheckStateChanged.connect(self.mainMenu.setSpellCheckState) self.docEditor.closeDocumentRequest.connect(self.closeDocEditor) self.docEditor.toggleFocusModeRequest.connect(self.toggleFocusMode) + self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle) + self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem) self.docViewer.documentLoaded.connect(self.docViewerPanel.updateHandle) self.docViewer.loadDocumentTagRequest.connect(self._followTag) self.docViewer.togglePanelVisibility.connect(self._toggleViewerPanelVisibility) + self.docViewer.requestProjectItemSelected.connect(self.projView.setSelectedHandle) self.docViewerPanel.loadDocumentTagRequest.connect(self._followTag) self.docViewerPanel.openDocumentRequest.connect(self._openDocument) diff --git a/tests/conftest.py b/tests/conftest.py index 1886bfd4..be3c243f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import sys import pytest diff --git a/tests/mocked.py b/tests/mocked.py index a22258f5..0c1d215c 100644 --- a/tests/mocked.py +++ b/tests/mocked.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations from PyQt5.QtGui import QPixmap from PyQt5.QtWidgets import QWidget diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index 08e8b3e9..2ca1a35f 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import time from PyQt5.QtCore import QUrl diff --git a/tests/test_base/test_base_config.py b/tests/test_base/test_base_config.py index d97f57a4..d8938d45 100644 --- a/tests/test_base/test_base_config.py +++ b/tests/test_base/test_base_config.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import sys import pytest diff --git a/tests/test_base/test_base_error.py b/tests/test_base/test_base_error.py index 886a8e80..b8afa2a0 100644 --- a/tests/test_base/test_base_error.py +++ b/tests/test_base/test_base_error.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_base/test_base_init.py b/tests/test_base/test_base_init.py index cbc20c51..01645e04 100644 --- a/tests/test_base/test_base_init.py +++ b/tests/test_base/test_base_init.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import sys import pytest diff --git a/tests/test_base/test_base_shared.py b/tests/test_base/test_base_shared.py index d699da6f..4e3de75a 100644 --- a/tests/test_base/test_base_shared.py +++ b/tests/test_base/test_base_shared.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_core/test_core_buildsettings.py b/tests/test_core/test_core_buildsettings.py index eb4affdf..3486fb85 100644 --- a/tests/test_core/test_core_buildsettings.py +++ b/tests/test_core/test_core_buildsettings.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import json import uuid diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py index 0f127e05..c52b9ad5 100644 --- a/tests/test_core/test_core_coretools.py +++ b/tests/test_core/test_core_coretools.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import uuid import pytest diff --git a/tests/test_core/test_core_docbuild.py b/tests/test_core/test_core_docbuild.py index 4c5e924a..854a0edd 100644 --- a/tests/test_core/test_core_docbuild.py +++ b/tests/test_core/test_core_docbuild.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import json import pytest diff --git a/tests/test_core/test_core_document.py b/tests/test_core/test_core_document.py index 32f0f258..1cf88732 100644 --- a/tests/test_core/test_core_document.py +++ b/tests/test_core/test_core_document.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 6354b39c..9e7856e8 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import json import pytest diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index 2878137f..5f4b8512 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import copy import pytest diff --git a/tests/test_core/test_core_options.py b/tests/test_core/test_core_options.py index 01345630..44ac84dc 100644 --- a/tests/test_core/test_core_options.py +++ b/tests/test_core/test_core_options.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import json import pytest diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 1cc58342..475c9ce1 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -18,8 +18,8 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations -from PyQt5.QtWidgets import QMessageBox import pytest from shutil import copyfile @@ -28,6 +28,8 @@ from zipfile import ZipFile from mocked import causeOSError from tools import C, cmpFiles, buildTestProject, XML_IGNORE +from PyQt5.QtWidgets import QMessageBox + from novelwriter import CONFIG, SHARED from novelwriter.enum import nwItemClass from novelwriter.constants import nwFiles diff --git a/tests/test_core/test_core_projectxml.py b/tests/test_core/test_core_projectxml.py index a6c6e5e1..6b43ca06 100644 --- a/tests/test_core/test_core_projectxml.py +++ b/tests/test_core/test_core_projectxml.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import json import pytest diff --git a/tests/test_core/test_core_sessions.py b/tests/test_core/test_core_sessions.py index 159e7469..efbbad10 100644 --- a/tests/test_core/test_core_sessions.py +++ b/tests/test_core/test_core_sessions.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_core/test_core_spellcheck.py b/tests/test_core/test_core_spellcheck.py index d20269b5..6645f6c1 100644 --- a/tests/test_core/test_core_spellcheck.py +++ b/tests/test_core/test_core_spellcheck.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import sys import pytest diff --git a/tests/test_core/test_core_status.py b/tests/test_core/test_core_status.py index a42701b9..31820389 100644 --- a/tests/test_core/test_core_status.py +++ b/tests/test_core/test_core_status.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_core/test_core_storage.py b/tests/test_core/test_core_storage.py index 19d8a5b5..50770539 100644 --- a/tests/test_core/test_core_storage.py +++ b/tests/test_core/test_core_storage.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import json import pytest diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py index 771040ab..b0776ef6 100644 --- a/tests/test_core/test_core_tohtml.py +++ b/tests/test_core/test_core_tohtml.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index d5cbee87..c218b7d7 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import json import pytest diff --git a/tests/test_core/test_core_tomd.py b/tests/test_core/test_core_tomd.py index 204f1ac1..c44447a4 100644 --- a/tests/test_core/test_core_tomd.py +++ b/tests/test_core/test_core_tomd.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py index 24be4883..ab6bf41c 100644 --- a/tests/test_core/test_core_toodt.py +++ b/tests/test_core/test_core_toodt.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest import zipfile diff --git a/tests/test_dialogs/test_dlg_about.py b/tests/test_dialogs/test_dlg_about.py index d2ab97a6..36ba5d42 100644 --- a/tests/test_dialogs/test_dlg_about.py +++ b/tests/test_dialogs/test_dlg_about.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_dialogs/test_dlg_dialogs.py b/tests/test_dialogs/test_dlg_dialogs.py index 39c8c770..ad0c338a 100644 --- a/tests/test_dialogs/test_dlg_dialogs.py +++ b/tests/test_dialogs/test_dlg_dialogs.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index 8071b555..92825356 100644 --- a/tests/test_dialogs/test_dlg_docmerge.py +++ b/tests/test_dialogs/test_dlg_docmerge.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py index 0de88a72..adf186da 100644 --- a/tests/test_dialogs/test_dlg_docsplit.py +++ b/tests/test_dialogs/test_dlg_docsplit.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index dc51fd32..ae4d34ae 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_dialogs/test_dlg_projdetails.py b/tests/test_dialogs/test_dlg_projdetails.py index 88fd9a92..8ea28443 100644 --- a/tests/test_dialogs/test_dlg_projdetails.py +++ b/tests/test_dialogs/test_dlg_projdetails.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_dialogs/test_dlg_projload.py b/tests/test_dialogs/test_dlg_projload.py index e5afe6aa..ad1bd49a 100644 --- a/tests/test_dialogs/test_dlg_projload.py +++ b/tests/test_dialogs/test_dlg_projload.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index 824d0403..1413bb4a 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_dialogs/test_dlg_wordlist.py b/tests/test_dialogs/test_dlg_wordlist.py index 6012cf8e..e775baa0 100644 --- a/tests/test_dialogs/test_dlg_wordlist.py +++ b/tests/test_dialogs/test_dlg_wordlist.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_ext/test_ext_wheeleventfilter.py b/tests/test_ext/test_ext_wheeleventfilter.py index 1120a237..2d348358 100644 --- a/tests/test_ext/test_ext_wheeleventfilter.py +++ b/tests/test_ext/test_ext_wheeleventfilter.py @@ -18,11 +18,12 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations -from PyQt5.QtCore import QEvent, QObject, QPoint, Qt import pytest from PyQt5.QtGui import QKeyEvent, QWheelEvent +from PyQt5.QtCore import QEvent, QObject, QPoint, Qt from PyQt5.QtWidgets import QWidget from novelwriter.extensions.wheeleventfilter import WheelEventFilter diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index c6464987..51217182 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -18,21 +18,23 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest from tools import C, buildTestProject 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.QtWidgets import QAction, qApp +from PyQt5.QtWidgets import QAction, QMenu, qApp from novelwriter import CONFIG, SHARED 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, GuiDocToolBar +from novelwriter.dialogs.editlabel import GuiEditLabel KEY_DELAY = 1 @@ -209,6 +211,148 @@ def testGuiEditor_MetaData(qtbot, nwGUI, projPath, mockRnd): # 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 def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd): """Test the document actions. This is not an extensive test of the diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index b05e35b1..79c8dedb 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -18,13 +18,14 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest from mocked import causeException -from PyQt5.QtGui import QTextCursor -from PyQt5.QtCore import Qt, QUrl +from PyQt5.QtGui import QMouseEvent, QTextCursor +from PyQt5.QtCore import QEvent, QPoint, Qt, QUrl from PyQt5.QtWidgets import QMenu, qApp, QAction from novelwriter import CONFIG, SHARED @@ -58,7 +59,10 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): assert nwGUI.projView.projTree.getSelectedHandle() is None # Re-select via header click - docViewer.docHeader.mousePressEvent(None) # type: ignore + button = Qt.MouseButton.LeftButton + modifier = Qt.KeyboardModifier.NoModifier + event = QMouseEvent(QEvent.MouseButtonPress, QPoint(), button, button, modifier) + docViewer.docHeader.mousePressEvent(event) assert nwGUI.projView.projTree.getSelectedHandle() == "88243afbe5ed8" # Reload the text @@ -127,7 +131,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): assert docViewer.docAction(nwDocAction.COPY) is False # Open again via menu - assert nwGUI.projView.setSelectedHandle("88243afbe5ed8") + assert nwGUI.projView.projTree.setSelectedHandle("88243afbe5ed8") nwGUI.mainMenu.aViewDoc.activate(QAction.Trigger) # Open context menu diff --git a/tests/test_gui/test_gui_docviewerpanel.py b/tests/test_gui/test_gui_docviewerpanel.py index cff00ccf..1ff773ae 100644 --- a/tests/test_gui/test_gui_docviewerpanel.py +++ b/tests/test_gui/test_gui_docviewerpanel.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 52e2bc0f..4ed784be 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import sys import pytest diff --git a/tests/test_gui/test_gui_i18n.py b/tests/test_gui/test_gui_i18n.py index a53ef837..210dc710 100644 --- a/tests/test_gui/test_gui_i18n.py +++ b/tests/test_gui/test_gui_i18n.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import sys import pytest diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index c00fe9da..5bf25674 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -18,15 +18,16 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest +from tools import C, writeFile, buildTestProject + from PyQt5.QtGui import QTextCursor, QTextBlock from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox -from tools import C, writeFile, buildTestProject - from novelwriter import CONFIG, SHARED from novelwriter.enum import nwDocAction, nwDocInsert from novelwriter.constants import nwKeyWords, nwUnicode diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index e01c0dd9..7cd0eb18 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 85ea4640..8e0f72ce 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import time import pytest diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 76495311..679f4826 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -156,12 +156,9 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn # Rename plot folder with monkeypatch.context() as mp: mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("Stuff", True)) - projTree.renameTreeItem(C.hPlotRoot) is True + projTree.renameTreeItem(C.hPlotRoot) assert project.tree[C.hPlotRoot].itemName == "Stuff" # type: ignore - # Rename invalid folder - projTree.renameTreeItem("0000000000000") is False - # Other Checks # ============ @@ -1095,12 +1092,12 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd) mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("FooBar", True)) projTree.clearSelection() assert SHARED.project.tree[C.hChapterDoc].itemName == "New Chapter" # type: ignore - assert projView.renameTreeItem(C.hChapterDoc) is True + projView.renameTreeItem(C.hChapterDoc) assert SHARED.project.tree[C.hChapterDoc].itemName == "FooBar" # type: ignore projTree.setSelectedHandle(C.hSceneDoc) assert SHARED.project.tree[C.hSceneDoc].itemName == "New Scene" # type: ignore - assert projView.renameTreeItem() is True + projView.renameTreeItem() assert SHARED.project.tree[C.hSceneDoc].itemName == "FooBar" # type: ignore # Check Crash Resistance diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index 2a024f8a..e500dfd9 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import time import pytest diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index cfb16ab1..9bc452d7 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_tools/test_tools_dictionaries.py b/tests/test_tools/test_tools_dictionaries.py index 5c3b471e..eee0f0f7 100644 --- a/tests/test_tools/test_tools_dictionaries.py +++ b/tests/test_tools/test_tools_dictionaries.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest import enchant diff --git a/tests/test_tools/test_tools_lipsum.py b/tests/test_tools/test_tools_lipsum.py index 31944091..19160851 100644 --- a/tests/test_tools/test_tools_lipsum.py +++ b/tests/test_tools/test_tools_lipsum.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_tools/test_tools_manusbuild.py b/tests/test_tools/test_tools_manusbuild.py index a23f383e..d29f3145 100644 --- a/tests/test_tools/test_tools_manusbuild.py +++ b/tests/test_tools/test_tools_manusbuild.py @@ -18,9 +18,8 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations -from PyQt5.QtCore import QUrl -from PyQt5.QtGui import QDesktopServices import pytest from pathlib import Path @@ -28,6 +27,8 @@ from pytestqt.qtbot import QtBot from tools import buildTestProject +from PyQt5.QtGui import QDesktopServices +from PyQt5.QtCore import QUrl from PyQt5.QtWidgets import QDialogButtonBox, QFileDialog, QListWidgetItem, QMessageBox from novelwriter.enum import nwBuildFmt diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py index af8a8146..d734a117 100644 --- a/tests/test_tools/test_tools_manuscript.py +++ b/tests/test_tools/test_tools_manuscript.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import sys import pytest diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py index 0a5f9c5a..a752c966 100644 --- a/tests/test_tools/test_tools_manussettings.py +++ b/tests/test_tools/test_tools_manussettings.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import pytest diff --git a/tests/test_tools/test_tools_projwizard.py b/tests/test_tools/test_tools_projwizard.py index 9fe8865b..65a80687 100644 --- a/tests/test_tools/test_tools_projwizard.py +++ b/tests/test_tools/test_tools_projwizard.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import sys import pytest diff --git a/tests/test_tools/test_tools_writingstats.py b/tests/test_tools/test_tools_writingstats.py index 64de8775..b5868f6e 100644 --- a/tests/test_tools/test_tools_writingstats.py +++ b/tests/test_tools/test_tools_writingstats.py @@ -18,6 +18,7 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations import json import pytest