diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index a1413aee8..58f718f46 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -460,9 +460,8 @@ def loadText(self, tHandle, tLine=None) -> bool: return True - def updateTagHighLighting(self): - """Rerun the syntax highlighter on all meta data lines. - """ + def updateTagHighLighting(self) -> None: + """Rerun the syntax highlighter on all meta data lines.""" self.highLight.rehighlightByType(GuiDocHighlighter.BLOCK_META) return @@ -672,7 +671,7 @@ def saveCursorPosition(self) -> None: self._nwItem.setCursorPos(cursPos) return - def setCursorLine(self, line: int) -> bool: + def setCursorLine(self, line: int | None) -> bool: """Move the cursor to a given line in the document.""" if not isinstance(line, int): return False diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index d265ade89..7cd673649 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -329,7 +329,7 @@ def highlightBlock(self, text: str) -> None: self.setFormat(0, 4, self._hStyles["header4h"]) self.setFormat(4, len(text), self._hStyles["header4"]) - if text.startswith("#! "): # Title + elif text.startswith("#! "): # Title self.setFormat(0, 2, self._hStyles["header1h"]) self.setFormat(2, len(text), self._hStyles["header1"]) @@ -367,7 +367,7 @@ def highlightBlock(self, text: str) -> None: self.setFormat(tLen-1, tLen, self._hStyles["keyword"]) return - # Regular text + # Regular Text self.setCurrentBlockState(self.BLOCK_TEXT) for rX, xFmt in self.rxRules: rxItt = rX.globalMatch(text, 0) @@ -389,7 +389,7 @@ def highlightBlock(self, text: str) -> None: while rxSpell.hasNext(): rxMatch = rxSpell.next() if not SHARED.spelling.checkWord(rxMatch.captured(0)): - if rxMatch.captured(0).isupper() or rxMatch.captured(0).isnumeric(): + if not rxMatch.captured(0).isalpha() or rxMatch.captured(0).isupper(): continue xPos = rxMatch.capturedStart(0) xLen = rxMatch.capturedLength(0) diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index 266d735aa..4b7b4e7bf 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -25,99 +25,96 @@ import logging -from PyQt5.QtCore import Qt, QSize, pyqtSignal -from PyQt5.QtWidgets import ( - QToolBar, QWidget, QSizePolicy, QAction, QMenu, QToolButton -) +from typing import TYPE_CHECKING + +from PyQt5.QtCore import QEvent, QPoint, Qt, QSize, pyqtSignal +from PyQt5.QtGui import QPalette +from PyQt5.QtWidgets import QMenu, QToolButton, QVBoxLayout, QWidget from novelwriter import CONFIG, SHARED from novelwriter.enum import nwView +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) -class GuiSideBar(QToolBar): +class GuiSideBar(QWidget): viewChangeRequested = pyqtSignal(nwView) - def __init__(self, mainGui): + def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) logger.debug("Create: GuiSideBar") self.mainGui = mainGui - # Style - iPx = CONFIG.pxInt(22) - mPx = CONFIG.pxInt(60) - - lblFont = SHARED.theme.guiFont - lblFont.setPointSizeF(0.65*SHARED.theme.fontPointSize) - - self.setMovable(False) - self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) - self.setIconSize(QSize(iPx, iPx)) - self.setMaximumWidth(mPx) + iPx = CONFIG.pxInt(24) + iconSize = QSize(iPx, iPx) self.setContentsMargins(0, 0, 0, 0) - stretch = QWidget(self) - stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - - # Actions - self.aProject = QAction(self.tr("Project"), self) - self.aProject.setFont(lblFont) - self.aProject.setToolTip(self.tr("Project Tree View")) - self.aProject.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT)) - - self.aNovel = QAction(self.tr("Novel"), self) - self.aNovel.setFont(lblFont) - self.aNovel.setToolTip(self.tr("Novel Tree View")) - self.aNovel.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL)) - - self.aOutline = QAction(self.tr("Outline"), self) - self.aOutline.setFont(lblFont) - self.aOutline.setToolTip(self.tr("Novel Outline View")) - self.aOutline.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE)) - - self.aBuild = QAction(self.tr("Build"), self) - self.aBuild.setFont(lblFont) - self.aBuild.setToolTip(self.tr("Build Manuscript")) - self.aBuild.triggered.connect(lambda: self.mainGui.showBuildManuscriptDialog()) - - self.aDetails = QAction(self.tr("Details"), self) - self.aDetails.setFont(lblFont) - self.aDetails.setToolTip(self.tr("Project Details")) - self.aDetails.triggered.connect(lambda: self.mainGui.showProjectDetailsDialog()) - - self.aStats = QAction(self.tr("Stats"), self) - self.aStats.setFont(lblFont) - self.aStats.setToolTip(self.tr("Writing Statistics")) - self.aStats.triggered.connect(lambda: self.mainGui.showWritingStatsDialog()) + # Buttons + self.tbProject = QToolButton(self) + self.tbProject.setToolTip(self.tr("Project Tree View")) + self.tbProject.setIconSize(iconSize) + self.tbProject.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT)) + + self.tbNovel = QToolButton(self) + self.tbNovel.setToolTip(self.tr("Novel Tree View")) + self.tbNovel.setIconSize(iconSize) + self.tbNovel.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL)) + + self.tbOutline = QToolButton(self) + self.tbOutline.setToolTip(self.tr("Novel Outline View")) + self.tbOutline.setIconSize(iconSize) + self.tbOutline.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE)) + + self.tbBuild = QToolButton(self) + self.tbBuild.setToolTip(self.tr("Build Manuscript")) + self.tbBuild.setIconSize(iconSize) + self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog) + + self.tbDetails = QToolButton(self) + self.tbDetails.setToolTip(self.tr("Project Details")) + self.tbDetails.setIconSize(iconSize) + self.tbDetails.clicked.connect(self.mainGui.showProjectDetailsDialog) + + self.tbStats = QToolButton(self) + self.tbStats.setToolTip(self.tr("Writing Statistics")) + self.tbStats.setIconSize(iconSize) + self.tbStats.clicked.connect(self.mainGui.showWritingStatsDialog) # Settings Menu - self.mSettings = QMenu() + self.tbSettings = QToolButton(self) + self.tbSettings.setToolTip(self.tr("Settings")) + self.tbSettings.setIconSize(iconSize) + self.tbSettings.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly) + self.mSettings = _PopRightMenu(self.tbSettings) self.mSettings.addAction(self.mainGui.mainMenu.aEditWordList) self.mSettings.addAction(self.mainGui.mainMenu.aProjectSettings) self.mSettings.addSeparator() self.mSettings.addAction(self.mainGui.mainMenu.aPreferences) - self.tbSettings = QToolButton(self) - self.tbSettings.setFont(lblFont) - self.tbSettings.setText(self.tr("Settings")) self.tbSettings.setMenu(self.mSettings) - self.tbSettings.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.tbSettings.setPopupMode(QToolButton.InstantPopup) # Assemble - self.addAction(self.aProject) - self.addAction(self.aNovel) - self.addAction(self.aOutline) - self.addAction(self.aBuild) - self.addWidget(stretch) - self.addAction(self.aDetails) - self.addAction(self.aStats) - self.addWidget(self.tbSettings) + self.outerBox = QVBoxLayout() + self.outerBox.addWidget(self.tbProject) + self.outerBox.addWidget(self.tbNovel) + self.outerBox.addWidget(self.tbOutline) + self.outerBox.addWidget(self.tbBuild) + self.outerBox.addStretch(1) + self.outerBox.addWidget(self.tbDetails) + self.outerBox.addWidget(self.tbStats) + self.outerBox.addWidget(self.tbSettings) + self.outerBox.setContentsMargins(0, 0, CONFIG.pxInt(2), 0) + self.outerBox.setSpacing(CONFIG.pxInt(4)) + + self.setLayout(self.outerBox) self.updateTheme() @@ -125,19 +122,54 @@ def __init__(self, mainGui): return - def updateTheme(self): - """Initialise GUI elements that depend on specific settings. - """ - self.setStyleSheet("QToolBar {border: 0px;}") - - self.aProject.setIcon(SHARED.theme.getIcon("view_editor")) - self.aNovel.setIcon(SHARED.theme.getIcon("view_novel")) - self.aOutline.setIcon(SHARED.theme.getIcon("view_outline")) - self.aBuild.setIcon(SHARED.theme.getIcon("view_build")) - self.aDetails.setIcon(SHARED.theme.getIcon("proj_details")) - self.aStats.setIcon(SHARED.theme.getIcon("proj_stats")) + def updateTheme(self) -> None: + """Initialise GUI elements that depend on specific settings.""" + qPalette = self.palette() + qPalette.setBrush(QPalette.Window, qPalette.base()) + self.setPalette(qPalette) + + fadeCol = qPalette.text().color() + buttonStyle = ( + "QToolButton {{padding: {0}px; border: none; background: transparent;}} " + "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}" + ).format(CONFIG.pxInt(4), fadeCol.red(), fadeCol.green(), fadeCol.blue()) + buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}" + + self.tbProject.setIcon(SHARED.theme.getIcon("view_editor")) + self.tbProject.setStyleSheet(buttonStyle) + + self.tbNovel.setIcon(SHARED.theme.getIcon("view_novel")) + self.tbNovel.setStyleSheet(buttonStyle) + + self.tbOutline.setIcon(SHARED.theme.getIcon("view_outline")) + self.tbOutline.setStyleSheet(buttonStyle) + + self.tbBuild.setIcon(SHARED.theme.getIcon("view_build")) + self.tbBuild.setStyleSheet(buttonStyle) + + self.tbDetails.setIcon(SHARED.theme.getIcon("proj_details")) + self.tbDetails.setStyleSheet(buttonStyle) + + self.tbStats.setIcon(SHARED.theme.getIcon("proj_stats")) + self.tbStats.setStyleSheet(buttonStyle) + self.tbSettings.setIcon(SHARED.theme.getIcon("settings")) + self.tbSettings.setStyleSheet(buttonStyleMenu) return # END Class GuiSideBar + + +class _PopRightMenu(QMenu): + + def event(self, event: QEvent): + """Overload the show event and move the menu popup location.""" + if event.type() == QEvent.Show: + parent = self.parent() + if isinstance(parent, QWidget): + offset = QPoint(parent.width(), parent.height() - self.height()) + self.move(parent.mapToGlobal(offset)) + return super(_PopRightMenu, self).event(event) + +# END Class _PopRightMenu diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 968066485..03cc58e86 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -33,8 +33,8 @@ from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot from PyQt5.QtGui import QCloseEvent, QCursor, QIcon, QKeySequence from PyQt5.QtWidgets import ( - qApp, QDialog, QFileDialog, QMainWindow, QMessageBox, QShortcut, QSplitter, - QStackedWidget, QVBoxLayout, QWidget + QDialog, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, QShortcut, + QSplitter, QStackedWidget, QVBoxLayout, QWidget, qApp ) from novelwriter import CONFIG, SHARED, __hexversion__ @@ -95,7 +95,7 @@ def __init__(self) -> None: logger.debug("Create: GUI") self.setObjectName("GuiMain") - self.threadPool = QThreadPool() + self.threadPool = QThreadPool(self) # System Info # =========== @@ -143,17 +143,17 @@ def __init__(self) -> None: self.itemDetails = GuiItemDetails(self) self.outlineView = GuiOutlineView(self) self.mainMenu = GuiMainMenu(self) - self.viewsBar = GuiSideBar(self) + self.sideBar = GuiSideBar(self) # Project Tree Stack - self.projStack = QStackedWidget() + self.projStack = QStackedWidget(self) self.projStack.addWidget(self.projView) self.projStack.addWidget(self.novelView) self.projStack.currentChanged.connect(self._projStackChanged) # Project Tree View - self.treePane = QWidget() - self.treeBox = QVBoxLayout() + self.treePane = QWidget(self) + self.treeBox = QVBoxLayout(self) self.treeBox.setContentsMargins(0, 0, 0, 0) self.treeBox.setSpacing(mPx) self.treeBox.addWidget(self.projStack) @@ -161,7 +161,7 @@ def __init__(self) -> None: self.treePane.setLayout(self.treeBox) # Splitter : Document Viewer / Document Meta - self.splitView = QSplitter(Qt.Vertical) + self.splitView = QSplitter(Qt.Vertical, self) self.splitView.addWidget(self.docViewer) self.splitView.addWidget(self.viewMeta) self.splitView.setHandleWidth(hWd) @@ -169,7 +169,7 @@ def __init__(self) -> None: self.splitView.setSizes(CONFIG.viewPanePos) # Splitter : Document Editor / Document Viewer - self.splitDocs = QSplitter(Qt.Horizontal) + self.splitDocs = QSplitter(Qt.Horizontal, self) self.splitDocs.addWidget(self.docEditor) self.splitDocs.addWidget(self.splitView) self.splitDocs.setOpaqueResize(False) @@ -185,7 +185,7 @@ def __init__(self) -> None: self.splitMain.setSizes(CONFIG.mainPanePos) # Main Stack : Editor / Outline - self.mainStack = QStackedWidget() + self.mainStack = QStackedWidget(self) self.mainStack.addWidget(self.splitMain) self.mainStack.addWidget(self.outlineView) self.mainStack.currentChanged.connect(self._mainStackChanged) @@ -222,11 +222,19 @@ def __init__(self) -> None: # Initialise the Project Tree self.rebuildTrees() - # Set Main Window Elements + # Assemble Main Window Elements + self.mainBox = QHBoxLayout(self) + self.mainBox.addWidget(self.sideBar) + self.mainBox.addWidget(self.mainStack) + self.mainBox.setContentsMargins(0, 0, 0, 0) + self.mainBox.setSpacing(0) + + self.mainWidget = QWidget(self) + self.mainWidget.setLayout(self.mainBox) + self.setMenuBar(self.mainMenu) - self.setCentralWidget(self.mainStack) + self.setCentralWidget(self.mainWidget) self.setStatusBar(self.mainStatus) - self.addToolBar(Qt.LeftToolBarArea, self.viewsBar) self.setContextMenuPolicy(Qt.NoContextMenu) # Issue #1147 # Connect Signals @@ -236,7 +244,7 @@ def __init__(self) -> None: SHARED.projectStatusMessage.connect(self.mainStatus.setStatusMessage) SHARED.spellLanguageChanged.connect(self.mainStatus.setLanguage) - self.viewsBar.viewChangeRequested.connect(self._changeView) + self.sideBar.viewChangeRequested.connect(self._changeView) self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox) self.projView.openDocumentRequest.connect(self._openDocument) @@ -269,15 +277,15 @@ def __init__(self) -> None: # ======================= # Set Up Auto-Save Project Timer - self.asProjTimer = QTimer() + self.asProjTimer = QTimer(self) self.asProjTimer.timeout.connect(self._autoSaveProject) # Set Up Auto-Save Document Timer - self.asDocTimer = QTimer() + self.asDocTimer = QTimer(self) self.asDocTimer.timeout.connect(self._autoSaveDocument) # Main Clock - self.mainTimer = QTimer() + self.mainTimer = QTimer(self) self.mainTimer.setInterval(1000) self.mainTimer.timeout.connect(self._timeTick) self.mainTimer.start() @@ -872,7 +880,7 @@ def showPreferencesDialog(self) -> None: SHARED.theme.loadTheme() self.docEditor.updateTheme() self.docViewer.updateTheme() - self.viewsBar.updateTheme() + self.sideBar.updateTheme() self.projView.updateTheme() self.novelView.updateTheme() self.outlineView.updateTheme() @@ -1022,7 +1030,7 @@ def showAboutNWDialog(self, showNotes: bool = False) -> bool: def showAboutQtDialog(self) -> None: """Show the about dialog for Qt.""" - msgBox = QMessageBox() + msgBox = QMessageBox(self) msgBox.aboutQt(self, "About Qt") return @@ -1146,7 +1154,7 @@ def toggleFocusMode(self) -> bool: self.treePane.setVisible(isVisible) self.mainStatus.setVisible(isVisible) self.mainMenu.setVisible(isVisible) - self.viewsBar.setVisible(isVisible) + self.sideBar.setVisible(isVisible) hideDocFooter = self.isFocusMode and CONFIG.hideFocusFooter self.docEditor.docFooter.setVisible(not hideDocFooter) diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 0716214e5..0a66ef135 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -72,8 +72,7 @@ def testGuiMain_ProjectBlocker(nwGUI): @pytest.mark.gui def testGuiMain_Launch(qtbot, monkeypatch, nwGUI, prjLipsum): - """Test the handling of launch tasks. - """ + """Test the handling of launch tasks.""" monkeypatch.setattr(GuiProjectLoad, "exec_", lambda *a: None) monkeypatch.setattr(GuiProjectLoad, "result", lambda *a: QDialog.Accepted) CONFIG.lastNotes = "0x0" @@ -140,8 +139,7 @@ def testGuiMain_NewProject(monkeypatch, nwGUI, projPath): @pytest.mark.gui def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): - """Test handling of project tree items based on GUI focus states. - """ + """Test handling of project tree items based on GUI focus states.""" buildTestProject(nwGUI, projPath) sHandle = "000000000000f" @@ -190,8 +188,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): @pytest.mark.gui def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): - """Test the document editor. - """ + """Test the document editor.""" monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True) monkeypatch.setattr(QInputDialog, "getText", lambda *a, text: (text, True)) @@ -555,9 +552,8 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): @pytest.mark.gui -def testGuiMain_FocusFullMode(qtbot, nwGUI, projPath, mockRnd): - """Test toggling focus mode in main window. - """ +def testGuiMain_Features(qtbot, nwGUI, projPath, mockRnd): + """Test various features of the main window.""" buildTestProject(nwGUI, projPath) assert nwGUI.isFocusMode is False @@ -576,7 +572,7 @@ def testGuiMain_FocusFullMode(qtbot, nwGUI, projPath, mockRnd): assert nwGUI.treePane.isVisible() is False assert nwGUI.mainStatus.isVisible() is False assert nwGUI.mainMenu.isVisible() is False - assert nwGUI.viewsBar.isVisible() is False + assert nwGUI.sideBar.isVisible() is False assert nwGUI.splitView.isVisible() is False # Disable focus mode @@ -584,7 +580,7 @@ def testGuiMain_FocusFullMode(qtbot, nwGUI, projPath, mockRnd): assert nwGUI.treePane.isVisible() is True assert nwGUI.mainStatus.isVisible() is True assert nwGUI.mainMenu.isVisible() is True - assert nwGUI.viewsBar.isVisible() is True + assert nwGUI.sideBar.isVisible() is True assert nwGUI.splitView.isVisible() is True # Full Screen Mode @@ -596,6 +592,13 @@ def testGuiMain_FocusFullMode(qtbot, nwGUI, projPath, mockRnd): nwGUI.toggleFullScreenMode() assert nwGUI.windowState() & Qt.WindowFullScreen != Qt.WindowFullScreen + # SideBar Menu + # ============ + + # Just make sure the custom event handler executes and doesn't fail + nwGUI.sideBar.mSettings.show() + nwGUI.sideBar.mSettings.hide() + # qtbot.stop() -# END Test testGuiMain_FocusFullMode +# END Test testGuiMain_Features