diff --git a/docs/source/usage_shortcuts.rst b/docs/source/usage_shortcuts.rst index 86e60afa..4e931351 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/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index d0b69572..94b4c633 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -46,9 +46,17 @@ 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-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/nw_tb-bold.svg b/novelwriter/assets/icons/typicons_dark/nw_tb-bold.svg new file mode 100644 index 00000000..2eaeb846 --- /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 00000000..0ec5fb2a --- /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 00000000..02768596 --- /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 00000000..4ea59ec3 --- /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 00000000..639dd694 --- /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 00000000..e0ae3587 --- /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 00000000..1d60aec2 --- /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 00000000..3a249f4d --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/nw_tb-underline.svg @@ -0,0 +1,5 @@ + + + + + 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 00000000..19ec3bde --- /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 4bf1c92f..00000000 --- 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 22fd9456..62314511 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -46,9 +46,17 @@ 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-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/nw_tb-bold.svg b/novelwriter/assets/icons/typicons_light/nw_tb-bold.svg new file mode 100644 index 00000000..3fbfbc88 --- /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 00000000..893d8930 --- /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 00000000..24b3809d --- /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 00000000..fc19a690 --- /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 00000000..89f4b7ee --- /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-subscript.svg b/novelwriter/assets/icons/typicons_light/nw_tb-subscript.svg new file mode 100644 index 00000000..2384f6ea --- /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 00000000..df0aa35c --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_tb-superscript.svg @@ -0,0 +1,5 @@ + + + + + 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 00000000..ca122269 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/nw_tb-underline.svg @@ -0,0 +1,5 @@ + + + + + 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 00000000..3b5d6383 --- /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 c23c08ca..00000000 --- a/novelwriter/assets/icons/typicons_light/typ_th-menu.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/novelwriter/config.py b/novelwriter/config.py index 50c8c1ab..ebd0d05f 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -178,9 +178,11 @@ class Config: 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 @@ class Config: # 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 @@ class Config: 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/core/options.py b/novelwriter/core/options.py index 0dd62f06..48a0c594 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -28,7 +28,7 @@ import json 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 @@ if TYPE_CHECKING: # pragma: no cover logger = logging.getLogger(__name__) +NWEnum = TypeVar("NWEnum", bound=Enum) + VALID_MAP = { "GuiWritingStats": { "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2", @@ -201,11 +203,11 @@ class OptionState: 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/doceditor.py b/novelwriter/gui/doceditor.py index e9094c8b..a3bc291c 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -46,7 +46,7 @@ from PyQt5.QtGui import ( ) 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,12 @@ class GuiDocEditor(QPlainTextEdit): self.docHeader = GuiDocEditHeader(self) self.docFooter = GuiDocEditFooter(self) self.docSearch = GuiDocEditSearch(self) + self.docToolBar = GuiDocToolBar(self) + + # 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) @@ -248,6 +256,7 @@ class GuiDocEditor(QPlainTextEdit): self.docSearch.updateTheme() self.docHeader.updateTheme() self.docFooter.updateTheme() + self.docToolBar.updateTheme() return def updateSyntaxColours(self) -> None: @@ -400,6 +409,7 @@ class GuiDocEditor(QPlainTextEdit): qApp.processEvents() self.setDocumentChanged(False) self._qDocument.clearUndoRedoStacks() + self.docToolBar.setVisible(CONFIG.showEditToolBar) qApp.restoreOverrideCursor() @@ -514,6 +524,7 @@ class GuiDocEditor(QPlainTextEdit): 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(): @@ -841,30 +852,11 @@ class GuiDocEditor(QPlainTextEdit): 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() - 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 ## @@ -959,6 +951,27 @@ class GuiDocEditor(QPlainTextEdit): 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 ## @@ -1179,6 +1192,21 @@ class GuiDocEditor(QPlainTextEdit): return + @pyqtSlot() + def _closeCurrentDocument(self) -> None: + """Close the document. Forwarded to the main Gui.""" + self.closeDocumentRequest.emit() + self.docToolBar.setVisible(False) + 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 ## @@ -2072,6 +2100,142 @@ 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): + + requestDocAction = pyqtSignal(nwDocAction) + + 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.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 + # ======== + + self.outerBox = QVBoxLayout() + 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) + + 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) + + 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 + + ## + # Private 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 + + # =============================================================================================== # # The Embedded Document Search/Replace Feature # Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport @@ -2487,6 +2651,9 @@ class GuiDocEditSearch(QFrame): class GuiDocEditHeader(QWidget): + closeDocumentRequest = pyqtSignal() + toggleToolBarRequest = pyqtSignal() + def __init__(self, docEditor: GuiDocEditor) -> None: super().__init__(parent=docEditor) @@ -2499,6 +2666,7 @@ class GuiDocEditHeader(QWidget): fPx = int(0.9*SHARED.theme.fontPixelSize) hSp = CONFIG.pxInt(6) + iconSize = QSize(fPx, fPx) # Main Widget Settings self.setAutoFillBackground(True) @@ -2518,46 +2686,46 @@ class GuiDocEditHeader(QWidget): 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 +2751,7 @@ class GuiDocEditHeader(QWidget): 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 +2761,7 @@ class GuiDocEditHeader(QWidget): "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 +2791,7 @@ class GuiDocEditHeader(QWidget): 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 +2813,7 @@ class GuiDocEditHeader(QWidget): 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 +2835,16 @@ class GuiDocEditHeader(QWidget): # 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 18dfdbc5..ff548fb6 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -34,8 +34,8 @@ from typing import TYPE_CHECKING 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, @@ -705,7 +705,7 @@ class GuiDocViewHeader(QWidget): 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 @@ class GuiDocViewHeader(QWidget): 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 @@ class GuiDocViewHeader(QWidget): 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 @@ class GuiDocViewHeader(QWidget): 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 @@ -1020,24 +1020,12 @@ class GuiDocViewFooter(QWidget): # 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/mainmenu.py b/novelwriter/gui/mainmenu.py index fa8b64ad..e6453a82 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -30,7 +30,7 @@ from pathlib import Path 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 @@ class GuiMainMenu(QMenuBar): 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 @@ -209,7 +209,7 @@ class GuiMainMenu(QMenuBar): # 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() @@ -246,12 +246,12 @@ class GuiMainMenu(QMenuBar): # 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 @@ class GuiMainMenu(QMenuBar): # 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 @@ class GuiMainMenu(QMenuBar): # 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 @@ -330,7 +330,7 @@ class GuiMainMenu(QMenuBar): # 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")) @@ -350,22 +350,30 @@ class GuiMainMenu(QMenuBar): # 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 @@ class GuiMainMenu(QMenuBar): # 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 @@ class GuiMainMenu(QMenuBar): # 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 @@ class GuiMainMenu(QMenuBar): # 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 @@ class GuiMainMenu(QMenuBar): # 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 @@ class GuiMainMenu(QMenuBar): 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 @@ class GuiMainMenu(QMenuBar): # 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 @@ class GuiMainMenu(QMenuBar): # 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 @@ class GuiMainMenu(QMenuBar): # 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 @@ class GuiMainMenu(QMenuBar): 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 @@ class GuiMainMenu(QMenuBar): # 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 @@ class GuiMainMenu(QMenuBar): # 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 @@ class GuiMainMenu(QMenuBar): # 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 @@ class GuiMainMenu(QMenuBar): # 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/gui/noveltree.py b/novelwriter/gui/noveltree.py index 0742ab54..40aae9ef 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -29,9 +29,10 @@ import logging 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 import CONFIG, SHARED 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 @@ class GuiNovelView(QWidget): # 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 @@ class GuiNovelView(QWidget): return - def closeProjectTasks(self): + def closeProjectTasks(self) -> None: """Run closing project tasks.""" lastColType = self.novelTree.lastColType lastColSize = self.novelTree.lastColSize @@ -150,15 +151,13 @@ class GuiNovelView(QWidget): 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 @@ class GuiNovelView(QWidget): ## @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 @@ class GuiNovelView(QWidget): 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 @@ class GuiNovelToolBar(QWidget): # 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 @@ class GuiNovelToolBar(QWidget): "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 @@ class GuiNovelToolBar(QWidget): 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 @@ class GuiNovelToolBar(QWidget): ## @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 @@ class GuiNovelToolBar(QWidget): # 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 @@ class GuiNovelTree(QTreeWidget): 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,11 +465,10 @@ class GuiNovelTree(QTreeWidget): 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) + self._pMore = SHARED.theme.loadDecoration("deco_doc_more", h=iPx) return ## @@ -489,28 +476,26 @@ class GuiNovelTree(QTreeWidget): ## @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 @@ class GuiNovelTree(QTreeWidget): 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 @@ class GuiNovelTree(QTreeWidget): 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 @@ class GuiNovelTree(QTreeWidget): 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 @@ class GuiNovelTree(QTreeWidget): 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 @@ class GuiNovelTree(QTreeWidget): # 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 @@ class GuiNovelTree(QTreeWidget): 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 @@ class GuiNovelTree(QTreeWidget): ## @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 @@ class GuiNovelTree(QTreeWidget): 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 @@ class GuiNovelTree(QTreeWidget): # 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 @@ class GuiNovelTree(QTreeWidget): 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 @@ class GuiNovelTree(QTreeWidget): 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 @@ class GuiNovelTree(QTreeWidget): 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 @@ class GuiNovelTree(QTreeWidget): 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 84234ccf..596a422b 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -261,6 +261,7 @@ class GuiOutlineToolBar(QToolBar): 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 787f5671..d1c2f903 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -361,12 +361,13 @@ class GuiProjectToolBar(QWidget): "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")) diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index 4b7b4e7b..be719a42 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -115,7 +115,6 @@ class GuiSideBar(QWidget): self.outerBox.setSpacing(CONFIG.pxInt(4)) self.setLayout(self.outerBox) - self.updateTheme() logger.debug("Ready: GuiSideBar") diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index da8f1be4..6c4d7e76 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -36,7 +36,7 @@ from PyQt5.QtGui import ( ) 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 @@ logger = logging.getLogger(__name__) class GuiTheme: - def __init__(self): + def __init__(self) -> None: self.iconCache = GuiIcons(self) @@ -130,6 +130,7 @@ class GuiTheme: 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 @@ -182,7 +183,7 @@ class GuiTheme: # Theme Methods ## - def loadTheme(self): + def loadTheme(self) -> bool: """Load the currently specified GUI theme.""" guiTheme = CONFIG.guiTheme if guiTheme not in self._availThemes: @@ -269,7 +270,7 @@ class GuiTheme: 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: @@ -363,7 +364,7 @@ class GuiTheme: # Internal Functions ## - def _setGuiFont(self): + def _setGuiFont(self) -> None: """Update the GUI's font style from settings.""" theFont = QFont() fontDB = QFontDatabase() @@ -395,9 +396,7 @@ class GuiTheme: 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(",") @@ -414,9 +413,8 @@ class GuiTheme: 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)) @@ -447,14 +445,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,21 +475,27 @@ 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", } - 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 @@ -501,7 +513,7 @@ class GuiIcons: # 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. @@ -575,52 +587,60 @@ class GuiIcons: # 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): - """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. - """ - if iconKey in self._qIcons: - return self._qIcons[iconKey] + def getIcon(self, name: str) -> QIcon: + """Return an icon from the icon buffer, or load it.""" + 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 getPixmap(self, iconKey, iconSize): + def getToggleIcon(self, name: str, size: tuple[int, int]) -> QIcon: + """Return a toggle icon from the icon buffer. or load it.""" + 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, 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 """ @@ -647,17 +667,16 @@ class GuiIcons: 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)] @@ -665,27 +684,27 @@ class GuiIcons: # 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() @@ -696,9 +715,8 @@ class GuiIcons: # 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/novelwriter/guimain.py b/novelwriter/guimain.py index 4d324677..41301476 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -62,7 +62,7 @@ from novelwriter.tools.writingstats import GuiWritingStats 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 @@ class GuiMain(QMainWindow): 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) @@ -267,6 +272,8 @@ class GuiMain(QMainWindow): 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) @@ -733,20 +740,6 @@ class GuiMain(QMainWindow): 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 ## @@ -1119,12 +1112,6 @@ class GuiMain(QMainWindow): 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 +1126,44 @@ class GuiMain(QMainWindow): 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 +1187,165 @@ class GuiMain(QMainWindow): 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(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.""" + 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 +1500,4 @@ class GuiMain(QMainWindow): 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/reference/baseConfig_novelwriter.conf b/tests/reference/baseConfig_novelwriter.conf index f3434415..95df9072 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 dfa3f3fd..6ea3a0d5 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 747a8f73..01345630 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 diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 9a28e5c2..064fcf17 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -24,15 +24,15 @@ import pytest 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() == theText + 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() == 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 diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 74e0e352..52e2bc0f 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 diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index f476ca94..cfb16ab1 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