diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 6f6fc63e..2eb80148 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -254,11 +254,13 @@ def main(sysArgs: list | None = None) -> GuiMain | None: pass # Quietly ignore error # Import GUI (after dependency checks), and launch + from novelwriter.gui.theme import GuiTheme from novelwriter.guimain import GuiMain if testMode: # Only used for testing where the test framework creates the app CONFIG.loadConfig() + SHARED.initTheme(GuiTheme()) return GuiMain() app = QApplication([CONFIG.appName, (f"-style={qtStyle}")]) @@ -274,6 +276,7 @@ def main(sysArgs: list | None = None) -> GuiMain | None: # Run Config steps that require the QApplication CONFIG.loadConfig() CONFIG.initLocalisation(app) + SHARED.initTheme(GuiTheme()) # Launch main GUI nwGUI = GuiMain() diff --git a/novelwriter/extensions/switch.py b/novelwriter/extensions/switch.py index 9ee155a7..2b320506 100644 --- a/novelwriter/extensions/switch.py +++ b/novelwriter/extensions/switch.py @@ -98,14 +98,14 @@ class NSwitch(QAbstractButton): trackBrush = palette.highlight() thumbBrush = palette.highlightedText() else: - trackBrush = palette.dark() + trackBrush = palette.mid() thumbBrush = palette.light() if self.isEnabled(): trackOpacity = 1.0 else: trackOpacity = 0.6 - trackBrush = palette.shadow() + trackBrush = palette.dark() thumbBrush = palette.mid() painter.setBrush(trackBrush) @@ -114,6 +114,7 @@ class NSwitch(QAbstractButton): painter.setBrush(thumbBrush) painter.drawEllipse(self._offset - self._rR, self._rB, self._rH, self._rH) + painter.end() return diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 9093bb8f..98efd50b 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -304,6 +304,20 @@ class GuiTheme: backCol = self._guiPalette.window().color() textCol = self._guiPalette.windowText().color() + # Calculate Based on Qt Fusion + light = backCol.lighter(150) + mid = backCol.darker(130) + midLight = mid.lighter(110) + dark = backCol.darker(150) + shadow = dark.darker(135) + + self._guiPalette.setColor(QPalette.ColorRole.Light, light) + self._guiPalette.setColor(QPalette.ColorRole.Mid, mid) + self._guiPalette.setColor(QPalette.ColorRole.Midlight, midLight) + self._guiPalette.setColor(QPalette.ColorRole.Dark, dark) + self._guiPalette.setColor(QPalette.ColorRole.Shadow, shadow) + + # Calculate Help Text backLNess = backCol.lightnessF() textLNess = textCol.lightnessF() self.isLightTheme = backLNess > textLNess diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 9e10e3f3..56e17517 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -56,7 +56,6 @@ from novelwriter.gui.projtree import GuiProjectView from novelwriter.gui.search import GuiProjectSearch from novelwriter.gui.sidebar import GuiSideBar from novelwriter.gui.statusbar import GuiMainStatus -from novelwriter.gui.theme import GuiTheme from novelwriter.tools.dictionaries import GuiDictionaries from novelwriter.tools.manuscript import GuiManuscript from novelwriter.tools.noveldetails import GuiNovelDetails @@ -101,7 +100,7 @@ class GuiMain(QMainWindow): # ============ # Initialise UserData Instance - SHARED.initSharedData(self, GuiTheme()) + SHARED.initSharedData(self) # Prepare Main Window self.resize(*CONFIG.mainWinSize) @@ -1054,6 +1053,7 @@ class GuiMain(QMainWindow): # We are doing this manually instead of connecting to # paletteChanged since the processing order matters SHARED.theme.loadTheme() + self.setPalette(QApplication.palette()) self.docEditor.updateTheme() self.docViewer.updateTheme() self.docViewerPanel.updateTheme() diff --git a/novelwriter/shared.py b/novelwriter/shared.py index 2fd5359c..588252d9 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -164,17 +164,24 @@ class SharedData(QObject): # Methods ## - def initSharedData(self, gui: GuiMain, theme: GuiTheme) -> None: + def initTheme(self, theme: GuiTheme) -> None: + """Initialise the GUI theme. This must be called before the GUI + is created. + """ + self._theme = theme + return + + def initSharedData(self, gui: GuiMain) -> None: """Initialise the SharedData instance. This must be called as soon as the Main GUI is created to ensure the SHARED singleton has the properties needed for operation. """ self._clock.start() self._gui = gui - self._theme = theme self._resetProject() logger.debug("Ready: SharedData") - logger.debug("Thread Pool Max Count: %d", QThreadPool.globalInstance().maxThreadCount()) + if pool := QThreadPool.globalInstance(): + logger.debug("Thread Pool Max Count: %d", pool.maxThreadCount()) return def closeDocument(self, tHandle: str | None = None) -> None: @@ -266,7 +273,8 @@ class SharedData(QObject): def runInThreadPool(self, runnable: QRunnable, priority: int = 0) -> None: """Queue a runnable in the application thread pool.""" - QThreadPool.globalInstance().start(runnable, priority=priority) + if pool := QThreadPool.globalInstance(): + pool.start(runnable, priority=priority) return def getProjectPath( @@ -278,13 +286,13 @@ class SharedData(QObject): label = (self.tr("novelWriter Project File or Zip File") if allowZip else self.tr("novelWriter Project File")) ext = f"{nwFiles.PROJ_FILE} *.zip" if allowZip else nwFiles.PROJ_FILE - ffilter = formatFileFilter([(label, ext), "*"]) + fFilter = formatFileFilter([(label, ext), "*"]) selected, _ = QFileDialog.getOpenFileName( - parent, self.tr("Open Project"), str(path or ""), filter=ffilter + parent, self.tr("Open Project"), str(path or ""), filter=fFilter ) return Path(selected) if selected else None - def getFont(self, current: QFont, native: bool) -> tuple[QFont, bool]: + def getFont(self, current: QFont, native: bool) -> tuple[QFont, bool | None]: """Open the font dialog and select a font.""" kwargs = {} if not native: diff --git a/tests/conftest.py b/tests/conftest.py index 5ffebaa7..0c7541d2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,7 +33,7 @@ from PyQt6.QtWidgets import QMessageBox sys.path.insert(1, str(Path(__file__).parent.parent.absolute())) -from novelwriter import CONFIG, SHARED, main # noqa: E402 +from novelwriter import CONFIG, SHARED # noqa: E402 from tests.mocked import MockGuiMain, MockTheme # noqa: E402 from tests.tools import cleanProject # noqa: E402 @@ -160,11 +160,15 @@ def mockGUI(qtbot, monkeypatch): @pytest.fixture(scope="function") def nwGUI(qtbot, monkeypatch, functionFixture): """Create an instance of the novelWriter GUI.""" + from novelwriter.gui.theme import GuiTheme + from novelwriter.guimain import GuiMain + monkeypatch.setattr(QMessageBox, "exec", lambda *a: None) monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes) - nwGUI = main(["--testmode", f"--config={_TMP_CONF}", f"--data={_TMP_CONF}"]) - assert nwGUI is not None + CONFIG.loadConfig() + SHARED.initTheme(GuiTheme()) + nwGUI = GuiMain() qtbot.addWidget(nwGUI) resetConfigVars() nwGUI.docEditor.initEditor() diff --git a/tests/test_base/test_base_shared.py b/tests/test_base/test_base_shared.py index 85a8200a..4f147b2b 100644 --- a/tests/test_base/test_base_shared.py +++ b/tests/test_base/test_base_shared.py @@ -56,7 +56,8 @@ def testBaseSharedData_Init(): assert mockGui is not mockTheme # Properly initialise the class - shared.initSharedData(mockGui, mockTheme) # type: ignore + shared.initTheme(mockTheme) # type: ignore + shared.initSharedData(mockGui) # type: ignore assert shared.mainGui is mockGui assert shared.theme is mockTheme @@ -94,7 +95,8 @@ def testBaseSharedData_Projects(monkeypatch, caplog, fncPath): # Initialise the instance, should create an empty project mockGui = MockGuiMain() mockTheme = MockTheme() - shared.initSharedData(mockGui, mockTheme) # type: ignore + shared.initTheme(mockTheme) # type: ignore + shared.initSharedData(mockGui) # type: ignore assert isinstance(shared.project, NWProject) assert shared.hasProject is False @@ -150,7 +152,8 @@ def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog): mockGui = MockGuiMain() mockTheme = MockTheme() - shared.initSharedData(mockGui, mockTheme) # type: ignore + shared.initTheme(mockTheme) # type: ignore + shared.initSharedData(mockGui) # type: ignore assert shared.lastAlert == ""