From e236fb30dcb1487a1cf45333e936a7e38365a371 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 7 Nov 2023 17:47:59 +0100 Subject: [PATCH 01/10] Clean up document header signals --- docs/source/usage_shortcuts.rst | 4 +- novelwriter/gui/mainmenu.py | 4 +- novelwriter/gui/sidebar.py | 1 - novelwriter/guimain.py | 330 +++++++++++++++-------------- tests/test_gui/test_gui_guimain.py | 10 +- 5 files changed, 180 insertions(+), 169 deletions(-) diff --git a/docs/source/usage_shortcuts.rst b/docs/source/usage_shortcuts.rst index 86e60afa2..4e9313510 100644 --- a/docs/source/usage_shortcuts.rst +++ b/docs/source/usage_shortcuts.rst @@ -77,10 +77,10 @@ Text Search Shortcuts ":kbd:`F3`", "Find the next occurrence of the search word" ":kbd:`Ctrl+F`", "Open the search bar and search for the selected word, if any is selected" - ":kbd:`Ctrl+G`", "Find next occurrence of search word in current document" + ":kbd:`Ctrl+G`", "Find the next occurrence of the search word" ":kbd:`Ctrl+H`", "Open the search tool and populate with the selected word (Mac :kbd:`Cmd+=`)" ":kbd:`Ctrl+Shift+1`", "Replace selected occurrence of the search word, and move to the next" - ":kbd:`Ctrl+Shift+G`", "Find previous occurrence of the search word" + ":kbd:`Ctrl+Shift+G`", "Find the previous occurrence of the search word" ":kbd:`Shift+F3`", "Find the previous occurrence of the search word" diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index fa8b64add..6355ca63f 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -209,7 +209,7 @@ def _buildDocumentMenu(self) -> None: # Document > Close self.aCloseDoc = self.docuMenu.addAction(self.tr("Close Document")) self.aCloseDoc.setShortcut("Ctrl+W") - self.aCloseDoc.triggered.connect(lambda: self.mainGui.closeDocEditor()) + self.aCloseDoc.triggered.connect(self.mainGui.closeDocEditor) # Document > Separator self.docuMenu.addSeparator() @@ -330,7 +330,7 @@ def _buildViewMenu(self) -> None: # View > Focus Mode self.aFocusMode = self.viewMenu.addAction(self.tr("Focus Mode")) self.aFocusMode.setShortcut("F8") - self.aFocusMode.triggered.connect(lambda: self.mainGui.toggleFocusMode()) + self.aFocusMode.triggered.connect(self.mainGui.toggleFocusMode) # View > Toggle Full Screen self.aFullScreen = self.viewMenu.addAction(self.tr("Full Screen Mode")) diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index 4b7b4e7bf..be719a42d 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -115,7 +115,6 @@ def __init__(self, mainGui: GuiMain) -> None: self.outerBox.setSpacing(CONFIG.pxInt(4)) self.setLayout(self.outerBox) - self.updateTheme() logger.debug("Ready: GuiSideBar") diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 4d324677f..adcebca8a 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -267,6 +267,8 @@ def __init__(self) -> None: self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta) self.docEditor.statusMessage.connect(self.mainStatus.setStatusMessage) self.docEditor.spellCheckStateChanged.connect(self.mainMenu.setSpellCheckState) + self.docEditor.closeDocumentRequest.connect(self.closeDocEditor) + self.docEditor.toggleFocusModeRequest.connect(self.toggleFocusMode) self.docViewer.loadDocumentTagRequest.connect(self._followTag) @@ -1119,12 +1121,6 @@ def switchFocus(self, paneNo: nwWidget) -> None: self.outlineView.setTreeFocus() return - def closeDocEditor(self) -> None: - """Close the document editor. This does not hide the editor.""" - self.closeDocument() - SHARED.project.data.setLastHandle(None, "editor") - return - def closeDocViewer(self, byUser: bool = True) -> bool: """Close the document view panel.""" self.docViewer.clearViewer() @@ -1139,13 +1135,44 @@ def closeDocViewer(self, byUser: bool = True) -> bool: return not self.splitView.isVisible() - def toggleFocusMode(self) -> bool: + def toggleFullScreenMode(self) -> None: + """Toggle full screen mode""" + self.setWindowState(self.windowState() ^ Qt.WindowFullScreen) + return + + ## + # Events + ## + + def closeEvent(self, event: QCloseEvent): + """Capture the closing event of the GUI and call the close + function to handle all the close process steps. + """ + if self.closeMain(): + event.accept() + else: + event.ignore() + return + + ## + # Public Slots + ## + + @pyqtSlot() + def closeDocEditor(self) -> None: + """Close the document editor. This does not hide the editor.""" + self.closeDocument() + SHARED.project.data.setLastHandle(None, "editor") + return + + @pyqtSlot() + def toggleFocusMode(self) -> None: """Handle toggle focus mode. The Main GUI Focus Mode hides tree, view, statusbar and menu. """ if self.docEditor.docHandle is None: logger.error("No document open, so not activating Focus Mode") - return False + return self.isFocusMode = not self.isFocusMode if self.isFocusMode: @@ -1169,11 +1196,141 @@ def toggleFocusMode(self) -> bool: elif self.docViewer.docHandle is not None: self.splitView.setVisible(True) - return True + return - def toggleFullScreenMode(self) -> None: - """Toggle full screen mode""" - self.setWindowState(self.windowState() ^ Qt.WindowFullScreen) + ## + # Private Slots + ## + + @pyqtSlot(str, nwDocMode) + def _followTag(self, tag: str, mode: nwDocMode) -> None: + """Follow a tag after user interaction with a link.""" + tHandle, sTitle = self._getTagSource(tag) + if tHandle is not None: + if mode == nwDocMode.EDIT: + self.openDocument(tHandle) + elif mode == nwDocMode.VIEW: + self.viewDocument(tHandle=tHandle, sTitle=sTitle) + return + + @pyqtSlot(str, nwDocMode, str, bool) + def _openDocument(self, tHandle: str, mode: nwDocMode, sTitle: str, setFocus: bool) -> None: + """Handle an open document request.""" + if tHandle is not None: + if mode == nwDocMode.EDIT: + tLine = None + hItem = SHARED.project.index.getItemHeader(tHandle, sTitle) + if hItem is not None: + tLine = hItem.line + self.openDocument(tHandle, tLine=tLine, changeFocus=setFocus) + elif mode == nwDocMode.VIEW: + self.viewDocument(tHandle=tHandle, sTitle=sTitle) + return + + @pyqtSlot(nwView) + def _changeView(self, view: nwView) -> None: + """Handle the requested change of view from the GuiViewBar.""" + if view == nwView.EDITOR: + # Only change the main stack, but not the project stack + self.mainStack.setCurrentWidget(self.splitMain) + + elif view == nwView.PROJECT: + self.mainStack.setCurrentWidget(self.splitMain) + self.projStack.setCurrentWidget(self.projView) + + elif view == nwView.NOVEL: + self.mainStack.setCurrentWidget(self.splitMain) + self.projStack.setCurrentWidget(self.novelView) + + elif view == nwView.OUTLINE: + self.mainStack.setCurrentWidget(self.outlineView) + + return + + @pyqtSlot() + def _timeTick(self) -> None: + """Process time tick of the main timer.""" + if not SHARED.hasProject: + return + currTime = time() + editIdle = currTime - self.docEditor.lastActive > CONFIG.userIdleTime + userIdle = qApp.applicationState() != Qt.ApplicationActive + self.mainStatus.setUserIdle(editIdle or userIdle) + SHARED.updateIdleTime(currTime, editIdle or userIdle) + self.mainStatus.updateTime(idleTime=SHARED.projectIdleTime) + return + + @pyqtSlot() + def _autoSaveProject(self) -> None: + """Autosave of the project. This is a timer-activated slot.""" + doSave = SHARED.hasProject + doSave &= SHARED.project.projChanged + doSave &= SHARED.project.storage.isOpen() + if doSave: + logger.debug("Autosaving project") + self.saveProject(autoSave=True) + return + + @pyqtSlot() + def _autoSaveDocument(self) -> None: + """Autosave of the document. This is a timer-activated slot.""" + if SHARED.hasProject and self.docEditor.docChanged: + logger.debug("Autosaving document") + self.saveDocument() + return + + @pyqtSlot() + def _updateStatusWordCount(self) -> None: + """Update the word count on the status bar.""" + if not SHARED.hasProject: + self.mainStatus.setProjectStats(0, 0) + + SHARED.project.updateWordCounts() + if CONFIG.incNotesWCount: + iTotal = sum(SHARED.project.data.initCounts) + cTotal = sum(SHARED.project.data.currCounts) + self.mainStatus.setProjectStats(cTotal, cTotal - iTotal) + else: + iNovel, _ = SHARED.project.data.initCounts + cNovel, _ = SHARED.project.data.currCounts + self.mainStatus.setProjectStats(cNovel, cNovel - iNovel) + + return + + @pyqtSlot() + def _keyPressReturn(self) -> None: + """Forward the return/enter keypress to the function that opens + the currently selected item. + """ + self.openSelectedItem() + return + + @pyqtSlot() + def _keyPressEscape(self) -> None: + """Process escape keypress in the main window.""" + if self.docEditor.docSearch.isVisible(): + self.docEditor.closeSearch() + elif self.isFocusMode: + self.toggleFocusMode() + return + + @pyqtSlot(int) + def _mainStackChanged(self, index: int) -> None: + """Process main window tab change.""" + if index == self.idxOutlineView: + if SHARED.hasProject: + self.outlineView.refreshTree() + return + + @pyqtSlot(int) + def _projStackChanged(self, index: int) -> None: + """Process project view tab change.""" + sHandle = None + if index == self.idxProjView: + sHandle = self.projView.getSelectedHandle() + elif index == self.idxNovelView: + sHandle, _ = self.novelView.getSelectedHandle() + self.itemDetails.updateViewBox(sHandle) return ## @@ -1328,153 +1485,4 @@ def _getTagSource(self, tag: str) -> tuple[str | None, str | None]: return None, None return tHandle, sTitle - ## - # Events - ## - - def closeEvent(self, event: QCloseEvent): - """Capture the closing event of the GUI and call the close - function to handle all the close process steps. - """ - if self.closeMain(): - event.accept() - else: - event.ignore() - return - - ## - # Private Slots - ## - - @pyqtSlot(str, nwDocMode) - def _followTag(self, tag: str, mode: nwDocMode) -> None: - """Follow a tag after user interaction with a link.""" - tHandle, sTitle = self._getTagSource(tag) - if tHandle is not None: - if mode == nwDocMode.EDIT: - self.openDocument(tHandle) - elif mode == nwDocMode.VIEW: - self.viewDocument(tHandle=tHandle, sTitle=sTitle) - return - - @pyqtSlot(str, nwDocMode, str, bool) - def _openDocument(self, tHandle: str, mode: nwDocMode, sTitle: str, setFocus: bool) -> None: - """Handle an open document request.""" - if tHandle is not None: - if mode == nwDocMode.EDIT: - tLine = None - hItem = SHARED.project.index.getItemHeader(tHandle, sTitle) - if hItem is not None: - tLine = hItem.line - self.openDocument(tHandle, tLine=tLine, changeFocus=setFocus) - elif mode == nwDocMode.VIEW: - self.viewDocument(tHandle=tHandle, sTitle=sTitle) - return - - @pyqtSlot(nwView) - def _changeView(self, view: nwView) -> None: - """Handle the requested change of view from the GuiViewBar.""" - if view == nwView.EDITOR: - # Only change the main stack, but not the project stack - self.mainStack.setCurrentWidget(self.splitMain) - - elif view == nwView.PROJECT: - self.mainStack.setCurrentWidget(self.splitMain) - self.projStack.setCurrentWidget(self.projView) - - elif view == nwView.NOVEL: - self.mainStack.setCurrentWidget(self.splitMain) - self.projStack.setCurrentWidget(self.novelView) - - elif view == nwView.OUTLINE: - self.mainStack.setCurrentWidget(self.outlineView) - - return - - @pyqtSlot() - def _timeTick(self) -> None: - """Process time tick of the main timer.""" - if not SHARED.hasProject: - return - currTime = time() - editIdle = currTime - self.docEditor.lastActive > CONFIG.userIdleTime - userIdle = qApp.applicationState() != Qt.ApplicationActive - self.mainStatus.setUserIdle(editIdle or userIdle) - SHARED.updateIdleTime(currTime, editIdle or userIdle) - self.mainStatus.updateTime(idleTime=SHARED.projectIdleTime) - return - - @pyqtSlot() - def _autoSaveProject(self) -> None: - """Autosave of the project. This is a timer-activated slot.""" - doSave = SHARED.hasProject - doSave &= SHARED.project.projChanged - doSave &= SHARED.project.storage.isOpen() - if doSave: - logger.debug("Autosaving project") - self.saveProject(autoSave=True) - return - - @pyqtSlot() - def _autoSaveDocument(self) -> None: - """Autosave of the document. This is a timer-activated slot.""" - if SHARED.hasProject and self.docEditor.docChanged: - logger.debug("Autosaving document") - self.saveDocument() - return - - @pyqtSlot() - def _updateStatusWordCount(self) -> None: - """Update the word count on the status bar.""" - if not SHARED.hasProject: - self.mainStatus.setProjectStats(0, 0) - - SHARED.project.updateWordCounts() - if CONFIG.incNotesWCount: - iTotal = sum(SHARED.project.data.initCounts) - cTotal = sum(SHARED.project.data.currCounts) - self.mainStatus.setProjectStats(cTotal, cTotal - iTotal) - else: - iNovel, _ = SHARED.project.data.initCounts - cNovel, _ = SHARED.project.data.currCounts - self.mainStatus.setProjectStats(cNovel, cNovel - iNovel) - - return - - @pyqtSlot() - def _keyPressReturn(self) -> None: - """Forward the return/enter keypress to the function that opens - the currently selected item. - """ - self.openSelectedItem() - return - - @pyqtSlot() - def _keyPressEscape(self) -> None: - """Process escape keypress in the main window.""" - if self.docEditor.docSearch.isVisible(): - self.docEditor.closeSearch() - elif self.isFocusMode: - self.toggleFocusMode() - return - - @pyqtSlot(int) - def _mainStackChanged(self, index: int) -> None: - """Process main window tab change.""" - if index == self.idxOutlineView: - if SHARED.hasProject: - self.outlineView.refreshTree() - return - - @pyqtSlot(int) - def _projStackChanged(self, index: int) -> None: - """Process project view tab change.""" - sHandle = None - if index == self.idxProjView: - sHandle = self.projView.getSelectedHandle() - elif index == self.idxNovelView: - sHandle, _ = self.novelView.getSelectedHandle() - self.itemDetails.updateViewBox(sHandle) - return - # END Class GuiMain diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 74e0e3522..52e2bc0fd 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -594,14 +594,18 @@ def testGuiMain_Features(qtbot, nwGUI, projPath, mockRnd): # ========== # No document open, so not allowing focus mode - assert nwGUI.toggleFocusMode() is False + nwGUI.toggleFocusMode() + assert nwGUI.treePane.isVisible() is True + assert nwGUI.mainStatus.isVisible() is True + assert nwGUI.mainMenu.isVisible() is True + assert nwGUI.sideBar.isVisible() is True # Open a file in editor and viewer assert nwGUI.openDocument(C.hSceneDoc) assert nwGUI.viewDocument(C.hSceneDoc) # Enable focus mode - assert nwGUI.toggleFocusMode() is True + nwGUI.toggleFocusMode() assert nwGUI.treePane.isVisible() is False assert nwGUI.mainStatus.isVisible() is False assert nwGUI.mainMenu.isVisible() is False @@ -609,7 +613,7 @@ def testGuiMain_Features(qtbot, nwGUI, projPath, mockRnd): assert nwGUI.splitView.isVisible() is False # Disable focus mode - assert nwGUI.toggleFocusMode() is True + nwGUI.toggleFocusMode() assert nwGUI.treePane.isVisible() is True assert nwGUI.mainStatus.isVisible() is True assert nwGUI.mainMenu.isVisible() is True From 394cdd5dadbbcbf882afdb85699a0c61f277baae Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 7 Nov 2023 17:48:34 +0100 Subject: [PATCH 02/10] Add a basic document editor toolbar --- novelwriter/gui/doceditor.py | 154 +++++++++++++++++++++++++---------- novelwriter/gui/docviewer.py | 8 +- 2 files changed, 117 insertions(+), 45 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index e9094c8b4..f12220e51 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -46,7 +46,7 @@ ) from PyQt5.QtWidgets import ( QAction, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QMenu, - QPlainTextEdit, QPushButton, QShortcut, QToolBar, QToolButton, QWidget, + QPlainTextEdit, QPushButton, QShortcut, QToolBar, QToolButton, QVBoxLayout, QWidget, qApp ) @@ -83,6 +83,8 @@ class GuiDocEditor(QPlainTextEdit): novelStructureChanged = pyqtSignal() novelItemMetaChanged = pyqtSignal(str) spellCheckStateChanged = pyqtSignal(bool) + closeDocumentRequest = pyqtSignal() + toggleFocusModeRequest = pyqtSignal() def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) @@ -135,6 +137,10 @@ def __init__(self, mainGui: GuiMain) -> None: self.docHeader = GuiDocEditHeader(self) self.docFooter = GuiDocEditFooter(self) self.docSearch = GuiDocEditSearch(self) + self.docToolBar = GuiDocToolBar(self) + + # Connect Signals + self.docHeader.closeDocumentRequest.connect(self._closeCurrentDocument) # Context Menu self.setContextMenuPolicy(Qt.CustomContextMenu) @@ -248,6 +254,7 @@ def updateTheme(self) -> None: self.docSearch.updateTheme() self.docHeader.updateTheme() self.docFooter.updateTheme() + self.docToolBar.updateTheme() return def updateSyntaxColours(self) -> None: @@ -514,6 +521,7 @@ def updateDocMargins(self) -> None: fY = wH - fH - tB - sH self.docHeader.setGeometry(tB, tB, tW, tH) self.docFooter.setGeometry(tB, fY, tW, fH) + self.docToolBar.move(0, tH) rH = 0 if self.docSearch.isVisible(): @@ -857,6 +865,7 @@ def closeSearch(self) -> bool: self.docSearch.closeSearch() return self.docSearch.isVisible() + @pyqtSlot() def toggleSearch(self) -> None: """Toggle the visibility of the search box.""" if self.docSearch.isVisible(): @@ -1179,6 +1188,12 @@ def _updateSelCounts(self, cCount: int, wCount: int, pCount: int) -> None: return + @pyqtSlot() + def _closeCurrentDocument(self) -> None: + """Close the document. Forwarded to the main Gui.""" + self.closeDocumentRequest.emit() + return + ## # Search & Replace ## @@ -2072,6 +2087,77 @@ class BackgroundWordCounterSignals(QObject): # END Class BackgroundWordCounterSignals +# =============================================================================================== # +# The Formatting and Options Fold Out Menu +# Only used by DocEditor, and is opened by the first button in the header +# =============================================================================================== # + +class GuiDocToolBar(QWidget): + + def __init__(self, docEditor: GuiDocEditor) -> None: + super().__init__(parent=docEditor) + + logger.debug("Create: GuiDocToolBar") + + cM = CONFIG.pxInt(4) + tPx = int(0.8*SHARED.theme.fontPixelSize) + iconSize = QSize(tPx, tPx) + self.setContentsMargins(0, 0, 0, 0) + + # General Buttons + # =============== + + self.tbSticky = QToolButton(self) + self.tbSticky.setIconSize(iconSize) + + self.tbEdit = QToolButton(self) + self.tbEdit.setIconSize(iconSize) + + # Assemble + # ======== + + self.outerBox = QVBoxLayout() + self.outerBox.addWidget(self.tbSticky) + self.outerBox.addWidget(self.tbEdit) + self.outerBox.setContentsMargins(cM, cM, cM, cM) + self.outerBox.setSpacing(cM) + + self.setLayout(self.outerBox) + self.updateTheme() + + logger.debug("Ready: GuiDocToolBar") + + return + + 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)) + + self.setPalette(palette) + + # qPalette = self.palette() + # qPalette.setBrush(QPalette.Window, qPalette.base()) + # self.setPalette(qPalette) + + # fadeCol = qPalette.text().color() + # buttonStyle = ( + # "QToolButton {{padding: {0}px; border: none; background: transparent;}} " + # "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}" + # ).format(CONFIG.pxInt(4), fadeCol.red(), fadeCol.green(), fadeCol.blue()) + # buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}" + # self.tbEdit.setStyleSheet(buttonStyle) + + self.tbSticky.setIcon(SHARED.theme.getIcon("sticky-off")) + self.tbEdit.setIcon(SHARED.theme.getIcon("edit")) + + return + +# END Class GuiDocToolBar + + # =============================================================================================== # # The Embedded Document Search/Replace Feature # Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport @@ -2487,6 +2573,9 @@ def _alertSearchValid(self, isValid: bool) -> None: class GuiDocEditHeader(QWidget): + toggleToolBarRequest = pyqtSignal() + closeDocumentRequest = pyqtSignal() + def __init__(self, docEditor: GuiDocEditor) -> None: super().__init__(parent=docEditor) @@ -2499,6 +2588,7 @@ def __init__(self, docEditor: GuiDocEditor) -> None: fPx = int(0.9*SHARED.theme.fontPixelSize) hSp = CONFIG.pxInt(6) + iconSize = QSize(fPx, fPx) # Main Widget Settings self.setAutoFillBackground(True) @@ -2518,46 +2608,46 @@ def __init__(self, docEditor: GuiDocEditor) -> None: self.itemTitle.setFont(lblFont) # Buttons - self.editButton = QToolButton(self) - self.editButton.setContentsMargins(0, 0, 0, 0) - self.editButton.setIconSize(QSize(fPx, fPx)) - self.editButton.setFixedSize(fPx, fPx) - self.editButton.setToolButtonStyle(Qt.ToolButtonIconOnly) - self.editButton.setVisible(False) - self.editButton.setToolTip(self.tr("Edit document label")) - self.editButton.clicked.connect(self._editDocument) + self.tbButton = QToolButton(self) + self.tbButton.setContentsMargins(0, 0, 0, 0) + self.tbButton.setIconSize(iconSize) + self.tbButton.setFixedSize(fPx, fPx) + self.tbButton.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.tbButton.setVisible(False) + self.tbButton.setToolTip(self.tr("Toggle Tool Bar")) + self.tbButton.clicked.connect(lambda: self.toggleToolBarRequest.emit()) self.searchButton = QToolButton(self) self.searchButton.setContentsMargins(0, 0, 0, 0) - self.searchButton.setIconSize(QSize(fPx, fPx)) + self.searchButton.setIconSize(iconSize) self.searchButton.setFixedSize(fPx, fPx) self.searchButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.searchButton.setVisible(False) - self.searchButton.setToolTip(self.tr("Search document")) - self.searchButton.clicked.connect(self._searchDocument) + self.searchButton.setToolTip(self.tr("Search")) + self.searchButton.clicked.connect(self.docEditor.toggleSearch) self.minmaxButton = QToolButton(self) self.minmaxButton.setContentsMargins(0, 0, 0, 0) - self.minmaxButton.setIconSize(QSize(fPx, fPx)) + self.minmaxButton.setIconSize(iconSize) self.minmaxButton.setFixedSize(fPx, fPx) self.minmaxButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.minmaxButton.setVisible(False) self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode")) - self.minmaxButton.clicked.connect(self._minmaxDocument) + self.minmaxButton.clicked.connect(lambda: self.docEditor.toggleFocusModeRequest.emit()) self.closeButton = QToolButton(self) self.closeButton.setContentsMargins(0, 0, 0, 0) - self.closeButton.setIconSize(QSize(fPx, fPx)) + self.closeButton.setIconSize(iconSize) self.closeButton.setFixedSize(fPx, fPx) self.closeButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.closeButton.setVisible(False) - self.closeButton.setToolTip(self.tr("Close the document")) + self.closeButton.setToolTip(self.tr("Close")) self.closeButton.clicked.connect(self._closeDocument) # Assemble Layout self.outerBox = QHBoxLayout() self.outerBox.setSpacing(hSp) - self.outerBox.addWidget(self.editButton, 0) + self.outerBox.addWidget(self.tbButton, 0) self.outerBox.addWidget(self.searchButton, 0) self.outerBox.addWidget(self.itemTitle, 1) self.outerBox.addWidget(self.minmaxButton, 0) @@ -2583,7 +2673,7 @@ def __init__(self, docEditor: GuiDocEditor) -> None: def updateTheme(self) -> None: """Update theme elements.""" - self.editButton.setIcon(SHARED.theme.getIcon("edit")) + self.tbButton.setIcon(SHARED.theme.getIcon("menu")) self.searchButton.setIcon(SHARED.theme.getIcon("search")) self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise")) self.closeButton.setIcon(SHARED.theme.getIcon("close")) @@ -2593,7 +2683,7 @@ def updateTheme(self) -> None: "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" ).format(*SHARED.theme.colText) - self.editButton.setStyleSheet(buttonStyle) + self.tbButton.setStyleSheet(buttonStyle) self.searchButton.setStyleSheet(buttonStyle) self.minmaxButton.setStyleSheet(buttonStyle) self.closeButton.setStyleSheet(buttonStyle) @@ -2623,7 +2713,7 @@ def setTitleFromHandle(self, tHandle: str | None) -> bool: self._docHandle = tHandle if tHandle is None: self.itemTitle.setText("") - self.editButton.setVisible(False) + self.tbButton.setVisible(False) self.searchButton.setVisible(False) self.closeButton.setVisible(False) self.minmaxButton.setVisible(False) @@ -2645,7 +2735,7 @@ def setTitleFromHandle(self, tHandle: str | None) -> bool: return False self.itemTitle.setText(nwItem.itemName) - self.editButton.setVisible(True) + self.tbButton.setVisible(True) self.searchButton.setVisible(True) self.closeButton.setVisible(True) self.minmaxButton.setVisible(True) @@ -2667,34 +2757,16 @@ def updateFocusMode(self) -> None: # Private Slots ## - @pyqtSlot() - def _editDocument(self) -> None: - """Open the edit item dialog from the main GUI.""" - self.mainGui.editItemLabel(self._docHandle) - return - - @pyqtSlot() - def _searchDocument(self) -> None: - """Toggle the visibility of the search box.""" - self.docEditor.toggleSearch() - return - @pyqtSlot() def _closeDocument(self) -> None: """Trigger the close editor on the main window.""" - self.mainGui.closeDocEditor() - self.editButton.setVisible(False) + self.closeDocumentRequest.emit() + self.tbButton.setVisible(False) self.searchButton.setVisible(False) self.closeButton.setVisible(False) self.minmaxButton.setVisible(False) return - @pyqtSlot() - def _minmaxDocument(self) -> None: - """Switch on or off Focus Mode.""" - self.mainGui.toggleFocusMode() - return - ## # Events ## diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 18dfdbc51..9aff4eee5 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -705,7 +705,7 @@ def __init__(self, docViewer): self.backButton.setFixedSize(fPx, fPx) self.backButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.backButton.setVisible(False) - self.backButton.setToolTip(self.tr("Go backward")) + self.backButton.setToolTip(self.tr("Go Backward")) self.backButton.clicked.connect(self.docViewer.navBackward) self.forwardButton = QToolButton(self) @@ -714,7 +714,7 @@ def __init__(self, docViewer): self.forwardButton.setFixedSize(fPx, fPx) self.forwardButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.forwardButton.setVisible(False) - self.forwardButton.setToolTip(self.tr("Go forward")) + self.forwardButton.setToolTip(self.tr("Go Forward")) self.forwardButton.clicked.connect(self.docViewer.navForward) self.refreshButton = QToolButton(self) @@ -723,7 +723,7 @@ def __init__(self, docViewer): self.refreshButton.setFixedSize(fPx, fPx) self.refreshButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.refreshButton.setVisible(False) - self.refreshButton.setToolTip(self.tr("Reload the document")) + self.refreshButton.setToolTip(self.tr("Reload")) self.refreshButton.clicked.connect(self._refreshDocument) self.closeButton = QToolButton(self) @@ -732,7 +732,7 @@ def __init__(self, docViewer): self.closeButton.setFixedSize(fPx, fPx) self.closeButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.closeButton.setVisible(False) - self.closeButton.setToolTip(self.tr("Close the document")) + self.closeButton.setToolTip(self.tr("Close")) self.closeButton.clicked.connect(self._closeDocument) # Assemble Layout From 9282b39f2a1decc35da35ad1159a1d90b229b1ac Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 8 Nov 2023 16:54:15 +0100 Subject: [PATCH 03/10] Change the menu icon to three dots --- .../assets/icons/typicons_dark/icons.conf | 2 +- .../icons/typicons_dark/typ_th-dot-menu.svg | 4 + .../icons/typicons_dark/typ_th-menu.svg | 4 - .../assets/icons/typicons_light/icons.conf | 2 +- .../icons/typicons_light/typ_th-dot-menu.svg | 4 + .../icons/typicons_light/typ_th-menu.svg | 4 - novelwriter/core/options.py | 8 +- novelwriter/gui/noveltree.py | 224 ++++++++---------- novelwriter/gui/outline.py | 1 + novelwriter/gui/projtree.py | 7 +- 10 files changed, 120 insertions(+), 140 deletions(-) create mode 100644 novelwriter/assets/icons/typicons_dark/typ_th-dot-menu.svg delete mode 100644 novelwriter/assets/icons/typicons_dark/typ_th-menu.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_th-dot-menu.svg delete mode 100644 novelwriter/assets/icons/typicons_light/typ_th-menu.svg diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index d0b695725..c84461231 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -48,7 +48,7 @@ edit = typ_pencil.svg export = typ_export.svg forward = typ_chevron-right.svg maximise = typ_arrow-maximise.svg -menu = typ_th-menu.svg +menu = typ_th-dot-menu.svg minimise = typ_arrow-minimise.svg noncheckable = mixed_input-none.svg proj_chapter = mixed_document-chapter.svg diff --git a/novelwriter/assets/icons/typicons_dark/typ_th-dot-menu.svg b/novelwriter/assets/icons/typicons_dark/typ_th-dot-menu.svg new file mode 100644 index 000000000..19ec3bde0 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/typ_th-dot-menu.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_dark/typ_th-menu.svg b/novelwriter/assets/icons/typicons_dark/typ_th-menu.svg deleted file mode 100644 index 4bf1c92fa..000000000 --- a/novelwriter/assets/icons/typicons_dark/typ_th-menu.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index 22fd94562..d9aa905c5 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -48,7 +48,7 @@ edit = typ_pencil.svg export = typ_export.svg forward = typ_chevron-right.svg maximise = typ_arrow-maximise.svg -menu = typ_th-menu.svg +menu = typ_th-dot-menu.svg minimise = typ_arrow-minimise.svg noncheckable = mixed_input-none.svg proj_chapter = mixed_document-chapter.svg diff --git a/novelwriter/assets/icons/typicons_light/typ_th-dot-menu.svg b/novelwriter/assets/icons/typicons_light/typ_th-dot-menu.svg new file mode 100644 index 000000000..3b5d6383c --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/typ_th-dot-menu.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_light/typ_th-menu.svg b/novelwriter/assets/icons/typicons_light/typ_th-menu.svg deleted file mode 100644 index c23c08ca1..000000000 --- a/novelwriter/assets/icons/typicons_light/typ_th-menu.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py index 0dd62f066..48a0c594b 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -28,7 +28,7 @@ import logging from enum import Enum -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar from pathlib import Path from novelwriter.error import logException @@ -40,6 +40,8 @@ logger = logging.getLogger(__name__) +NWEnum = TypeVar("NWEnum", bound=Enum) + VALID_MAP = { "GuiWritingStats": { "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2", @@ -201,11 +203,11 @@ def getBool(self, group: str, name: str, default: bool) -> bool: return checkBool(self._state[group].get(name, default), default) return default - def getEnum(self, group: str, name: str, lookup: type, default: Enum) -> Enum: + def getEnum(self, group: str, name: str, lookup: type, default: NWEnum) -> NWEnum: """Return the value mapped to an enum. Otherwise return the default value. """ - if issubclass(lookup, Enum): + if issubclass(lookup, type(default)): if group in self._state: if name in self._state[group]: value = self._state[group][name] diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 0742ab548..246837333 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -29,9 +29,10 @@ from enum import Enum from time import time +from typing import TYPE_CHECKING -from PyQt5.QtGui import QFont, QPalette -from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal +from PyQt5.QtGui import QFocusEvent, QFont, QMouseEvent, QPalette, QResizeEvent +from PyQt5.QtCore import QModelIndex, QPoint, Qt, QSize, pyqtSlot, pyqtSignal from PyQt5.QtWidgets import ( QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QInputDialog, QMenu, QSizePolicy, QToolButton, QToolTip, QTreeWidget, @@ -42,8 +43,12 @@ from novelwriter.enum import nwDocMode, nwItemClass, nwOutline from novelwriter.common import minmax from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst +from novelwriter.core.index import IndexHeading from novelwriter.extensions.novelselector import NovelSelector +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) @@ -63,7 +68,7 @@ class GuiNovelView(QWidget): selectedItemChanged = pyqtSignal(str) openDocumentRequest = pyqtSignal(str, Enum, str, bool) - def __init__(self, mainGui): + def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) self.mainGui = mainGui @@ -92,33 +97,29 @@ def __init__(self, mainGui): # Methods ## - def updateTheme(self): - """Update theme elements. - """ + def updateTheme(self) -> None: + """Update theme elements.""" self.novelBar.updateTheme() self.novelTree.updateTheme() self.refreshTree() return - def initSettings(self): - """Initialise GUI elements that depend on specific settings. - """ + def initSettings(self) -> None: + """Initialise GUI elements that depend on specific settings.""" self.novelTree.initSettings() return - def clearNovelView(self): - """Clear project-related GUI content. - """ + def clearNovelView(self) -> None: + """Clear project-related GUI content.""" self.novelTree.clearContent() self.novelBar.clearContent() self.novelBar.setEnabled(False) return - def openProjectTasks(self): - """Run open project tasks. - """ + def openProjectTasks(self) -> None: + """Run open project tasks.""" lastNovel = SHARED.project.data.getLastHandle("novelTree") - if lastNovel not in SHARED.project.tree: + if lastNovel and lastNovel not in SHARED.project.tree: lastNovel = SHARED.project.tree.findRoot(nwItemClass.NOVEL) logger.debug("Setting novel tree to root item '%s'", lastNovel) @@ -140,7 +141,7 @@ def openProjectTasks(self): return - def closeProjectTasks(self): + def closeProjectTasks(self) -> None: """Run closing project tasks.""" lastColType = self.novelTree.lastColType lastColSize = self.novelTree.lastColSize @@ -150,15 +151,13 @@ def closeProjectTasks(self): self.clearNovelView() return - def setTreeFocus(self): - """Set the focus to the tree widget. - """ + def setTreeFocus(self) -> None: + """Set the focus to the tree widget.""" self.novelTree.setFocus() return - def treeHasFocus(self): - """Check if the novel tree has focus. - """ + def treeHasFocus(self) -> bool: + """Check if the novel tree has focus.""" return self.novelTree.hasFocus() ## @@ -166,21 +165,19 @@ def treeHasFocus(self): ## @pyqtSlot() - def refreshTree(self): - """Refresh the current tree. - """ + def refreshTree(self) -> None: + """Refresh the current tree.""" self.novelTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("novelTree")) return @pyqtSlot(str) - def updateRootItem(self, tHandle): - """If any root item changes, rebuild the novel root menu. - """ + def updateRootItem(self, tHandle: str) -> None: + """If any root item changes, rebuild the novel root menu.""" self.novelBar.buildNovelRootMenu() return @pyqtSlot(str) - def updateNovelItemMeta(self, tHandle): + def updateNovelItemMeta(self, tHandle: str) -> None: """The meta data of a novel item has changed, and the tree item needs to be refreshed. """ @@ -192,7 +189,7 @@ def updateNovelItemMeta(self, tHandle): class GuiNovelToolBar(QWidget): - def __init__(self, novelView): + def __init__(self, novelView: GuiNovelView) -> None: super().__init__(parent=novelView) logger.debug("Create: GuiNovelToolBar") @@ -269,9 +266,8 @@ def __init__(self, novelView): # Methods ## - def updateTheme(self): - """Update theme elements. - """ + def updateTheme(self) -> None: + """Update theme elements.""" # Icons self.tbNovel.setIcon(SHARED.theme.getIcon("cls_novel")) self.tbRefresh.setIcon(SHARED.theme.getIcon("refresh")) @@ -287,10 +283,11 @@ def updateTheme(self): "QToolButton {{padding: {0}px; border: none; background: transparent;}} " "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}" ).format(CONFIG.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue()) + buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}" self.tbNovel.setStyleSheet(buttonStyle) self.tbRefresh.setStyleSheet(buttonStyle) - self.tbMore.setStyleSheet(buttonStyle) + self.tbMore.setStyleSheet(buttonStyleMenu) self.novelValue.setStyleSheet( "QComboBox {border-style: none; padding-left: 0;} " @@ -301,30 +298,26 @@ def updateTheme(self): return - def clearContent(self): - """Run clearing project tasks. - """ + def clearContent(self) -> None: + """Run clearing project tasks.""" self.novelValue.clear() self.novelValue.setToolTip("") return - def buildNovelRootMenu(self): - """Build the novel root menu. - """ + def buildNovelRootMenu(self) -> None: + """Build the novel root menu.""" self.novelValue.updateList(prefix=self.novelPrefix) self.tbNovel.setVisible(self.novelValue.count() > 1) return - def setCurrentRoot(self, rootHandle): - """Set the current active root handle. - """ + def setCurrentRoot(self, rootHandle: str | None) -> None: + """Set the current active root handle.""" self.novelValue.setHandle(rootHandle) self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) return - def setLastColType(self, colType, doRefresh=True): - """Set the last column type. - """ + def setLastColType(self, colType: NovelTreeColumn, doRefresh: bool = True) -> None: + """Set the last column type.""" self.aLastCol[colType].setChecked(True) self.novelView.novelTree.setLastColType(colType, doRefresh=doRefresh) return @@ -334,24 +327,21 @@ def setLastColType(self, colType, doRefresh=True): ## @pyqtSlot() - def _openNovelSelector(self): - """Trigger the dropdown list of the novel selector. - """ + def _openNovelSelector(self) -> None: + """Trigger the dropdown list of the novel selector.""" self.novelValue.showPopup() return @pyqtSlot() - def _refreshNovelTree(self): - """Rebuild the current tree. - """ + def _refreshNovelTree(self) -> None: + """Rebuild the current tree.""" rootHandle = SHARED.project.data.getLastHandle("novelTree") self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) return @pyqtSlot() - def _selectLastColumnSize(self): - """Set the maximum width for the last column. - """ + def _selectLastColumnSize(self) -> None: + """Set the maximum width for the last column.""" oldSize = self.novelView.novelTree.lastColSize newSize, isOk = QInputDialog.getInt( self, self.tr("Column Size"), self.tr("Maximum column size in %"), oldSize, 15, 75, 5 @@ -365,9 +355,8 @@ def _selectLastColumnSize(self): # Internal Functions ## - def _addLastColAction(self, colType, actionLabel): - """Add a column selection entry to the last column menu. - """ + def _addLastColAction(self, colType, actionLabel) -> None: + """Add a column selection entry to the last column menu.""" aLast = self.mLastCol.addAction(actionLabel) aLast.setCheckable(True) aLast.setActionGroup(self.gLastCol) @@ -391,7 +380,7 @@ class GuiNovelTree(QTreeWidget): D_KEY = Qt.ItemDataRole.UserRole + 2 D_EXTRA = Qt.ItemDataRole.UserRole + 3 - def __init__(self, novelView): + def __init__(self, novelView: GuiNovelView) -> None: super().__init__(parent=novelView) logger.debug("Create: GuiNovelTree") @@ -461,9 +450,8 @@ def __init__(self, novelView): return - def initSettings(self): - """Set or update tree widget settings. - """ + def initSettings(self) -> None: + """Set or update tree widget settings.""" # Scroll bars if CONFIG.hideVScroll: self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) @@ -477,9 +465,8 @@ def initSettings(self): return - def updateTheme(self): - """Update theme elements. - """ + def updateTheme(self) -> None: + """Update theme elements.""" iPx = SHARED.theme.baseIconSize self._pMore = SHARED.theme.loadDecoration("deco_doc_more", pxH=iPx) return @@ -489,28 +476,26 @@ def updateTheme(self): ## @property - def lastColType(self): + def lastColType(self) -> NovelTreeColumn: return self._lastCol @property - def lastColSize(self): + def lastColSize(self) -> int: return int(self._lastColSize * 100) ## # Class Methods ## - def clearContent(self): - """Clear the GUI content and the related maps. - """ + def clearContent(self) -> None: + """Clear the GUI content and the related maps.""" self.clear() self._treeMap = {} self._lastBuild = 0 return - def refreshTree(self, rootHandle=None, overRide=False): - """Refresh the tree if it has been changed. - """ + def refreshTree(self, rootHandle: str | None = None, overRide: bool = False) -> None: + """Refresh the tree if it has been changed.""" logger.debug("Requesting refresh of the novel tree") if rootHandle is None: rootHandle = SHARED.project.tree.findRoot(nwItemClass.NOVEL) @@ -534,9 +519,8 @@ def refreshTree(self, rootHandle=None, overRide=False): return - def refreshHandle(self, tHandle): - """Refresh the data for a given handle. - """ + def refreshHandle(self, tHandle: str) -> None: + """Refresh the data for a given handle.""" idxData = SHARED.project.index.getItemData(tHandle) if idxData is None: return @@ -554,7 +538,7 @@ def refreshHandle(self, tHandle): return - def getSelectedHandle(self): + def getSelectedHandle(self) -> tuple[str | None, str | None]: """Get the currently selected or active handle. If multiple items are selected, return the first. """ @@ -566,9 +550,8 @@ def getSelectedHandle(self): return tHandle, sTitle return None, None - def setLastColType(self, colType, doRefresh=True): - """Change the content type of the last column and rebuild. - """ + def setLastColType(self, colType: NovelTreeColumn, doRefresh: bool = True) -> None: + """Change the content type of the last column and rebuild.""" if self._lastCol != colType: logger.debug("Changing last column to %s", colType.name) self._lastCol = colType @@ -578,15 +561,13 @@ def setLastColType(self, colType, doRefresh=True): self.refreshTree(rootHandle=lastNovel, overRide=True) return - def setLastColSize(self, colSize): - """Set the column size in integer values between 15 and 75. - """ + def setLastColSize(self, colSize: int) -> None: + """Set the column size in integer values between 15 and 75.""" self._lastColSize = minmax(colSize, 15, 75)/100.0 return - def setActiveHandle(self, tHandle): - """Highlight the rows associated with a given handle. - """ + def setActiveHandle(self, tHandle: str | None) -> None: + """Highlight the rows associated with a given handle.""" tStart = time() self._actHandle = tHandle @@ -612,20 +593,20 @@ def setActiveHandle(self, tHandle): # Events ## - def mousePressEvent(self, theEvent): + def mousePressEvent(self, event: QMouseEvent) -> None: """Overload mousePressEvent to clear selection if clicking the mouse in a blank area of the tree view, and to load a document for viewing if the user middle-clicked. """ - super().mousePressEvent(theEvent) + super().mousePressEvent(event) - if theEvent.button() == Qt.LeftButton: - selItem = self.indexAt(theEvent.pos()) + if event.button() == Qt.LeftButton: + selItem = self.indexAt(event.pos()) if not selItem.isValid(): self.clearSelection() - elif theEvent.button() == Qt.MiddleButton: - selItem = self.itemAt(theEvent.pos()) + elif event.button() == Qt.MiddleButton: + selItem = self.itemAt(event.pos()) if not isinstance(selItem, QTreeWidgetItem): return @@ -637,16 +618,14 @@ def mousePressEvent(self, theEvent): return - def focusOutEvent(self, theEvent): - """Clear the selection when the tree no longer has focus. - """ - super().focusOutEvent(theEvent) + def focusOutEvent(self, event: QFocusEvent) -> None: + """Clear the selection when the tree no longer has focus.""" + super().focusOutEvent(event) self.clearSelection() return - def resizeEvent(self, event): - """Elide labels in the extra column. - """ + def resizeEvent(self, event: QResizeEvent) -> None: + """Elide labels in the extra column.""" super().resizeEvent(event) newW = event.size().width() oldW = event.oldSize().width() @@ -665,18 +644,17 @@ def resizeEvent(self, event): ## @pyqtSlot("QModelIndex") - def _treeItemClicked(self, mIndex): - """The user clicked on an item in the tree. - """ - if mIndex.column() == self.C_MORE: - tHandle = mIndex.siblingAtColumn(self.C_DATA).data(self.D_HANDLE) - sTitle = mIndex.siblingAtColumn(self.C_DATA).data(self.D_TITLE) - tipPos = self.mapToGlobal(self.visualRect(mIndex).topRight()) + def _treeItemClicked(self, index: QModelIndex) -> None: + """The user clicked on an item in the tree.""" + if index.column() == self.C_MORE: + tHandle = index.siblingAtColumn(self.C_DATA).data(self.D_HANDLE) + sTitle = index.siblingAtColumn(self.C_DATA).data(self.D_TITLE) + tipPos = self.mapToGlobal(self.visualRect(index).topRight()) self._popMetaBox(tipPos, tHandle, sTitle) return @pyqtSlot() - def _treeSelectionChange(self): + def _treeSelectionChange(self) -> None: """Extract the handle and line number of the currently selected title, and send it to the tree meta panel. """ @@ -686,7 +664,7 @@ def _treeSelectionChange(self): return @pyqtSlot("QTreeWidgetItem*", int) - def _treeDoubleClick(self, tItem, colNo): + def _treeDoubleClick(self, item: QTreeWidgetItem, column: int) -> None: """Extract the handle and line number of the title double- clicked, and send it to the main gui class for opening in the document editor. @@ -699,9 +677,8 @@ def _treeDoubleClick(self, tItem, colNo): # Internal Functions ## - def _populateTree(self, rootHandle): - """Build the tree based on the project index. - """ + def _populateTree(self, rootHandle: str | None) -> None: + """Build the tree based on the project index.""" self.clearContent() tStart = time() logger.debug("Building novel tree for root item '%s'", rootHandle) @@ -728,9 +705,9 @@ def _populateTree(self, rootHandle): return - def _updateTreeItemValues(self, trItem, idxItem, tHandle, sTitle): - """Set the tree item values from the index entry. - """ + def _updateTreeItemValues(self, trItem: QTreeWidgetItem, idxItem: IndexHeading, + tHandle: str, sTitle: str) -> None: + """Set the tree item values from the index entry.""" iLevel = nwHeaders.H_LEVEL.get(idxItem.level, 0) hDec = SHARED.theme.getHeaderDecoration(iLevel) @@ -750,9 +727,8 @@ def _updateTreeItemValues(self, trItem, idxItem, tHandle, sTitle): return - def _getLastColumnText(self, tHandle, sTitle): - """Generate the text for the last column based on user settings. - """ + def _getLastColumnText(self, tHandle: str, sTitle: str) -> tuple[str, str]: + """Generate text for the last column based on user settings.""" if self._lastCol == NovelTreeColumn.HIDDEN: return "", "" @@ -777,14 +753,15 @@ def _getLastColumnText(self, tHandle, sTitle): return "", "" - def _popMetaBox(self, qPos, tHandle, sTitle): - """Show the novel meta data box. - """ + def _popMetaBox(self, qPos: QPoint, tHandle: str, sTitle: str) -> None: + """Show the novel meta data box.""" logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle) pIndex = SHARED.project.index novIdx = pIndex.getItemHeader(tHandle, sTitle) refTags = pIndex.getReferences(tHandle, sTitle) + if not novIdx: + return synopText = novIdx.synopsis if synopText: @@ -814,9 +791,8 @@ def _popMetaBox(self, qPos, tHandle, sTitle): return @staticmethod - def _appendMetaTag(refs, key, lines): - """Generate a reference list for a given reference key. - """ + def _appendMetaTag(refs: dict, key: str, lines: list[str]) -> list[str]: + """Generate a reference list for a given reference key.""" tags = ", ".join(refs.get(key, [])) if tags: lines.append(f"{trConst(nwLabels.KEY_NAME[key])}: {tags}") diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 84234ccfe..596a422b0 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -261,6 +261,7 @@ def updateTheme(self) -> None: self.novelValue.updateList(includeAll=True) self.aRefresh.setIcon(SHARED.theme.getIcon("refresh")) self.tbColumns.setIcon(SHARED.theme.getIcon("menu")) + self.tbColumns.setStyleSheet("QToolButton::menu-indicator {image: none;}") return def populateNovelList(self) -> None: diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 787f5671d..d1c2f903d 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -361,12 +361,13 @@ def updateTheme(self) -> None: "QToolButton {{padding: {0}px; border: none; background: transparent;}} " "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}" ).format(CONFIG.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue()) + buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}" - self.tbQuick.setStyleSheet(buttonStyle) + self.tbQuick.setStyleSheet(buttonStyleMenu) self.tbMoveU.setStyleSheet(buttonStyle) self.tbMoveD.setStyleSheet(buttonStyle) - self.tbAdd.setStyleSheet(buttonStyle) - self.tbMore.setStyleSheet(buttonStyle) + self.tbAdd.setStyleSheet(buttonStyleMenu) + self.tbMore.setStyleSheet(buttonStyleMenu) self.tbQuick.setIcon(SHARED.theme.getIcon("bookmark")) self.tbMoveU.setIcon(SHARED.theme.getIcon("up")) From 1a6072be2c47b6728049595cebd399507a378287 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 8 Nov 2023 16:54:42 +0100 Subject: [PATCH 04/10] Add editor toolbar icons --- .../assets/icons/typicons_dark/icons.conf | 8 ++++ .../assets/icons/typicons_dark/nw_tb-bold.svg | 4 ++ .../icons/typicons_dark/nw_tb-italic.svg | 4 ++ .../icons/typicons_dark/nw_tb-markdown.svg | 8 ++++ .../icons/typicons_dark/nw_tb-shortcode.svg | 8 ++++ .../icons/typicons_dark/nw_tb-strike.svg | 4 ++ .../icons/typicons_dark/nw_tb-subscript.svg | 5 ++ .../icons/typicons_dark/nw_tb-superscript.svg | 5 ++ .../icons/typicons_dark/nw_tb-underline.svg | 5 ++ .../assets/icons/typicons_light/icons.conf | 8 ++++ .../icons/typicons_light/nw_tb-bold.svg | 4 ++ .../icons/typicons_light/nw_tb-italic.svg | 4 ++ .../icons/typicons_light/nw_tb-markdown.svg | 8 ++++ .../icons/typicons_light/nw_tb-shortcode.svg | 8 ++++ .../icons/typicons_light/nw_tb-strike.svg | 4 ++ .../icons/typicons_light/nw_tb-underline.svg | 5 ++ novelwriter/gui/doceditor.py | 43 +++++++++++++---- novelwriter/gui/docviewer.py | 24 +++------- novelwriter/gui/theme.py | 47 ++++++++++++++----- 19 files changed, 168 insertions(+), 38 deletions(-) create mode 100644 novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg create mode 100644 novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg create mode 100644 novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg create mode 100644 novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg create mode 100644 novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg create mode 100644 novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg create mode 100644 novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg create mode 100644 novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg create mode 100644 novelwriter/assets/icons/typicons_light/nw_tb-bold.svg create mode 100644 novelwriter/assets/icons/typicons_light/nw_tb-italic.svg create mode 100644 novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg create mode 100644 novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg create mode 100644 novelwriter/assets/icons/typicons_light/nw_tb-strike.svg create mode 100644 novelwriter/assets/icons/typicons_light/nw_tb-underline.svg diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index c84461231..94b4c6337 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -46,6 +46,14 @@ cross = typ_times.svg down = typ_chevron-down.svg edit = typ_pencil.svg export = typ_export.svg +fmt_bold = nw_tb-bold.svg +fmt_italic = nw_tb-italic.svg +fmt_mode-md = nw_tb-markdown.svg +fmt_mode-sc = nw_tb-shortcode.svg +fmt_strike = nw_tb-strike.svg +fmt_subscript = nw_tb-subscript.svg +fmt_superscript = nw_tb-superscript.svg +fmt_underline = nw_tb-underline.svg forward = typ_chevron-right.svg maximise = typ_arrow-maximise.svg menu = typ_th-dot-menu.svg diff --git a/novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg b/novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg new file mode 100644 index 000000000..2eaeb846c --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg b/novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg new file mode 100644 index 000000000..0ec5fb2a1 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_tb-italic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg b/novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg new file mode 100644 index 000000000..027685965 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_tb-markdown.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg b/novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg new file mode 100644 index 000000000..4ea59ec38 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_tb-shortcode.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg b/novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg new file mode 100644 index 000000000..639dd6941 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_tb-strike.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg b/novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg new file mode 100644 index 000000000..7566bdee5 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg b/novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg new file mode 100644 index 000000000..0ac13bf39 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg b/novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg new file mode 100644 index 000000000..3a249f4df --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index d9aa905c5..623145114 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -46,6 +46,14 @@ cross = typ_times.svg down = typ_chevron-down.svg edit = typ_pencil.svg export = typ_export.svg +fmt_bold = nw_tb-bold.svg +fmt_italic = nw_tb-italic.svg +fmt_mode-md = nw_tb-markdown.svg +fmt_mode-sc = nw_tb-shortcode.svg +fmt_strike = nw_tb-strike.svg +fmt_subscript = nw_tb-subscript.svg +fmt_superscript = nw_tb-superscript.svg +fmt_underline = nw_tb-underline.svg forward = typ_chevron-right.svg maximise = typ_arrow-maximise.svg menu = typ_th-dot-menu.svg diff --git a/novelwriter/assets/icons/typicons_light/nw_tb-bold.svg b/novelwriter/assets/icons/typicons_light/nw_tb-bold.svg new file mode 100644 index 000000000..3fbfbc881 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_tb-bold.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_tb-italic.svg b/novelwriter/assets/icons/typicons_light/nw_tb-italic.svg new file mode 100644 index 000000000..893d8930d --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_tb-italic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg b/novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg new file mode 100644 index 000000000..24b3809d7 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_tb-markdown.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg b/novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg new file mode 100644 index 000000000..fc19a690f --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_tb-shortcode.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_tb-strike.svg b/novelwriter/assets/icons/typicons_light/nw_tb-strike.svg new file mode 100644 index 000000000..89f4b7ee5 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_tb-strike.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_tb-underline.svg b/novelwriter/assets/icons/typicons_light/nw_tb-underline.svg new file mode 100644 index 000000000..ca122269c --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_tb-underline.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index f12220e51..6e204219b 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -2107,18 +2107,39 @@ def __init__(self, docEditor: GuiDocEditor) -> None: # General Buttons # =============== - self.tbSticky = QToolButton(self) - self.tbSticky.setIconSize(iconSize) + self.tbMode = QToolButton(self) + self.tbMode.setIconSize(iconSize) + self.tbMode.setCheckable(True) - self.tbEdit = QToolButton(self) - self.tbEdit.setIconSize(iconSize) + self.tbBold = QToolButton(self) + self.tbBold.setIconSize(iconSize) + + self.tbItalic = QToolButton(self) + self.tbItalic.setIconSize(iconSize) + + self.tbStrike = QToolButton(self) + self.tbStrike.setIconSize(iconSize) + + self.tbUnderline = QToolButton(self) + self.tbUnderline.setIconSize(iconSize) + + self.tbSuperscript = QToolButton(self) + self.tbSuperscript.setIconSize(iconSize) + + self.tbSubscript = QToolButton(self) + self.tbSubscript.setIconSize(iconSize) # Assemble # ======== self.outerBox = QVBoxLayout() - self.outerBox.addWidget(self.tbSticky) - self.outerBox.addWidget(self.tbEdit) + self.outerBox.addWidget(self.tbMode) + self.outerBox.addWidget(self.tbBold) + self.outerBox.addWidget(self.tbItalic) + self.outerBox.addWidget(self.tbStrike) + self.outerBox.addWidget(self.tbUnderline) + self.outerBox.addWidget(self.tbSuperscript) + self.outerBox.addWidget(self.tbSubscript) self.outerBox.setContentsMargins(cM, cM, cM, cM) self.outerBox.setSpacing(cM) @@ -2150,8 +2171,14 @@ def updateTheme(self) -> None: # buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}" # self.tbEdit.setStyleSheet(buttonStyle) - self.tbSticky.setIcon(SHARED.theme.getIcon("sticky-off")) - self.tbEdit.setIcon(SHARED.theme.getIcon("edit")) + tPx = int(0.8*SHARED.theme.fontPixelSize) + self.tbMode.setIcon(SHARED.theme.getToggleIcon("fmt_mode", (tPx, tPx))) + self.tbBold.setIcon(SHARED.theme.getIcon("fmt_bold")) + self.tbItalic.setIcon(SHARED.theme.getIcon("fmt_italic")) + self.tbStrike.setIcon(SHARED.theme.getIcon("fmt_strike")) + self.tbUnderline.setIcon(SHARED.theme.getIcon("fmt_underline")) + self.tbSuperscript.setIcon(SHARED.theme.getIcon("fmt_superscript")) + self.tbSubscript.setIcon(SHARED.theme.getIcon("fmt_subscript")) return diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 9aff4eee5..ff548fb68 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -34,8 +34,8 @@ from PyQt5.QtCore import pyqtSignal, pyqtSlot, QPoint, QSize, Qt, QUrl from PyQt5.QtGui import ( - QColor, QCursor, QFont, QIcon, QMouseEvent, QPalette, QResizeEvent, - QTextCursor, QTextOption + QColor, QCursor, QFont, QMouseEvent, QPalette, QResizeEvent, QTextCursor, + QTextOption ) from PyQt5.QtWidgets import ( QAction, qApp, QFrame, QHBoxLayout, QLabel, QMenu, QScrollArea, @@ -1020,24 +1020,12 @@ def __init__(self, docViewer): # Methods ## - def updateTheme(self): - """Update theme elements. - """ + def updateTheme(self) -> None: + """Update theme elements.""" # Icons - fPx = int(0.9*SHARED.theme.fontPixelSize) - - stickyOn = SHARED.theme.getPixmap("sticky-on", (fPx, fPx)) - stickyOff = SHARED.theme.getPixmap("sticky-off", (fPx, fPx)) - stickyIcon = QIcon() - stickyIcon.addPixmap(stickyOn, QIcon.Normal, QIcon.On) - stickyIcon.addPixmap(stickyOff, QIcon.Normal, QIcon.Off) - - bulletOn = SHARED.theme.getPixmap("bullet-on", (fPx, fPx)) - bulletOff = SHARED.theme.getPixmap("bullet-off", (fPx, fPx)) - bulletIcon = QIcon() - bulletIcon.addPixmap(bulletOn, QIcon.Normal, QIcon.On) - bulletIcon.addPixmap(bulletOff, QIcon.Normal, QIcon.Off) + stickyIcon = SHARED.theme.getToggleIcon("sticky", (fPx, fPx)) + bulletIcon = SHARED.theme.getToggleIcon("bullet", (fPx, fPx)) self.showHide.setIcon(SHARED.theme.getIcon("reference")) self.stickyRefs.setIcon(stickyIcon) diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index da8f1be4f..22d8cf959 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -130,6 +130,7 @@ def __init__(self): self.getIcon = self.iconCache.getIcon self.getPixmap = self.iconCache.getPixmap self.getItemIcon = self.iconCache.getItemIcon + self.getToggleIcon = self.iconCache.getToggleIcon self.loadDecoration = self.iconCache.loadDecoration self.getHeaderDecoration = self.iconCache.getHeaderDecoration @@ -447,14 +448,22 @@ class GuiIcons: ICON_KEYS = { # Project and GUI Icons "novelwriter", "alert_error", "alert_info", "alert_question", "alert_warn", - "build_excluded", "build_filtered", "build_included", "cls_archive", "cls_character", - "cls_custom", "cls_entity", "cls_none", "cls_novel", "cls_object", "cls_plot", - "cls_timeline", "cls_trash", "cls_world", "proj_chapter", "proj_details", "proj_document", - "proj_folder", "proj_note", "proj_nwx", "proj_section", "proj_scene", "proj_stats", - "proj_title", "search_cancel", "search_case", "search_loop", "search_preserve", - "search_project", "search_regex", "search_word", "status_idle", "status_lang", - "status_lines", "status_stats", "status_time", "view_build", "view_editor", "view_novel", - "view_outline", + "build_excluded", "build_filtered", "build_included", "proj_chapter", "proj_details", + "proj_document", "proj_folder", "proj_note", "proj_nwx", "proj_section", "proj_scene", + "proj_stats", "proj_title", "status_idle", "status_lang", "status_lines", "status_stats", + "status_time", "view_build", "view_editor", "view_novel", "view_outline", + + # Class Icons + "cls_archive", "cls_character", "cls_custom", "cls_entity", "cls_none", "cls_novel", + "cls_object", "cls_plot", "cls_timeline", "cls_trash", "cls_world", + + # Search Icons + "search_cancel", "search_case", "search_loop", "search_preserve", "search_project", + "search_regex", "search_word", + + # Format Icons + "fmt_bold", "fmt_italic", "fmt_mode-md", "fmt_mode-sc", "fmt_strike", "fmt_subscript", + "fmt_superscript", "fmt_underline", # General Button Icons "add", "backward", "bookmark", "browse", "checked", "close", "cross", "down", "edit", @@ -469,6 +478,12 @@ class GuiIcons: "deco_doc_h0", "deco_doc_h1", "deco_doc_h2", "deco_doc_h3", "deco_doc_h4", "deco_doc_more", } + TOGGLE_ICON_KEYS = { + "sticky": ("sticky-on", "sticky-off"), + "bullet": ("bullet-on", "bullet-off"), + "fmt_mode": ("fmt_mode-sc", "fmt_mode-md"), + } + IMAGE_MAP = { "wiz-back": "wizard-back.jpg", } @@ -602,10 +617,7 @@ def loadDecoration(self, decoKey, pxW=None, pxH=None): return theDeco def getIcon(self, iconKey): - """Return an icon from the icon buffer. If it doesn't exist, - return, load it, and if it still doesn't exist, return an empty - icon. - """ + """Return an icon from the icon buffer, or load it.""" if iconKey in self._qIcons: return self._qIcons[iconKey] else: @@ -613,6 +625,17 @@ def getIcon(self, iconKey): self._qIcons[iconKey] = qIcon return qIcon + def getToggleIcon(self, iconKey: str, iconSize: tuple[int, int]) -> QIcon: + """Return a toggle icon from the icon buffer. or load it.""" + if iconKey in self.TOGGLE_ICON_KEYS: + pixOne = self.getPixmap(self.TOGGLE_ICON_KEYS[iconKey][0], iconSize) + pixTwo = self.getPixmap(self.TOGGLE_ICON_KEYS[iconKey][1], iconSize) + qIcon = QIcon() + qIcon.addPixmap(pixOne, QIcon.Normal, QIcon.On) + qIcon.addPixmap(pixTwo, QIcon.Normal, QIcon.Off) + return qIcon + return QIcon() + def getPixmap(self, iconKey, iconSize): """Return an icon from the icon buffer as a QPixmap. If it doesn't exist, return an empty QPixmap. From 711766cf52b5b54ae6982933768e5bfc16244188 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 8 Nov 2023 18:00:43 +0100 Subject: [PATCH 05/10] Change main menu to emit signals --- .../icons/typicons_light/nw_tb-subscript.svg | 5 + .../typicons_light/nw_tb-superscript.svg | 5 + novelwriter/config.py | 30 +- novelwriter/gui/doceditor.py | 106 ++++++-- novelwriter/gui/mainmenu.py | 256 +++++++++++++----- novelwriter/guimain.py | 45 ++- 6 files changed, 324 insertions(+), 123 deletions(-) create mode 100644 novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg create mode 100644 novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg diff --git a/novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg b/novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg new file mode 100644 index 000000000..db909aed7 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg b/novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg new file mode 100644 index 000000000..1634872e7 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/novelwriter/config.py b/novelwriter/config.py index 50c8c1abb..ebd0d05f7 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -178,9 +178,11 @@ def __init__(self) -> None: self.spellLanguage = "en" # State - self.showRefPanel = True # The reference panel for the viewer is visible - self.viewComments = True # Comments are shown in the viewer - self.viewSynopsis = True # Synopsis is shown in the viewer + self.showRefPanel = True # The reference panel for the viewer is visible + self.showEditToolBar = False # The document editor toolbar visibility + self.useShortcodes = False # Use shorcodes for basic formatting + self.viewComments = True # Comments are shown in the viewer + self.viewSynopsis = True # Synopsis is shown in the viewer # Search Bar Switches self.searchCase = False @@ -598,15 +600,17 @@ def loadConfig(self) -> bool: # State sec = "State" - self.showRefPanel = conf.rdBool(sec, "showrefpanel", self.showRefPanel) - self.viewComments = conf.rdBool(sec, "viewcomments", self.viewComments) - self.viewSynopsis = conf.rdBool(sec, "viewsynopsis", self.viewSynopsis) - self.searchCase = conf.rdBool(sec, "searchcase", self.searchCase) - self.searchWord = conf.rdBool(sec, "searchword", self.searchWord) - self.searchRegEx = conf.rdBool(sec, "searchregex", self.searchRegEx) - self.searchLoop = conf.rdBool(sec, "searchloop", self.searchLoop) - self.searchNextFile = conf.rdBool(sec, "searchnextfile", self.searchNextFile) - self.searchMatchCap = conf.rdBool(sec, "searchmatchcap", self.searchMatchCap) + self.showRefPanel = conf.rdBool(sec, "showrefpanel", self.showRefPanel) + self.showEditToolBar = conf.rdBool(sec, "showedittoolbar", self.showEditToolBar) + self.useShortcodes = conf.rdBool(sec, "useshortcodes", self.useShortcodes) + self.viewComments = conf.rdBool(sec, "viewcomments", self.viewComments) + self.viewSynopsis = conf.rdBool(sec, "viewsynopsis", self.viewSynopsis) + self.searchCase = conf.rdBool(sec, "searchcase", self.searchCase) + self.searchWord = conf.rdBool(sec, "searchword", self.searchWord) + self.searchRegEx = conf.rdBool(sec, "searchregex", self.searchRegEx) + self.searchLoop = conf.rdBool(sec, "searchloop", self.searchLoop) + self.searchNextFile = conf.rdBool(sec, "searchnextfile", self.searchNextFile) + self.searchMatchCap = conf.rdBool(sec, "searchmatchcap", self.searchMatchCap) # Deprecated Settings or Locations as of 2.0 # ToDo: These will be loaded for a few minor releases until the users have converted them @@ -721,6 +725,8 @@ def saveConfig(self) -> bool: conf["State"] = { "showrefpanel": str(self.showRefPanel), + "showedittoolbar": str(self.showEditToolBar), + "useshortcodes": str(self.useShortcodes), "viewcomments": str(self.viewComments), "viewsynopsis": str(self.viewSynopsis), "searchcase": str(self.searchCase), diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 6e204219b..efccc3617 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -138,9 +138,12 @@ def __init__(self, mainGui: GuiMain) -> None: self.docFooter = GuiDocEditFooter(self) self.docSearch = GuiDocEditSearch(self) self.docToolBar = GuiDocToolBar(self) + self.docToolBar.setVisible(CONFIG.showEditToolBar) # Connect Signals self.docHeader.closeDocumentRequest.connect(self._closeCurrentDocument) + self.docHeader.toggleToolBarRequest.connect(self._toggleToolBarVisibility) + self.docToolBar.requestDocAction.connect(self.docAction) # Context Menu self.setContextMenuPolicy(Qt.CustomContextMenu) @@ -849,31 +852,11 @@ def insertNewBlock(self, text: str, defaultAfter: bool = True) -> bool: return True - def insertKeyWord(self, keyword: str) -> bool: - """Insert a keyword in the text editor, at the cursor position. - If the insert line is not blank, a new line is started. - """ - if keyword not in nwKeyWords.VALID_KEYS: - logger.error("Invalid keyword '%s'", keyword) - return False - logger.debug("Inserting keyword '%s'", keyword) - state = self.insertNewBlock("%s: " % keyword) - return state - def closeSearch(self) -> bool: """Close the search box.""" self.docSearch.closeSearch() return self.docSearch.isVisible() - @pyqtSlot() - def toggleSearch(self) -> None: - """Toggle the visibility of the search box.""" - if self.docSearch.isVisible(): - self.docSearch.closeSearch() - else: - self.beginSearch() - return - ## # Document Events and Maintenance ## @@ -968,6 +951,27 @@ def updateDocInfo(self, tHandle: str) -> None: self.updateDocMargins() return + @pyqtSlot(str) + def insertKeyWord(self, keyword: str) -> bool: + """Insert a keyword in the text editor, at the cursor position. + If the insert line is not blank, a new line is started. + """ + if keyword not in nwKeyWords.VALID_KEYS: + logger.error("Invalid keyword '%s'", keyword) + return False + logger.debug("Inserting keyword '%s'", keyword) + state = self.insertNewBlock("%s: " % keyword) + return state + + @pyqtSlot() + def toggleSearch(self) -> None: + """Toggle the visibility of the search box.""" + if self.docSearch.isVisible(): + self.docSearch.closeSearch() + else: + self.beginSearch() + return + ## # Private Slots ## @@ -1194,6 +1198,14 @@ def _closeCurrentDocument(self) -> None: self.closeDocumentRequest.emit() return + @pyqtSlot() + def _toggleToolBarVisibility(self) -> None: + """Toggle the visibility of the tool bar.""" + state = not self.docToolBar.isVisible() + self.docToolBar.setVisible(state) + CONFIG.showEditToolBar = state + return + ## # Search & Replace ## @@ -2094,6 +2106,8 @@ class BackgroundWordCounterSignals(QObject): class GuiDocToolBar(QWidget): + requestDocAction = pyqtSignal(nwDocAction) + def __init__(self, docEditor: GuiDocEditor) -> None: super().__init__(parent=docEditor) @@ -2108,26 +2122,41 @@ def __init__(self, docEditor: GuiDocEditor) -> None: # =============== self.tbMode = QToolButton(self) + self.tbMode.setToolTip(self.tr("Toggle Markdown or Shortcodes Mode")) self.tbMode.setIconSize(iconSize) self.tbMode.setCheckable(True) + self.tbMode.setChecked(CONFIG.useShortcodes) + self.tbMode.toggled.connect(self._toggleFormatMode) self.tbBold = QToolButton(self) self.tbBold.setIconSize(iconSize) + self.tbBold.clicked.connect(self._formatBold) self.tbItalic = QToolButton(self) self.tbItalic.setIconSize(iconSize) + self.tbItalic.clicked.connect(self._formatItalic) self.tbStrike = QToolButton(self) self.tbStrike.setIconSize(iconSize) + self.tbStrike.clicked.connect(self._formatStrike) self.tbUnderline = QToolButton(self) self.tbUnderline.setIconSize(iconSize) + self.tbUnderline.clicked.connect( + lambda: self.requestDocAction.emit(nwDocAction.SC_ULINE) + ) self.tbSuperscript = QToolButton(self) self.tbSuperscript.setIconSize(iconSize) + self.tbSuperscript.clicked.connect( + lambda: self.requestDocAction.emit(nwDocAction.SC_SUP) + ) self.tbSubscript = QToolButton(self) self.tbSubscript.setIconSize(iconSize) + self.tbSubscript.clicked.connect( + lambda: self.requestDocAction.emit(nwDocAction.SC_SUB) + ) # Assemble # ======== @@ -2156,7 +2185,6 @@ def updateTheme(self) -> None: palette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack)) palette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText)) palette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) - self.setPalette(palette) # qPalette = self.palette() @@ -2182,6 +2210,40 @@ def updateTheme(self) -> None: return + ## + # Internal Slots + ## + + @pyqtSlot(bool) + def _toggleFormatMode(self, checked: bool) -> None: + """Toggle the formatting mode.""" + CONFIG.useShortcodes = checked + return + + @pyqtSlot() + def _formatBold(self): + """Call the bold format action.""" + self.requestDocAction.emit( + nwDocAction.SC_BOLD if self.tbMode.isChecked() else nwDocAction.STRONG + ) + return + + @pyqtSlot() + def _formatItalic(self): + """Call the italic format action.""" + self.requestDocAction.emit( + nwDocAction.SC_ITALIC if self.tbMode.isChecked() else nwDocAction.EMPH + ) + return + + @pyqtSlot() + def _formatStrike(self): + """Call the strikethrough format action.""" + self.requestDocAction.emit( + nwDocAction.SC_STRIKE if self.tbMode.isChecked() else nwDocAction.STRIKE + ) + return + # END Class GuiDocToolBar @@ -2600,8 +2662,8 @@ def _alertSearchValid(self, isValid: bool) -> None: class GuiDocEditHeader(QWidget): - toggleToolBarRequest = pyqtSignal() closeDocumentRequest = pyqtSignal() + toggleToolBarRequest = pyqtSignal() def __init__(self, docEditor: GuiDocEditor) -> None: super().__init__(parent=docEditor) diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 6355ca63f..e6453a823 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -30,7 +30,7 @@ from urllib.parse import urljoin from urllib.request import pathname2url -from PyQt5.QtCore import QUrl, pyqtSlot +from PyQt5.QtCore import QUrl, pyqtSignal, pyqtSlot from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QMenuBar, QAction @@ -50,6 +50,11 @@ class GuiMainMenu(QMenuBar): add them from this class. """ + requestDocAction = pyqtSignal(nwDocAction) + requestDocInsert = pyqtSignal(nwDocInsert) + requestDocInsertText = pyqtSignal(str) + requestDocKeyWordInsert = pyqtSignal(str) + def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) @@ -68,11 +73,6 @@ def __init__(self, mainGui: GuiMain) -> None: self._buildToolsMenu() self._buildHelpMenu() - # Function Pointers - self._docAction = self.mainGui.passDocumentAction - self._docInsert = self.mainGui.docEditor.insertText - self._insertKeyWord = self.mainGui.docEditor.insertKeyWord - logger.debug("Ready: GuiMainMenu") return @@ -246,12 +246,12 @@ def _buildEditMenu(self) -> None: # Edit > Undo self.aEditUndo = self.editMenu.addAction(self.tr("Undo")) self.aEditUndo.setShortcut("Ctrl+Z") - self.aEditUndo.triggered.connect(lambda: self._docAction(nwDocAction.UNDO)) + self.aEditUndo.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.UNDO)) # Edit > Redo self.aEditRedo = self.editMenu.addAction(self.tr("Redo")) self.aEditRedo.setShortcut("Ctrl+Y") - self.aEditRedo.triggered.connect(lambda: self._docAction(nwDocAction.REDO)) + self.aEditRedo.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.REDO)) # Edit > Separator self.editMenu.addSeparator() @@ -259,17 +259,17 @@ def _buildEditMenu(self) -> None: # Edit > Cut self.aEditCut = self.editMenu.addAction(self.tr("Cut")) self.aEditCut.setShortcut("Ctrl+X") - self.aEditCut.triggered.connect(lambda: self._docAction(nwDocAction.CUT)) + self.aEditCut.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.CUT)) # Edit > Copy self.aEditCopy = self.editMenu.addAction(self.tr("Copy")) self.aEditCopy.setShortcut("Ctrl+C") - self.aEditCopy.triggered.connect(lambda: self._docAction(nwDocAction.COPY)) + self.aEditCopy.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.COPY)) # Edit > Paste self.aEditPaste = self.editMenu.addAction(self.tr("Paste")) self.aEditPaste.setShortcut("Ctrl+V") - self.aEditPaste.triggered.connect(lambda: self._docAction(nwDocAction.PASTE)) + self.aEditPaste.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.PASTE)) # Edit > Separator self.editMenu.addSeparator() @@ -277,12 +277,12 @@ def _buildEditMenu(self) -> None: # Edit > Select All self.aSelectAll = self.editMenu.addAction(self.tr("Select All")) self.aSelectAll.setShortcut("Ctrl+A") - self.aSelectAll.triggered.connect(lambda: self._docAction(nwDocAction.SEL_ALL)) + self.aSelectAll.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.SEL_ALL)) # Edit > Select Paragraph self.aSelectPar = self.editMenu.addAction(self.tr("Select Paragraph")) self.aSelectPar.setShortcut("Ctrl+Shift+A") - self.aSelectPar.triggered.connect(lambda: self._docAction(nwDocAction.SEL_PARA)) + self.aSelectPar.triggered.connect(lambda: self.requestDocAction.emit(nwDocAction.SEL_PARA)) return @@ -350,22 +350,30 @@ def _buildInsertMenu(self) -> None: # Insert > Short Dash self.aInsENDash = self.mInsDashes.addAction(self.tr("Short Dash")) self.aInsENDash.setShortcut("Ctrl+K, -") - self.aInsENDash.triggered.connect(lambda: self._docInsert(nwUnicode.U_ENDASH)) + self.aInsENDash.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_ENDASH) + ) # Insert > Long Dash self.aInsEMDash = self.mInsDashes.addAction(self.tr("Long Dash")) self.aInsEMDash.setShortcut("Ctrl+K, _") - self.aInsEMDash.triggered.connect(lambda: self._docInsert(nwUnicode.U_EMDASH)) + self.aInsEMDash.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_EMDASH) + ) # Insert > Long Dash self.aInsHorBar = self.mInsDashes.addAction(self.tr("Horizontal Bar")) self.aInsHorBar.setShortcut("Ctrl+K, Ctrl+_") - self.aInsHorBar.triggered.connect(lambda: self._docInsert(nwUnicode.U_HBAR)) + self.aInsHorBar.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_HBAR) + ) # Insert > Figure Dash self.aInsFigDash = self.mInsDashes.addAction(self.tr("Figure Dash")) self.aInsFigDash.setShortcut("Ctrl+K, ~") - self.aInsFigDash.triggered.connect(lambda: self._docInsert(nwUnicode.U_FGDASH)) + self.aInsFigDash.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_FGDASH) + ) # Insert > Quote Marks self.mInsQuotes = self.insMenu.addMenu(self.tr("Quote Marks")) @@ -373,27 +381,37 @@ def _buildInsertMenu(self) -> None: # Insert > Left Single Quote self.aInsQuoteLS = self.mInsQuotes.addAction(self.tr("Left Single Quote")) self.aInsQuoteLS.setShortcut("Ctrl+K, 1") - self.aInsQuoteLS.triggered.connect(lambda: self._docInsert(nwDocInsert.QUOTE_LS)) + self.aInsQuoteLS.triggered.connect( + lambda: self.requestDocInsert.emit(nwDocInsert.QUOTE_LS) + ) # Insert > Right Single Quote self.aInsQuoteRS = self.mInsQuotes.addAction(self.tr("Right Single Quote")) self.aInsQuoteRS.setShortcut("Ctrl+K, 2") - self.aInsQuoteRS.triggered.connect(lambda: self._docInsert(nwDocInsert.QUOTE_RS)) + self.aInsQuoteRS.triggered.connect( + lambda: self.requestDocInsert.emit(nwDocInsert.QUOTE_RS) + ) # Insert > Left Double Quote self.aInsQuoteLD = self.mInsQuotes.addAction(self.tr("Left Double Quote")) self.aInsQuoteLD.setShortcut("Ctrl+K, 3") - self.aInsQuoteLD.triggered.connect(lambda: self._docInsert(nwDocInsert.QUOTE_LD)) + self.aInsQuoteLD.triggered.connect( + lambda: self.requestDocInsert.emit(nwDocInsert.QUOTE_LD) + ) # Insert > Right Double Quote self.aInsQuoteRD = self.mInsQuotes.addAction(self.tr("Right Double Quote")) self.aInsQuoteRD.setShortcut("Ctrl+K, 4") - self.aInsQuoteRD.triggered.connect(lambda: self._docInsert(nwDocInsert.QUOTE_RD)) + self.aInsQuoteRD.triggered.connect( + lambda: self.requestDocInsert.emit(nwDocInsert.QUOTE_RD) + ) # Insert > Alternative Apostrophe self.aInsMSApos = self.mInsQuotes.addAction(self.tr("Alternative Apostrophe")) self.aInsMSApos.setShortcut("Ctrl+K, '") - self.aInsMSApos.triggered.connect(lambda: self._docInsert(nwUnicode.U_MAPOSS)) + self.aInsMSApos.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_MAPOSS) + ) # Insert > Symbols self.mInsPunct = self.insMenu.addMenu(self.tr("General Punctuation")) @@ -401,17 +419,23 @@ def _buildInsertMenu(self) -> None: # Insert > Ellipsis self.aInsEllipsis = self.mInsPunct.addAction(self.tr("Ellipsis")) self.aInsEllipsis.setShortcut("Ctrl+K, .") - self.aInsEllipsis.triggered.connect(lambda: self._docInsert(nwUnicode.U_HELLIP)) + self.aInsEllipsis.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_HELLIP) + ) # Insert > Prime self.aInsPrime = self.mInsPunct.addAction(self.tr("Prime")) self.aInsPrime.setShortcut("Ctrl+K, Ctrl+'") - self.aInsPrime.triggered.connect(lambda: self._docInsert(nwUnicode.U_PRIME)) + self.aInsPrime.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_PRIME) + ) # Insert > Double Prime self.aInsDPrime = self.mInsPunct.addAction(self.tr("Double Prime")) self.aInsDPrime.setShortcut("Ctrl+K, Ctrl+\"") - self.aInsDPrime.triggered.connect(lambda: self._docInsert(nwUnicode.U_DPRIME)) + self.aInsDPrime.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_DPRIME) + ) # Insert > White Spaces self.mInsSpace = self.insMenu.addMenu(self.tr("White Spaces")) @@ -419,17 +443,23 @@ def _buildInsertMenu(self) -> None: # Insert > Non-Breaking Space self.aInsNBSpace = self.mInsSpace.addAction(self.tr("Non-Breaking Space")) self.aInsNBSpace.setShortcut("Ctrl+K, Space") - self.aInsNBSpace.triggered.connect(lambda: self._docInsert(nwUnicode.U_NBSP)) + self.aInsNBSpace.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_NBSP) + ) # Insert > Thin Space self.aInsThinSpace = self.mInsSpace.addAction(self.tr("Thin Space")) self.aInsThinSpace.setShortcut("Ctrl+K, Shift+Space") - self.aInsThinSpace.triggered.connect(lambda: self._docInsert(nwUnicode.U_THSP)) + self.aInsThinSpace.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_THSP) + ) # Insert > Thin Non-Breaking Space self.aInsThinNBSpace = self.mInsSpace.addAction(self.tr("Thin Non-Breaking Space")) self.aInsThinNBSpace.setShortcut("Ctrl+K, Ctrl+Space") - self.aInsThinNBSpace.triggered.connect(lambda: self._docInsert(nwUnicode.U_THNBSP)) + self.aInsThinNBSpace.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_THNBSP) + ) # Insert > Symbols self.mInsSymbol = self.insMenu.addMenu(self.tr("Other Symbols")) @@ -437,42 +467,58 @@ def _buildInsertMenu(self) -> None: # Insert > List Bullet self.aInsBullet = self.mInsSymbol.addAction(self.tr("List Bullet")) self.aInsBullet.setShortcut("Ctrl+K, *") - self.aInsBullet.triggered.connect(lambda: self._docInsert(nwUnicode.U_BULL)) + self.aInsBullet.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_BULL) + ) # Insert > Hyphen Bullet self.aInsHyBull = self.mInsSymbol.addAction(self.tr("Hyphen Bullet")) self.aInsHyBull.setShortcut("Ctrl+K, Ctrl+-") - self.aInsHyBull.triggered.connect(lambda: self._docInsert(nwUnicode.U_HYBULL)) + self.aInsHyBull.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_HYBULL) + ) # Insert > Flower Mark self.aInsFlower = self.mInsSymbol.addAction(self.tr("Flower Mark")) self.aInsFlower.setShortcut("Ctrl+K, Ctrl+*") - self.aInsFlower.triggered.connect(lambda: self._docInsert(nwUnicode.U_FLOWER)) + self.aInsFlower.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_FLOWER) + ) # Insert > Per Mille self.aInsPerMille = self.mInsSymbol.addAction(self.tr("Per Mille")) self.aInsPerMille.setShortcut("Ctrl+K, %") - self.aInsPerMille.triggered.connect(lambda: self._docInsert(nwUnicode.U_PERMIL)) + self.aInsPerMille.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_PERMIL) + ) # Insert > Degree Symbol self.aInsDegree = self.mInsSymbol.addAction(self.tr("Degree Symbol")) self.aInsDegree.setShortcut("Ctrl+K, Ctrl+O") - self.aInsDegree.triggered.connect(lambda: self._docInsert(nwUnicode.U_DEGREE)) + self.aInsDegree.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_DEGREE) + ) # Insert > Minus Sign self.aInsMinus = self.mInsSymbol.addAction(self.tr("Minus Sign")) self.aInsMinus.setShortcut("Ctrl+K, Ctrl+M") - self.aInsMinus.triggered.connect(lambda: self._docInsert(nwUnicode.U_MINUS)) + self.aInsMinus.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_MINUS) + ) # Insert > Times Sign self.aInsTimes = self.mInsSymbol.addAction(self.tr("Times Sign")) self.aInsTimes.setShortcut("Ctrl+K, Ctrl+X") - self.aInsTimes.triggered.connect(lambda: self._docInsert(nwUnicode.U_TIMES)) + self.aInsTimes.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_TIMES) + ) # Insert > Division self.aInsDivide = self.mInsSymbol.addAction(self.tr("Division Sign")) self.aInsDivide.setShortcut("Ctrl+K, Ctrl+D") - self.aInsDivide.triggered.connect(lambda: self._docInsert(nwUnicode.U_DIVIDE)) + self.aInsDivide.triggered.connect( + lambda: self.requestDocInsertText.emit(nwUnicode.U_DIVIDE) + ) # Insert > Tags and References self.mInsKeywords = self.insMenu.addMenu(self.tr("Tags and References")) @@ -491,7 +537,7 @@ def _buildInsertMenu(self) -> None: self.mInsKWItems[keyWord][0].setText(trConst(nwLabels.KEY_NAME[keyWord])) self.mInsKWItems[keyWord][0].setShortcut(self.mInsKWItems[keyWord][1]) self.mInsKWItems[keyWord][0].triggered.connect( - lambda n, keyWord=keyWord: self._insertKeyWord(keyWord) + lambda n, keyWord=keyWord: self.requestDocKeyWordInsert.emit(keyWord) ) self.mInsKeywords.addAction(self.mInsKWItems[keyWord][0]) @@ -501,22 +547,30 @@ def _buildInsertMenu(self) -> None: # Insert > Synopsis Comment self.aInsSynopsis = self.mInsComments.addAction(self.tr("Synopsis Comment")) self.aInsSynopsis.setShortcut("Ctrl+K, S") - self.aInsSynopsis.triggered.connect(lambda: self._docInsert(nwDocInsert.SYNOPSIS)) + self.aInsSynopsis.triggered.connect( + lambda: self.requestDocInsert.emit(nwDocInsert.SYNOPSIS) + ) # Insert > Symbols self.mInsBreaks = self.insMenu.addMenu(self.tr("Page Break and Space")) # Insert > New Page self.aInsNewPage = self.mInsBreaks.addAction(self.tr("Page Break")) - self.aInsNewPage.triggered.connect(lambda: self._docInsert(nwDocInsert.NEW_PAGE)) + self.aInsNewPage.triggered.connect( + lambda: self.requestDocInsert.emit(nwDocInsert.NEW_PAGE) + ) # Insert > Vertical Space (Single) self.aInsVSpaceS = self.mInsBreaks.addAction(self.tr("Vertical Space (Single)")) - self.aInsVSpaceS.triggered.connect(lambda: self._docInsert(nwDocInsert.VSPACE_S)) + self.aInsVSpaceS.triggered.connect( + lambda: self.requestDocInsert.emit(nwDocInsert.VSPACE_S) + ) # Insert > Vertical Space (Multi) self.aInsVSpaceM = self.mInsBreaks.addAction(self.tr("Vertical Space (Multi)")) - self.aInsVSpaceM.triggered.connect(lambda: self._docInsert(nwDocInsert.VSPACE_M)) + self.aInsVSpaceM.triggered.connect( + lambda: self.requestDocInsert.emit(nwDocInsert.VSPACE_M) + ) # Insert > Placeholder Text self.aLipsumText = self.mInsBreaks.addAction(self.tr("Placeholder Text")) @@ -532,17 +586,23 @@ def _buildFormatMenu(self) -> None: # Format > Emphasis self.aFmtEmph = self.fmtMenu.addAction(self.tr("Emphasis")) self.aFmtEmph.setShortcut("Ctrl+I") - self.aFmtEmph.triggered.connect(lambda: self._docAction(nwDocAction.EMPH)) + self.aFmtEmph.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.EMPH) + ) # Format > Strong Emphasis self.aFmtStrong = self.fmtMenu.addAction(self.tr("Strong Emphasis")) self.aFmtStrong.setShortcut("Ctrl+B") - self.aFmtStrong.triggered.connect(lambda: self._docAction(nwDocAction.STRONG)) + self.aFmtStrong.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.STRONG) + ) # Format > Strikethrough self.aFmtStrike = self.fmtMenu.addAction(self.tr("Strikethrough")) self.aFmtStrike.setShortcut("Ctrl+D") - self.aFmtStrike.triggered.connect(lambda: self._docAction(nwDocAction.STRIKE)) + self.aFmtStrike.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.STRIKE) + ) # Edit > Separator self.fmtMenu.addSeparator() @@ -550,12 +610,16 @@ def _buildFormatMenu(self) -> None: # Format > Double Quotes self.aFmtDQuote = self.fmtMenu.addAction(self.tr("Wrap Double Quotes")) self.aFmtDQuote.setShortcut("Ctrl+\"") - self.aFmtDQuote.triggered.connect(lambda: self._docAction(nwDocAction.D_QUOTE)) + self.aFmtDQuote.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.D_QUOTE) + ) # Format > Single Quotes self.aFmtSQuote = self.fmtMenu.addAction(self.tr("Wrap Single Quotes")) self.aFmtSQuote.setShortcut("Ctrl+'") - self.aFmtSQuote.triggered.connect(lambda: self._docAction(nwDocAction.S_QUOTE)) + self.aFmtSQuote.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.S_QUOTE) + ) # Format > Separator self.fmtMenu.addSeparator() @@ -564,28 +628,40 @@ def _buildFormatMenu(self) -> None: self.mShortcodes = self.fmtMenu.addMenu(self.tr("More Formats ...")) # Shortcode Italic - self.aScItalic = self.mShortcodes.addAction(self.tr("Italics Shortcode")) - self.aScItalic.triggered.connect(lambda: self._docAction(nwDocAction.SC_ITALIC)) + self.aScItalic = self.mShortcodes.addAction(self.tr("Italics (Shortcode)")) + self.aScItalic.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.SC_ITALIC) + ) # Shortcode Bold - self.aScBold = self.mShortcodes.addAction(self.tr("Bold Shortcode")) - self.aScBold.triggered.connect(lambda: self._docAction(nwDocAction.SC_BOLD)) - - # Shortcode Underline - self.aScULine = self.mShortcodes.addAction(self.tr("Underline Shortcode")) - self.aScULine.triggered.connect(lambda: self._docAction(nwDocAction.SC_ULINE)) + self.aScBold = self.mShortcodes.addAction(self.tr("Bold (Shortcode)")) + self.aScBold.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.SC_BOLD) + ) # Shortcode Strikethrough - self.aScStrike = self.mShortcodes.addAction(self.tr("Strikethrough Shortcode")) - self.aScStrike.triggered.connect(lambda: self._docAction(nwDocAction.SC_STRIKE)) + self.aScStrike = self.mShortcodes.addAction(self.tr("Strikethrough (Shortcode)")) + self.aScStrike.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.SC_STRIKE) + ) + + # Shortcode Underline + self.aScULine = self.mShortcodes.addAction(self.tr("Underline")) + self.aScULine.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.SC_ULINE) + ) # Shortcode Superscript self.aScSuper = self.mShortcodes.addAction(self.tr("Superscript")) - self.aScSuper.triggered.connect(lambda: self._docAction(nwDocAction.SC_SUP)) + self.aScSuper.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.SC_SUP) + ) # Shortcode Subscript self.aScSub = self.mShortcodes.addAction(self.tr("Subscript")) - self.aScSub.triggered.connect(lambda: self._docAction(nwDocAction.SC_SUB)) + self.aScSub.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.SC_SUB) + ) # Format > Separator self.fmtMenu.addSeparator() @@ -593,33 +669,45 @@ def _buildFormatMenu(self) -> None: # Format > Header 1 (Partition) self.aFmtHead1 = self.fmtMenu.addAction(self.tr("Header 1 (Partition)")) self.aFmtHead1.setShortcut("Ctrl+1") - self.aFmtHead1.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_H1)) + self.aFmtHead1.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.BLOCK_H1) + ) # Format > Header 2 (Chapter) self.aFmtHead2 = self.fmtMenu.addAction(self.tr("Header 2 (Chapter)")) self.aFmtHead2.setShortcut("Ctrl+2") - self.aFmtHead2.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_H2)) + self.aFmtHead2.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.BLOCK_H2) + ) # Format > Header 3 (Scene) self.aFmtHead3 = self.fmtMenu.addAction(self.tr("Header 3 (Scene)")) self.aFmtHead3.setShortcut("Ctrl+3") - self.aFmtHead3.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_H3)) + self.aFmtHead3.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.BLOCK_H3) + ) # Format > Header 4 (Section) self.aFmtHead4 = self.fmtMenu.addAction(self.tr("Header 4 (Section)")) self.aFmtHead4.setShortcut("Ctrl+4") - self.aFmtHead4.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_H4)) + self.aFmtHead4.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.BLOCK_H4) + ) # Format > Separator self.fmtMenu.addSeparator() # Format > Novel Title self.aFmtTitle = self.fmtMenu.addAction(self.tr("Novel Title")) - self.aFmtTitle.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_TTL)) + self.aFmtTitle.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.BLOCK_TTL) + ) # Format > Unnumbered Chapter self.aFmtUnNum = self.fmtMenu.addAction(self.tr("Unnumbered Chapter")) - self.aFmtUnNum.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_UNN)) + self.aFmtUnNum.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.BLOCK_UNN) + ) # Format > Separator self.fmtMenu.addSeparator() @@ -627,17 +715,23 @@ def _buildFormatMenu(self) -> None: # Format > Align Left self.aFmtAlignLeft = self.fmtMenu.addAction(self.tr("Align Left")) self.aFmtAlignLeft.setShortcut("Ctrl+5") - self.aFmtAlignLeft.triggered.connect(lambda: self._docAction(nwDocAction.ALIGN_L)) + self.aFmtAlignLeft.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.ALIGN_L) + ) # Format > Align Centre self.aFmtAlignCentre = self.fmtMenu.addAction(self.tr("Align Centre")) self.aFmtAlignCentre.setShortcut("Ctrl+6") - self.aFmtAlignCentre.triggered.connect(lambda: self._docAction(nwDocAction.ALIGN_C)) + self.aFmtAlignCentre.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.ALIGN_C) + ) # Format > Align Right self.aFmtAlignRight = self.fmtMenu.addAction(self.tr("Align Right")) self.aFmtAlignRight.setShortcut("Ctrl+7") - self.aFmtAlignRight.triggered.connect(lambda: self._docAction(nwDocAction.ALIGN_R)) + self.aFmtAlignRight.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.ALIGN_R) + ) # Format > Separator self.fmtMenu.addSeparator() @@ -645,12 +739,16 @@ def _buildFormatMenu(self) -> None: # Format > Indent Left self.aFmtIndentLeft = self.fmtMenu.addAction(self.tr("Indent Left")) self.aFmtIndentLeft.setShortcut("Ctrl+8") - self.aFmtIndentLeft.triggered.connect(lambda: self._docAction(nwDocAction.INDENT_L)) + self.aFmtIndentLeft.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.INDENT_L) + ) # Format > Indent Right self.aFmtIndentRight = self.fmtMenu.addAction(self.tr("Indent Right")) self.aFmtIndentRight.setShortcut("Ctrl+9") - self.aFmtIndentRight.triggered.connect(lambda: self._docAction(nwDocAction.INDENT_R)) + self.aFmtIndentRight.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.INDENT_R) + ) # Format > Separator self.fmtMenu.addSeparator() @@ -658,27 +756,37 @@ def _buildFormatMenu(self) -> None: # Format > Comment self.aFmtComment = self.fmtMenu.addAction(self.tr("Toggle Comment")) self.aFmtComment.setShortcut("Ctrl+/") - self.aFmtComment.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_COM)) + self.aFmtComment.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.BLOCK_COM) + ) # Format > Remove Block Format self.aFmtNoFormat = self.fmtMenu.addAction(self.tr("Remove Block Format")) self.aFmtNoFormat.setShortcuts(["Ctrl+0", "Ctrl+Shift+/"]) - self.aFmtNoFormat.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_TXT)) + self.aFmtNoFormat.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.BLOCK_TXT) + ) # Format > Separator self.fmtMenu.addSeparator() # Format > Replace Single Quotes self.aFmtReplSng = self.fmtMenu.addAction(self.tr("Convert Single Quotes")) - self.aFmtReplSng.triggered.connect(lambda: self._docAction(nwDocAction.REPL_SNG)) + self.aFmtReplSng.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.REPL_SNG) + ) # Format > Replace Double Quotes self.aFmtReplDbl = self.fmtMenu.addAction(self.tr("Convert Double Quotes")) - self.aFmtReplDbl.triggered.connect(lambda: self._docAction(nwDocAction.REPL_DBL)) + self.aFmtReplDbl.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.REPL_DBL) + ) # Format > Remove In-Paragraph Breaks self.aFmtRmBreaks = self.fmtMenu.addAction(self.tr("Remove In-Paragraph Breaks")) - self.aFmtRmBreaks.triggered.connect(lambda: self._docAction(nwDocAction.RM_BREAKS)) + self.aFmtRmBreaks.triggered.connect( + lambda: self.requestDocAction.emit(nwDocAction.RM_BREAKS) + ) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index adcebca8a..413014763 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -62,7 +62,7 @@ from novelwriter.core.coretools import ProjectBuilder from novelwriter.enum import ( - nwDocAction, nwDocMode, nwItemType, nwItemClass, nwWidget, nwView + nwDocAction, nwDocInsert, nwDocMode, nwItemType, nwItemClass, nwWidget, nwView ) from novelwriter.common import getGuiItem, hexToInt from novelwriter.constants import nwFiles @@ -243,6 +243,11 @@ def __init__(self) -> None: SHARED.projectStatusMessage.connect(self.mainStatus.setStatusMessage) SHARED.spellLanguageChanged.connect(self.mainStatus.setLanguage) + self.mainMenu.requestDocAction.connect(self._passDocumentAction) + self.mainMenu.requestDocInsert.connect(self._passDocumentInsert) + self.mainMenu.requestDocInsertText.connect(self._passDocumentInsert) + self.mainMenu.requestDocKeyWordInsert.connect(self.docEditor.insertKeyWord) + self.sideBar.viewChangeRequested.connect(self._changeView) self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox) @@ -735,20 +740,6 @@ def importDocument(self) -> bool: return True - def passDocumentAction(self, action: nwDocAction) -> None: - """Pass on document action to the document viewer if it has - focus, or pass it to the document editor if it or any of - its child widgets have focus. If neither has focus, ignore the - action. - """ - if self.docViewer.hasFocus(): - self.docViewer.docAction(action) - elif self.docEditor.hasFocus(): - self.docEditor.docAction(action) - else: - logger.debug("Action cancelled as neither editor nor viewer has focus") - return - ## # Tree Item Actions ## @@ -1247,6 +1238,30 @@ def _changeView(self, view: nwView) -> None: return + @pyqtSlot(nwDocAction) + def _passDocumentAction(self, action: nwDocAction) -> None: + """Pass on a document action to the document viewer if it has + focus, or pass it to the document editor if it or any of its + child widgets have focus. If neither has focus, ignore it. + """ + if self.docViewer.hasFocus(): + self.docViewer.docAction(action) + elif self.docEditor.hasFocus(): + self.docEditor.docAction(action) + else: + logger.debug("Action cancelled as neither editor nor viewer has focus") + return + + @pyqtSlot(str) + @pyqtSlot(nwDocInsert) + def _passDocumentInsert(self, content: str | nwDocInsert) -> None: + """Pass on a document insert action to the document editor if it + has focus. If not, ignore it. + """ + if self.docEditor.hasFocus(): + self.docEditor.insertText(content) + return + @pyqtSlot() def _timeTick(self) -> None: """Process time tick of the main timer.""" From c88fbf629d19b721363261d0fabf038bd44be07d Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 8 Nov 2023 18:06:19 +0100 Subject: [PATCH 06/10] Update tests --- tests/reference/baseConfig_novelwriter.conf | 2 ++ tests/reference/guiPreferences_novelwriter.conf | 2 ++ tests/test_core/test_core_options.py | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf index f3434415f..95df90720 100644 --- a/tests/reference/baseConfig_novelwriter.conf +++ b/tests/reference/baseConfig_novelwriter.conf @@ -68,6 +68,8 @@ useridletime = 300 [State] showrefpanel = True +showedittoolbar = False +useshortcodes = False viewcomments = True viewsynopsis = True searchcase = False diff --git a/tests/reference/guiPreferences_novelwriter.conf b/tests/reference/guiPreferences_novelwriter.conf index dfa3f3fd5..6ea3a0d53 100644 --- a/tests/reference/guiPreferences_novelwriter.conf +++ b/tests/reference/guiPreferences_novelwriter.conf @@ -68,6 +68,8 @@ useridletime = 300 [State] showrefpanel = True +showedittoolbar = False +useshortcodes = False viewcomments = True viewsynopsis = True searchcase = False diff --git a/tests/test_core/test_core_options.py b/tests/test_core/test_core_options.py index 747a8f73f..013456302 100644 --- a/tests/test_core/test_core_options.py +++ b/tests/test_core/test_core_options.py @@ -140,7 +140,7 @@ def testCoreOptions_SetGet(mockGUI): assert theOpts.getFloat("GuiProjectDetails", "mockItem", None) is None assert theOpts.getBool("GuiProjectDetails", "clearDouble", None) is True assert theOpts.getBool("GuiProjectDetails", "mockItem", None) is None - assert theOpts.getEnum("GuiNovelView", "lastCol", NovelTreeColumn, None) == nwColHidden + assert theOpts.getEnum("GuiNovelView", "lastCol", NovelTreeColumn, nwColHidden) == nwColHidden # Get from non-existent groups assert theOpts.getValue("SomeGroup", "mockItem", None) is None From 5fbcca35a5ac31dec3dc90df3cc09211999b0092 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 8 Nov 2023 18:16:55 +0100 Subject: [PATCH 07/10] Make sure toolbar is hidden when document is closed --- novelwriter/gui/doceditor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index efccc3617..f0aa70416 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -138,7 +138,6 @@ def __init__(self, mainGui: GuiMain) -> None: self.docFooter = GuiDocEditFooter(self) self.docSearch = GuiDocEditSearch(self) self.docToolBar = GuiDocToolBar(self) - self.docToolBar.setVisible(CONFIG.showEditToolBar) # Connect Signals self.docHeader.closeDocumentRequest.connect(self._closeCurrentDocument) @@ -410,6 +409,7 @@ def loadText(self, tHandle, tLine=None) -> bool: qApp.processEvents() self.setDocumentChanged(False) self._qDocument.clearUndoRedoStacks() + self.docToolBar.setVisible(CONFIG.showEditToolBar) qApp.restoreOverrideCursor() @@ -1196,6 +1196,7 @@ def _updateSelCounts(self, cCount: int, wCount: int, pCount: int) -> None: def _closeCurrentDocument(self) -> None: """Close the document. Forwarded to the main Gui.""" self.closeDocumentRequest.emit() + self.docToolBar.setVisible(False) return @pyqtSlot() From 9a1947c0f5ac1dc486090d14a27ba55eb251886a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:26:19 +0100 Subject: [PATCH 08/10] Add colour to sub/superscript icons --- novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg | 4 ++-- novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg | 4 ++-- novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg | 4 ++-- novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg b/novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg index 7566bdee5..e0ae3587e 100644 --- a/novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg +++ b/novelwriter/assets/icons/typicons_dark/nw_tb-subscript.svg @@ -1,5 +1,5 @@ - - + + diff --git a/novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg b/novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg index 0ac13bf39..1d60aec27 100644 --- a/novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg +++ b/novelwriter/assets/icons/typicons_dark/nw_tb-superscript.svg @@ -1,5 +1,5 @@ - - + + diff --git a/novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg b/novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg index db909aed7..2384f6eac 100644 --- a/novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg +++ b/novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg @@ -1,5 +1,5 @@ - - + + diff --git a/novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg b/novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg index 1634872e7..df0aa35c1 100644 --- a/novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg +++ b/novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg @@ -1,5 +1,5 @@ - - + + From 003615c3bede4c399fb5554d07303246b3db6c35 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 8 Nov 2023 21:27:14 +0100 Subject: [PATCH 09/10] Make some minor modifications to the doc toolbar, and add typings in themes --- novelwriter/gui/doceditor.py | 14 +--- novelwriter/gui/noveltree.py | 2 +- novelwriter/gui/theme.py | 133 +++++++++++++++---------------- tests/test_gui/test_gui_theme.py | 6 +- 4 files changed, 69 insertions(+), 86 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index f0aa70416..a3bc291c9 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -2188,18 +2188,6 @@ def updateTheme(self) -> None: palette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) self.setPalette(palette) - # qPalette = self.palette() - # qPalette.setBrush(QPalette.Window, qPalette.base()) - # self.setPalette(qPalette) - - # fadeCol = qPalette.text().color() - # buttonStyle = ( - # "QToolButton {{padding: {0}px; border: none; background: transparent;}} " - # "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}" - # ).format(CONFIG.pxInt(4), fadeCol.red(), fadeCol.green(), fadeCol.blue()) - # buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}" - # self.tbEdit.setStyleSheet(buttonStyle) - tPx = int(0.8*SHARED.theme.fontPixelSize) self.tbMode.setIcon(SHARED.theme.getToggleIcon("fmt_mode", (tPx, tPx))) self.tbBold.setIcon(SHARED.theme.getIcon("fmt_bold")) @@ -2212,7 +2200,7 @@ def updateTheme(self) -> None: return ## - # Internal Slots + # Private Slots ## @pyqtSlot(bool) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 246837333..40aae9efc 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -468,7 +468,7 @@ def initSettings(self) -> None: def updateTheme(self) -> None: """Update theme elements.""" iPx = SHARED.theme.baseIconSize - self._pMore = SHARED.theme.loadDecoration("deco_doc_more", pxH=iPx) + self._pMore = SHARED.theme.loadDecoration("deco_doc_more", h=iPx) return ## diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 22d8cf959..6c4d7e767 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -36,7 +36,7 @@ ) from novelwriter import CONFIG -from novelwriter.enum import nwItemLayout, nwItemType +from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.error import logException from novelwriter.common import NWConfigParser, minmax from novelwriter.constants import nwLabels @@ -51,7 +51,7 @@ class GuiTheme: - def __init__(self): + def __init__(self) -> None: self.iconCache = GuiIcons(self) @@ -183,7 +183,7 @@ def getTextWidth(self, text: str, font: QFont | None = None) -> int: # Theme Methods ## - def loadTheme(self): + def loadTheme(self) -> bool: """Load the currently specified GUI theme.""" guiTheme = CONFIG.guiTheme if guiTheme not in self._availThemes: @@ -270,7 +270,7 @@ def loadTheme(self): return True - def loadSyntax(self): + def loadSyntax(self) -> bool: """Load the currently specified syntax highlighter theme.""" guiSyntax = CONFIG.guiSyntax if guiSyntax not in self._availSyntax: @@ -364,7 +364,7 @@ def listSyntax(self) -> list[tuple[str, str]]: # Internal Functions ## - def _setGuiFont(self): + def _setGuiFont(self) -> None: """Update the GUI's font style from settings.""" theFont = QFont() fontDB = QFontDatabase() @@ -396,9 +396,7 @@ def _listConf(self, targetDict: dict, checkDir: Path) -> bool: return True - def _parseColour( - self, parser: NWConfigParser, section: str, name: str - ) -> list[int]: + def _parseColour(self, parser: NWConfigParser, section: str, name: str) -> list[int]: """Parse a colour value from a config string.""" if parser.has_option(section, name): values = parser.get(section, name).split(",") @@ -415,9 +413,8 @@ def _parseColour( result = [0, 0, 0] return result - def _setPalette( - self, parser: NWConfigParser, section: str, name: str, value: QPalette.ColorRole - ): + def _setPalette(self, parser: NWConfigParser, section: str, + name: str, value: QPalette.ColorRole) -> None: """Set a palette colour value from a config string.""" self._guiPalette.setColor( value, QColor(*self._parseColour(parser, section, name)) @@ -488,17 +485,17 @@ class GuiIcons: "wiz-back": "wizard-back.jpg", } - def __init__(self, mainTheme): + def __init__(self, mainTheme: GuiTheme) -> None: self.mainTheme = mainTheme # Storage - self._qIcons = {} - self._themeMap = {} - self._headerDec = [] - self._confName = "icons.conf" + self._qIcons: dict[str, QIcon] = {} + self._themeMap: dict[str, Path] = {} + self._headerDec: list[QPixmap] = [] # Icon Theme Path + self._confName = "icons.conf" self._iconPath = CONFIG.assetPath("icons") # Icon Theme Meta @@ -516,7 +513,7 @@ def __init__(self, mainTheme): # Actions ## - def loadTheme(self, iconTheme): + def loadTheme(self, iconTheme: str) -> bool: """Update the theme map. This is more of an init, since many of the GUI icons cannot really be replaced without writing specific update functions for the classes where they're used. @@ -590,60 +587,60 @@ def loadTheme(self, iconTheme): # Access Functions ## - def loadDecoration(self, decoKey, pxW=None, pxH=None): + def loadDecoration(self, name: str, w: int | None = None, h: int | None = None) -> QPixmap: """Load graphical decoration element based on the decoration map or the icon map. This function always returns a QPixmap. """ - if decoKey in self._themeMap: - imgPath = self._themeMap[decoKey] - elif decoKey in self.IMAGE_MAP: - imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[decoKey] + if name in self._themeMap: + imgPath = self._themeMap[name] + elif name in self.IMAGE_MAP: + imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name] else: - logger.error("Decoration with name '%s' does not exist", decoKey) + logger.error("Decoration with name '%s' does not exist", name) return QPixmap() if not imgPath.is_file(): logger.error("Asset not found: %s", imgPath) return QPixmap() - theDeco = QPixmap(str(imgPath)) - if pxW is not None and pxH is not None: - return theDeco.scaled(pxW, pxH, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) - elif pxW is None and pxH is not None: - return theDeco.scaledToHeight(pxH, Qt.SmoothTransformation) - elif pxW is not None and pxH is None: - return theDeco.scaledToWidth(pxW, Qt.SmoothTransformation) + pixmap = QPixmap(str(imgPath)) + if w is not None and h is not None: + return pixmap.scaled(w, h, Qt.IgnoreAspectRatio, Qt.SmoothTransformation) + elif w is None and h is not None: + return pixmap.scaledToHeight(h, Qt.SmoothTransformation) + elif w is not None and h is None: + return pixmap.scaledToWidth(w, Qt.SmoothTransformation) - return theDeco + return pixmap - def getIcon(self, iconKey): + def getIcon(self, name: str) -> QIcon: """Return an icon from the icon buffer, or load it.""" - if iconKey in self._qIcons: - return self._qIcons[iconKey] + if name in self._qIcons: + return self._qIcons[name] else: - qIcon = self._loadIcon(iconKey) - self._qIcons[iconKey] = qIcon - return qIcon + icon = self._loadIcon(name) + self._qIcons[name] = icon + return icon - def getToggleIcon(self, iconKey: str, iconSize: tuple[int, int]) -> QIcon: + def getToggleIcon(self, name: str, size: tuple[int, int]) -> QIcon: """Return a toggle icon from the icon buffer. or load it.""" - if iconKey in self.TOGGLE_ICON_KEYS: - pixOne = self.getPixmap(self.TOGGLE_ICON_KEYS[iconKey][0], iconSize) - pixTwo = self.getPixmap(self.TOGGLE_ICON_KEYS[iconKey][1], iconSize) - qIcon = QIcon() - qIcon.addPixmap(pixOne, QIcon.Normal, QIcon.On) - qIcon.addPixmap(pixTwo, QIcon.Normal, QIcon.Off) - return qIcon + if name in self.TOGGLE_ICON_KEYS: + pOne = self.getPixmap(self.TOGGLE_ICON_KEYS[name][0], size) + pTwo = self.getPixmap(self.TOGGLE_ICON_KEYS[name][1], size) + icon = QIcon() + icon.addPixmap(pOne, QIcon.Normal, QIcon.On) + icon.addPixmap(pTwo, QIcon.Normal, QIcon.Off) + return icon return QIcon() - def getPixmap(self, iconKey, iconSize): + def getPixmap(self, name: str, size: tuple[int, int]) -> QPixmap: """Return an icon from the icon buffer as a QPixmap. If it doesn't exist, return an empty QPixmap. """ - qIcon = self.getIcon(iconKey) - return qIcon.pixmap(iconSize[0], iconSize[1], QIcon.Normal) + return self.getIcon(name).pixmap(size[0], size[1], QIcon.Normal) - def getItemIcon(self, tType, tClass, tLayout, hLevel="H0"): + def getItemIcon(self, tType: nwItemType, tClass: nwItemClass, + tLayout: nwItemLayout, hLevel: str = "H0") -> QIcon: """Get the correct icon for a project item based on type, class and header level """ @@ -670,17 +667,16 @@ def getItemIcon(self, tType, tClass, tLayout, hLevel="H0"): return self.getIcon(iconName) - def getHeaderDecoration(self, hLevel): - """Get the decoration for a specific header level. - """ + def getHeaderDecoration(self, hLevel: int) -> QPixmap: + """Get the decoration for a specific header level.""" if not self._headerDec: iPx = self.mainTheme.baseIconSize self._headerDec = [ - self.loadDecoration("deco_doc_h0", pxH=iPx), - self.loadDecoration("deco_doc_h1", pxH=iPx), - self.loadDecoration("deco_doc_h2", pxH=iPx), - self.loadDecoration("deco_doc_h3", pxH=iPx), - self.loadDecoration("deco_doc_h4", pxH=iPx), + self.loadDecoration("deco_doc_h0", h=iPx), + self.loadDecoration("deco_doc_h1", h=iPx), + self.loadDecoration("deco_doc_h2", h=iPx), + self.loadDecoration("deco_doc_h3", h=iPx), + self.loadDecoration("deco_doc_h4", h=iPx), ] return self._headerDec[minmax(hLevel, 0, 4)] @@ -688,27 +684,27 @@ def getHeaderDecoration(self, hLevel): # Internal Functions ## - def _loadIcon(self, iconKey): + def _loadIcon(self, name: str) -> QIcon: """Load an icon from the assets themes folder. Is guaranteed to return a QIcon. """ - if iconKey not in self.ICON_KEYS: - logger.error("Requested unknown icon name '%s'", iconKey) + if name not in self.ICON_KEYS: + logger.error("Requested unknown icon name '%s'", name) return QIcon() # If we just want the app icons, return right away - if iconKey == "novelwriter": + if name == "novelwriter": return QIcon(str(self._iconPath / "novelwriter.svg")) - elif iconKey == "proj_nwx": + elif name == "proj_nwx": return QIcon(str(self._iconPath / "x-novelwriter-project.svg")) # Otherwise, we load from the theme folder - if iconKey in self._themeMap: - logger.debug("Loading: %s", self._themeMap[iconKey].name) - return QIcon(str(self._themeMap[iconKey])) + if name in self._themeMap: + logger.debug("Loading: %s", self._themeMap[name].name) + return QIcon(str(self._themeMap[name])) # If we didn't find one, give up and return an empty icon - logger.warning("Did not load an icon for '%s'", iconKey) + logger.warning("Did not load an icon for '%s'", name) return QIcon() @@ -719,9 +715,8 @@ def _loadIcon(self, iconKey): # Module Functions # =============================================================================================== # -def _loadInternalName(confParser, confFile): - """Open a conf file and read the 'name' setting. - """ +def _loadInternalName(confParser: NWConfigParser, confFile: str | Path) -> str: + """Open a conf file and read the 'name' setting.""" try: with open(confFile, mode="r", encoding="utf-8") as inFile: confParser.read_file(inFile) diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index f476ca94d..cfb16ab17 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -322,17 +322,17 @@ def testGuiTheme_Icons(qtbot, caplog, monkeypatch, nwGUI, tstPaths): assert qPix.isNull() is True # Test image sizes - qPix = iconCache.loadDecoration("wiz-back", pxW=100, pxH=None) + qPix = iconCache.loadDecoration("wiz-back", w=100, h=None) assert qPix.isNull() is False assert qPix.width() == 100 assert qPix.height() > 100 - qPix = iconCache.loadDecoration("wiz-back", pxW=None, pxH=100) + qPix = iconCache.loadDecoration("wiz-back", w=None, h=100) assert qPix.isNull() is False assert qPix.width() < 100 assert qPix.height() == 100 - qPix = iconCache.loadDecoration("wiz-back", pxW=100, pxH=100) + qPix = iconCache.loadDecoration("wiz-back", w=100, h=100) assert qPix.isNull() is False assert qPix.width() == 100 assert qPix.height() == 100 From 6d411e17a6290371858f52a225f347c2fbe8df58 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 8 Nov 2023 23:12:39 +0100 Subject: [PATCH 10/10] Improve test coverage --- tests/test_gui/test_gui_doceditor.py | 385 +++++++++++++++++++-------- 1 file changed, 278 insertions(+), 107 deletions(-) diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 9a28e5c2b..064fcf17b 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -24,15 +24,15 @@ from tools import C, buildTestProject from mocked import causeOSError -from PyQt5.QtCore import QThreadPool, Qt from PyQt5.QtGui import QTextBlock, QTextCursor, QTextOption +from PyQt5.QtCore import QThreadPool, Qt from PyQt5.QtWidgets import QAction, 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 +from novelwriter.gui.doceditor import GuiDocEditor, GuiDocToolBar KEY_DELAY = 1 @@ -214,9 +214,8 @@ def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd): buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True - theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText) - nwGUI.docEditor.replaceText(theText) - + text = "### A Scene\n\n%s" % "\n\n".join(ipsumText) + nwGUI.docEditor.replaceText(text) theDoc = nwGUI.docEditor.document() # Select/Cut/Copy/Paste/Undo/Redo @@ -228,7 +227,7 @@ def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd): assert nwGUI.docEditor.docAction(nwDocAction.SEL_ALL) is True theCursor = nwGUI.docEditor.textCursor() assert theCursor.hasSelection() is True - assert theCursor.selectedText() == theText.replace("\n", "\u2029") + assert theCursor.selectedText() == text.replace("\n", "\u2029") theCursor.clearSelection() # Select Paragraph @@ -239,7 +238,7 @@ def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd): assert theCursor.selectedText() == ipsumText[1] # Cut Selected Text - nwGUI.docEditor.replaceText(theText) + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(1000) assert nwGUI.docEditor.docAction(nwDocAction.SEL_PARA) is True assert nwGUI.docEditor.docAction(nwDocAction.CUT) is True @@ -254,10 +253,10 @@ def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd): # Paste Back In assert nwGUI.docEditor.docAction(nwDocAction.PASTE) is True - assert nwGUI.docEditor.getText() == theText + assert nwGUI.docEditor.getText() == text # Copy Next Paragraph - nwGUI.docEditor.replaceText(theText) + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(1500) assert nwGUI.docEditor.docAction(nwDocAction.SEL_PARA) is True assert nwGUI.docEditor.docAction(nwDocAction.COPY) is True @@ -279,84 +278,132 @@ def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd): # Emphasis/Undo/Redo # ================== - theText = "### A Scene\n\n%s" % ipsumText[0] - nwGUI.docEditor.replaceText(theText) + text = "### A Scene\n\n%s" % ipsumText[0] + nwGUI.docEditor.replaceText(text) # Emphasis nwGUI.docEditor.setCursorPosition(50) assert nwGUI.docEditor.docAction(nwDocAction.EMPH) is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "_consectetur_") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "_consectetur_") assert nwGUI.docEditor.docAction(nwDocAction.UNDO) is True - assert nwGUI.docEditor.getText() == theText + assert nwGUI.docEditor.getText() == text # Strong nwGUI.docEditor.setCursorPosition(50) assert nwGUI.docEditor.docAction(nwDocAction.STRONG) is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "**consectetur**") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "**consectetur**") assert nwGUI.docEditor.docAction(nwDocAction.UNDO) is True - assert nwGUI.docEditor.getText() == theText + assert nwGUI.docEditor.getText() == text # Strikeout nwGUI.docEditor.setCursorPosition(50) assert nwGUI.docEditor.docAction(nwDocAction.STRIKE) is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "~~consectetur~~") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "~~consectetur~~") assert nwGUI.docEditor.docAction(nwDocAction.UNDO) is True - assert nwGUI.docEditor.getText() == theText + assert nwGUI.docEditor.getText() == text # Redo assert nwGUI.docEditor.docAction(nwDocAction.REDO) is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "~~consectetur~~") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "~~consectetur~~") + assert nwGUI.docEditor.docAction(nwDocAction.UNDO) is True + assert nwGUI.docEditor.getText() == text + + # Shortcodes + # ========== + + text = "### A Scene\n\n%s" % ipsumText[0] + nwGUI.docEditor.replaceText(text) + + # Italic + nwGUI.docEditor.setCursorPosition(46) + assert nwGUI.docEditor.docAction(nwDocAction.SC_ITALIC) is True + assert nwGUI.docEditor.getText() == text.replace("consectetur", "[i]consectetur[/i]") + assert nwGUI.docEditor.docAction(nwDocAction.UNDO) is True + assert nwGUI.docEditor.getText() == text + + # Bold + nwGUI.docEditor.setCursorPosition(46) + assert nwGUI.docEditor.docAction(nwDocAction.SC_BOLD) is True + assert nwGUI.docEditor.getText() == text.replace("consectetur", "[b]consectetur[/b]") + assert nwGUI.docEditor.docAction(nwDocAction.UNDO) is True + assert nwGUI.docEditor.getText() == text + + # Strikethrough + nwGUI.docEditor.setCursorPosition(46) + assert nwGUI.docEditor.docAction(nwDocAction.SC_STRIKE) is True + assert nwGUI.docEditor.getText() == text.replace("consectetur", "[s]consectetur[/s]") + assert nwGUI.docEditor.docAction(nwDocAction.UNDO) is True + assert nwGUI.docEditor.getText() == text + + # Underline + nwGUI.docEditor.setCursorPosition(46) + assert nwGUI.docEditor.docAction(nwDocAction.SC_ULINE) is True + assert nwGUI.docEditor.getText() == text.replace("consectetur", "[u]consectetur[/u]") + assert nwGUI.docEditor.docAction(nwDocAction.UNDO) is True + assert nwGUI.docEditor.getText() == text + + # Superscript + nwGUI.docEditor.setCursorPosition(46) + assert nwGUI.docEditor.docAction(nwDocAction.SC_SUP) is True + assert nwGUI.docEditor.getText() == text.replace("consectetur", "[sup]consectetur[/sup]") + assert nwGUI.docEditor.docAction(nwDocAction.UNDO) is True + assert nwGUI.docEditor.getText() == text + + # Subscript + nwGUI.docEditor.setCursorPosition(46) + assert nwGUI.docEditor.docAction(nwDocAction.SC_SUB) is True + assert nwGUI.docEditor.getText() == text.replace("consectetur", "[sub]consectetur[/sub]") assert nwGUI.docEditor.docAction(nwDocAction.UNDO) is True - assert nwGUI.docEditor.getText() == theText + assert nwGUI.docEditor.getText() == text # Quotes # ====== - theText = "### A Scene\n\n%s" % ipsumText[0] - nwGUI.docEditor.replaceText(theText) + text = "### A Scene\n\n%s" % ipsumText[0] + nwGUI.docEditor.replaceText(text) # Add Single Quotes nwGUI.docEditor.setCursorPosition(50) assert nwGUI.docEditor.docAction(nwDocAction.S_QUOTE) is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "\u2018consectetur\u2019") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "\u2018consectetur\u2019") assert nwGUI.docEditor.docAction(nwDocAction.UNDO) is True - assert nwGUI.docEditor.getText() == theText + assert nwGUI.docEditor.getText() == text # Add Double Quotes nwGUI.docEditor.setCursorPosition(50) assert nwGUI.docEditor.docAction(nwDocAction.D_QUOTE) is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "\u201cconsectetur\u201d") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "\u201cconsectetur\u201d") assert nwGUI.docEditor.docAction(nwDocAction.UNDO) is True - assert nwGUI.docEditor.getText() == theText + assert nwGUI.docEditor.getText() == text # Replace Single Quotes - repText = theText.replace("consectetur", "'consectetur'") + repText = text.replace("consectetur", "'consectetur'") nwGUI.docEditor.replaceText(repText) assert nwGUI.docEditor.docAction(nwDocAction.SEL_ALL) is True assert nwGUI.docEditor.docAction(nwDocAction.REPL_SNG) is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "\u2018consectetur\u2019") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "\u2018consectetur\u2019") # Replace Double Quotes - repText = theText.replace("consectetur", "\"consectetur\"") + repText = text.replace("consectetur", "\"consectetur\"") nwGUI.docEditor.replaceText(repText) assert nwGUI.docEditor.docAction(nwDocAction.SEL_ALL) is True assert nwGUI.docEditor.docAction(nwDocAction.REPL_DBL) is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "\u201cconsectetur\u201d") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "\u201cconsectetur\u201d") # Remove Line Breaks # ================== - theText = "### A Scene\n\n%s" % ipsumText[0] - repText = theText[:100] + theText[100:].replace(" ", "\n", 3) + text = "### A Scene\n\n%s" % ipsumText[0] + repText = text[:100] + text[100:].replace(" ", "\n", 3) nwGUI.docEditor.replaceText(repText) assert nwGUI.docEditor.docAction(nwDocAction.RM_BREAKS) is True - assert nwGUI.docEditor.getText().strip() == theText.strip() + assert nwGUI.docEditor.getText().strip() == text.strip() # Format Block # ============ - theText = "## Scene Title\n\nScene text.\n\n" - nwGUI.docEditor.replaceText(theText) + text = "## Scene Title\n\nScene text.\n\n" + nwGUI.docEditor.replaceText(text) # Header 1 nwGUI.docEditor.setCursorPosition(0) @@ -437,20 +484,125 @@ def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd): # END Test testGuiEditor_Actions +@pytest.mark.gui +def testGuiEditor_ToolBar(qtbot, nwGUI, projPath, mockRnd): + """Test the document actions. This is not an extensive test of the + action features, just that the actions are actually called. The + various action features are tested when their respective functions + are tested. + """ + buildTestProject(nwGUI, projPath) + assert nwGUI.openDocument(C.hSceneDoc) is True + + docEditor: GuiDocEditor = nwGUI.docEditor + docToolBar: GuiDocToolBar = docEditor.docToolBar + + text = ( + "### A Scene\n\n" + "Text bold one\n\n" + "Text bold two\n\n" + "Text italic one\n\n" + "Text italic two\n\n" + "Text strikethrough one\n\n" + "Text strikethrough two\n\n" + "Text underline one\n\n" + "Text superscript one\n\n" + "Text subscript one\n\n" + ) + length = len(text) + docEditor.replaceText(text) + assert len(docEditor.getText()) == length + + # Show the ToolBar + assert docToolBar.isVisible() is False + docEditor._toggleToolBarVisibility() + assert docToolBar.isVisible() is True + + # Markdown Mode + assert docToolBar.tbMode.isChecked() is False + + # Click Bold + docEditor.setCursorPosition(20) + docToolBar.tbBold.click() + assert len(docEditor.getText()) == length + 4 + + # Click Italic + docEditor.setCursorPosition(54) + docToolBar.tbItalic.click() + assert len(docEditor.getText()) == length + 6 + + # Click Strikethrough + docEditor.setCursorPosition(90) + docToolBar.tbStrike.click() + assert len(docEditor.getText()) == length + 10 + + # Shortcode Mode + docToolBar.tbMode.click() + assert docToolBar.tbMode.isChecked() is True + + # Click Bold + docEditor.setCursorPosition(39) + docToolBar.tbBold.click() + assert len(docEditor.getText()) == length + 17 + + # Click Italic + docEditor.setCursorPosition(80) + docToolBar.tbItalic.click() + assert len(docEditor.getText()) == length + 24 + + # Click Strikethrough + docEditor.setCursorPosition(132) + docToolBar.tbStrike.click() + assert len(docEditor.getText()) == length + 31 + + # Click Underline + docEditor.setCursorPosition(163) + docToolBar.tbUnderline.click() + assert len(docEditor.getText()) == length + 38 + + # Click Superscript + docEditor.setCursorPosition(190) + docToolBar.tbSuperscript.click() + assert len(docEditor.getText()) == length + 49 + + # Click Subscript + docEditor.setCursorPosition(223) + docToolBar.tbSubscript.click() + assert len(docEditor.getText()) == length + 60 + + # Check Result + assert docEditor.getText() == ( + "### A Scene\n\n" + "Text **bold** one\n\n" + "Text [b]bold[/b] two\n\n" + "Text _italic_ one\n\n" + "Text [i]italic[/i] two\n\n" + "Text ~~strikethrough~~ one\n\n" + "Text [s]strikethrough[/s] two\n\n" + "Text [u]underline[/u] one\n\n" + "Text [sup]superscript[/sup] one\n\n" + "Text [sub]subscript[/sub] one\n\n" + ) + + # qtbot.stop() + +# END Test testGuiEditor_ToolBar + + @pytest.mark.gui def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd): """Test the document insert functions.""" buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True - theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText) - nwGUI.docEditor.replaceText(theText) + text = "### A Scene\n\n%s" % "\n\n".join(ipsumText) + nwGUI.docEditor.replaceText(text) # Insert Text # =========== - theText = "### A Scene\n\n%s" % ipsumText[0] - nwGUI.docEditor.replaceText(theText) + text = "### A Scene\n\n%s" % ipsumText[0] + nwGUI.docEditor.replaceText(text) # No Document Handle nwGUI.docEditor._docHandle = None @@ -461,23 +613,23 @@ def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd # Insert String nwGUI.docEditor.setCursorPosition(24) assert nwGUI.docEditor.insertText(", ipsumer,") is True - assert nwGUI.docEditor.getText() == theText[:24] + ", ipsumer," + theText[24:] + assert nwGUI.docEditor.getText() == text[:24] + ", ipsumer," + text[24:] # Single Quotes - nwGUI.docEditor.replaceText(theText) + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(41) assert nwGUI.docEditor.insertText(nwDocInsert.QUOTE_LS) is True nwGUI.docEditor.setCursorPosition(53) assert nwGUI.docEditor.insertText(nwDocInsert.QUOTE_RS) is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "\u2018consectetur\u2019") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "\u2018consectetur\u2019") # Double Quotes - nwGUI.docEditor.replaceText(theText) + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(41) assert nwGUI.docEditor.insertText(nwDocInsert.QUOTE_LD) is True nwGUI.docEditor.setCursorPosition(53) assert nwGUI.docEditor.insertText(nwDocInsert.QUOTE_RD) is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "\u201cconsectetur\u201d") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "\u201cconsectetur\u201d") # Invalid Inserts assert nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT) is False @@ -486,18 +638,18 @@ def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd # Insert KeyWords # =============== - theText = "### A Scene\n\n\n%s" % ipsumText[0] - nwGUI.docEditor.replaceText(theText) + text = "### A Scene\n\n\n%s" % ipsumText[0] + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorLine(3) # Invalid Keyword assert nwGUI.docEditor.insertKeyWord("stuff") is False - assert nwGUI.docEditor.getText() == theText + assert nwGUI.docEditor.getText() == text # Valid Keyword assert nwGUI.docEditor.insertKeyWord(nwKeyWords.POV_KEY) is True assert nwGUI.docEditor.insertText("Jane\n") - assert nwGUI.docEditor.getText() == theText.replace( + assert nwGUI.docEditor.getText() == text.replace( "\n\n\n", "\n\n@pov: Jane\n\n", 1 ) @@ -510,7 +662,7 @@ def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd nwGUI.docEditor.setCursorPosition(20) assert nwGUI.docEditor.insertKeyWord(nwKeyWords.CHAR_KEY) is True assert nwGUI.docEditor.insertText("John") - assert nwGUI.docEditor.getText() == theText.replace( + assert nwGUI.docEditor.getText() == text.replace( "\n\n\n", "\n\n@pov: Jane\n@char: John\n\n", 1 ) @@ -525,45 +677,45 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex buildTestProject(nwGUI, projPath) assert nwGUI.openDocument(C.hSceneDoc) is True - theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText) - nwGUI.docEditor.replaceText(theText) + text = "### A Scene\n\n%s" % "\n\n".join(ipsumText) + nwGUI.docEditor.replaceText(text) # Clear Surrounding # ================= # No Selection - theText = "### A Scene\n\n%s" % ipsumText[0] - nwGUI.docEditor.replaceText(theText) + text = "### A Scene\n\n%s" % ipsumText[0] + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) - theCursor = nwGUI.docEditor.textCursor() - assert nwGUI.docEditor._clearSurrounding(theCursor, 1) is False + cursor = nwGUI.docEditor.textCursor() + assert nwGUI.docEditor._clearSurrounding(cursor, 1) is False # Clear Characters, 1 Layer - repText = theText.replace("consectetur", "=consectetur=") + repText = text.replace("consectetur", "=consectetur=") nwGUI.docEditor.replaceText(repText) nwGUI.docEditor.setCursorPosition(45) - theCursor = nwGUI.docEditor.textCursor() - theCursor.select(QTextCursor.WordUnderCursor) - assert nwGUI.docEditor._clearSurrounding(theCursor, 1) is True - assert nwGUI.docEditor.getText() == theText + cursor = nwGUI.docEditor.textCursor() + cursor.select(QTextCursor.WordUnderCursor) + assert nwGUI.docEditor._clearSurrounding(cursor, 1) is True + assert nwGUI.docEditor.getText() == text # Clear Characters, 2 Layers - repText = theText.replace("consectetur", "==consectetur==") + repText = text.replace("consectetur", "==consectetur==") nwGUI.docEditor.replaceText(repText) nwGUI.docEditor.setCursorPosition(45) - theCursor = nwGUI.docEditor.textCursor() - theCursor.select(QTextCursor.WordUnderCursor) - assert nwGUI.docEditor._clearSurrounding(theCursor, 2) is True - assert nwGUI.docEditor.getText() == theText + cursor = nwGUI.docEditor.textCursor() + cursor.select(QTextCursor.WordUnderCursor) + assert nwGUI.docEditor._clearSurrounding(cursor, 2) is True + assert nwGUI.docEditor.getText() == text # Wrap Selection # ============== - theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText[0:2]) - nwGUI.docEditor.replaceText(theText) + text = "### A Scene\n\n%s" % "\n\n".join(ipsumText[0:2]) + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) # No Selection @@ -572,23 +724,23 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex assert nwGUI.docEditor._wrapSelection("=", "=") is False # Wrap Equal - nwGUI.docEditor.replaceText(theText) + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) assert nwGUI.docEditor._wrapSelection("=") is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "=consectetur=") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "=consectetur=") # Wrap Unequal - nwGUI.docEditor.replaceText(theText) + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) assert nwGUI.docEditor._wrapSelection("=", "*") is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "=consectetur*") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "=consectetur*") # Past Paragraph - nwGUI.docEditor.replaceText(theText) - theCursor = nwGUI.docEditor.textCursor() - theCursor.setPosition(13, QTextCursor.MoveAnchor) - theCursor.setPosition(1000, QTextCursor.KeepAnchor) - nwGUI.docEditor.setTextCursor(theCursor) + nwGUI.docEditor.replaceText(text) + cursor = nwGUI.docEditor.textCursor() + cursor.setPosition(13, QTextCursor.MoveAnchor) + cursor.setPosition(1000, QTextCursor.KeepAnchor) + nwGUI.docEditor.setTextCursor(cursor) assert nwGUI.docEditor._wrapSelection("=") is True newText = nwGUI.docEditor.getText() @@ -599,8 +751,8 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex # Toggle Format # ============= - theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText[0:2]) - nwGUI.docEditor.replaceText(theText) + text = "### A Scene\n\n%s" % "\n\n".join(ipsumText[0:2]) + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) # No Selection @@ -609,17 +761,17 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex assert nwGUI.docEditor._toggleFormat(2, "=") is False # Wrap Single Equal - nwGUI.docEditor.replaceText(theText) + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) assert nwGUI.docEditor._toggleFormat(1, "=") is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "=consectetur=") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "=consectetur=") # Past Paragraph - nwGUI.docEditor.replaceText(theText) - theCursor = nwGUI.docEditor.textCursor() - theCursor.setPosition(13, QTextCursor.MoveAnchor) - theCursor.setPosition(1000, QTextCursor.KeepAnchor) - nwGUI.docEditor.setTextCursor(theCursor) + nwGUI.docEditor.replaceText(text) + cursor = nwGUI.docEditor.textCursor() + cursor.setPosition(13, QTextCursor.MoveAnchor) + cursor.setPosition(1000, QTextCursor.KeepAnchor) + nwGUI.docEditor.setTextCursor(cursor) assert nwGUI.docEditor._toggleFormat(1, "=") is True newText = nwGUI.docEditor.getText() @@ -628,31 +780,31 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex assert newPara[2] == ipsumText[1] # Wrap Double Equal - nwGUI.docEditor.replaceText(theText) + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) assert nwGUI.docEditor._toggleFormat(2, "=") is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "==consectetur==") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "==consectetur==") # Toggle Double Equal - nwGUI.docEditor.replaceText(theText) + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) assert nwGUI.docEditor._toggleFormat(2, "=") is True assert nwGUI.docEditor._toggleFormat(2, "=") is True - assert nwGUI.docEditor.getText() == theText + assert nwGUI.docEditor.getText() == text # Toggle Triple+Double Equal - nwGUI.docEditor.replaceText(theText) + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) assert nwGUI.docEditor._toggleFormat(3, "=") is True assert nwGUI.docEditor._toggleFormat(2, "=") is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "=consectetur=") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "=consectetur=") # Toggle Unequal - repText = theText.replace("consectetur", "=consectetur==") + repText = text.replace("consectetur", "=consectetur==") nwGUI.docEditor.replaceText(repText) nwGUI.docEditor.setCursorPosition(45) assert nwGUI.docEditor._toggleFormat(1, "=") is True - assert nwGUI.docEditor.getText() == theText.replace("consectetur", "consectetur=") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "consectetur=") assert nwGUI.docEditor._toggleFormat(1, "=") is True assert nwGUI.docEditor.getText() == repText @@ -660,15 +812,15 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex # ============== # No Selection - theText = "### A Scene\n\n%s" % ipsumText[0].replace("consectetur", "=consectetur=") - nwGUI.docEditor.replaceText(theText) + text = "### A Scene\n\n%s" % ipsumText[0].replace("consectetur", "=consectetur=") + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) assert nwGUI.docEditor._replaceQuotes("=", "<", ">") is False # First Paragraph Selected # This should not replace anything in second paragraph - theText = "### A Scene\n\n%s" % "\n\n".join(ipsumText[0:2]).replace("ipsum", "=ipsum=") - nwGUI.docEditor.replaceText(theText) + text = "### A Scene\n\n%s" % "\n\n".join(ipsumText[0:2]).replace("ipsum", "=ipsum=") + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) assert nwGUI.docEditor.docAction(nwDocAction.SEL_PARA) assert nwGUI.docEditor._replaceQuotes("=", "<", ">") is True @@ -679,12 +831,12 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex assert newPara[2] == ipsumText[1].replace("ipsum", "=ipsum=") # Edge of Document - theText = ipsumText[0].replace("Lorem", "=Lorem=") - nwGUI.docEditor.replaceText(theText) + text = ipsumText[0].replace("Lorem", "=Lorem=") + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) assert nwGUI.docEditor.docAction(nwDocAction.SEL_ALL) assert nwGUI.docEditor._replaceQuotes("=", "<", ">") is True - assert nwGUI.docEditor.getText() == theText.replace("=Lorem=", "") + assert nwGUI.docEditor.getText() == text.replace("=Lorem=", "") # Remove Line Breaks # ================== @@ -693,20 +845,20 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex parTwo = ipsumText[1].replace(" ", "\n", 5) # Remove All - theText = "### A Scene\n\n%s\n\n%s" % (parOne, parTwo) - nwGUI.docEditor.replaceText(theText) + text = "### A Scene\n\n%s\n\n%s" % (parOne, parTwo) + nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) nwGUI.docEditor._removeInParLineBreaks() assert nwGUI.docEditor.getText() == "### A Scene\n\n%s\n" % "\n\n".join(ipsumText[0:2]) # Remove First Paragraph # Second paragraphs should remain unchanged - theText = "### A Scene\n\n%s\n\n%s" % (parOne, parTwo) - nwGUI.docEditor.replaceText(theText) - theCursor = nwGUI.docEditor.textCursor() - theCursor.setPosition(16, QTextCursor.MoveAnchor) - theCursor.setPosition(680, QTextCursor.KeepAnchor) - nwGUI.docEditor.setTextCursor(theCursor) + text = "### A Scene\n\n%s\n\n%s" % (parOne, parTwo) + nwGUI.docEditor.replaceText(text) + cursor = nwGUI.docEditor.textCursor() + cursor.setPosition(16, QTextCursor.MoveAnchor) + cursor.setPosition(680, QTextCursor.KeepAnchor) + nwGUI.docEditor.setTextCursor(cursor) nwGUI.docEditor._removeInParLineBreaks() newText = nwGUI.docEditor.getText() @@ -720,6 +872,25 @@ def testGuiEditor_TextManipulation(qtbot, monkeypatch, nwGUI, projPath, ipsumTex assert newPara[6] == twoBits[4] assert newPara[7] == " ".join(twoBits[5:]) + # Key Press Events + # ================ + text = "### A Scene\n\n%s\n\n%s" % (parOne, parTwo) + nwGUI.docEditor.replaceText(text) + assert nwGUI.docEditor.getText() == text + + # Select All + qtbot.keyClick(nwGUI.docEditor, Qt.Key_A, modifier=Qt.ControlModifier, delay=KEY_DELAY) + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Delete, delay=KEY_DELAY) + assert nwGUI.docEditor.getText() == "" + + # Undo + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Z, modifier=Qt.ControlModifier, delay=KEY_DELAY) + assert nwGUI.docEditor.getText() == text + + # Redo + qtbot.keyClick(nwGUI.docEditor, Qt.Key_Y, modifier=Qt.ControlModifier, delay=KEY_DELAY) + assert nwGUI.docEditor.getText() == "" + # qtbot.stop() # END Test testGuiEditor_TextManipulation