diff --git a/CHANGELOG.md b/CHANGELOG.md index 97e73da0..2b8a9a03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # novelWriter Changelog +## Version 2.4.3 [2024-05-20] + +### Release Notes + +This is a patch release that fixes issues with the document font in the editor, viewer and +manuscript preview on some Linux distros, and also fixes a potential crash on Windows when using +the spell check dictionary install tool. + +### Detailed Changelog + +**Bugfixes** + +* Fix a crash in the dictionaries install tool on Windows if the config folder reported by the + third party Enchant spell checker tool didn't already exist prior to adding new dictionaries. + The folder is now created when the tool is opened if it doesn't exist. Issue #1874. PR #1876. +* Fix issues setting a different text font for the editor and viewer, and related issues with the + preview in the Manuscript Build tool, on certain platforms. Changing the font and setting + non-standard font sizes produced unexpected results when reloading. The issue seems to be related + to Qt 5.15.3, but that is not fully confirmed. However, the only place so far where the issue is + observed is on Mint 21.3. Issues #1862 and #1875. PR #1877. + +---- + ## Version 2.4.2 [2024-05-18] ### Release Notes diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 29d8ad1a..55e6c76a 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -42,8 +42,8 @@ from PyQt5.QtCore import ( pyqtSlot ) from PyQt5.QtGui import ( - QColor, QCursor, QFont, QKeyEvent, QKeySequence, QMouseEvent, QPalette, - QPixmap, QResizeEvent, QTextBlock, QTextCursor, QTextDocument, QTextOption + QColor, QCursor, QKeyEvent, QKeySequence, QMouseEvent, QPalette, QPixmap, + QResizeEvent, QTextBlock, QTextCursor, QTextDocument, QTextOption ) from PyQt5.QtWidgets import ( QAction, QApplication, QFrame, QGridLayout, QHBoxLayout, QLabel, QLineEdit, @@ -322,10 +322,7 @@ class GuiDocEditor(QPlainTextEdit): SHARED.updateSpellCheckLanguage() # Set font - font = QFont() - font.setFamily(CONFIG.textFont) - font.setPointSize(CONFIG.textSize) - self._qDocument.setDefaultFont(font) + self.initFont() # Update highlighter settings self._qDocument.syntaxHighlighter.initHighlighter() @@ -375,6 +372,23 @@ class GuiDocEditor(QPlainTextEdit): return + def initFont(self) -> None: + """Set the font of the main widget and sub-widgets. This needs + special attention since there appears to be a bug in Qt 5.15.3. + See issues #1862 and #1875. + """ + font = self.font() + font.setFamily(CONFIG.textFont) + font.setPointSize(CONFIG.textSize) + self.setFont(font) + + # Reset sub-widget font to GUI font + self.docHeader.updateFont() + self.docFooter.updateFont() + self.docSearch.updateFont() + + return + def loadText(self, tHandle: str, tLine: int | None = None) -> bool: """Load text from a document into the editor. If we have an I/O error, we must handle this and clear the editor so that we don't @@ -2402,9 +2416,6 @@ class GuiDocEditSearch(QFrame): iSz = SHARED.theme.baseIconSize mPx = CONFIG.pxInt(6) - self.boxFont = SHARED.theme.guiFont - self.boxFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) - self.setContentsMargins(0, 0, 0, 0) self.setAutoFillBackground(True) self.setFrameStyle(QFrame.Shape.StyledPanel | QFrame.Shadow.Plain) @@ -2416,12 +2427,10 @@ class GuiDocEditSearch(QFrame): # ========== self.searchBox = QLineEdit(self) - self.searchBox.setFont(self.boxFont) self.searchBox.setPlaceholderText(self.tr("Search for")) self.searchBox.returnPressed.connect(self._doSearch) self.replaceBox = QLineEdit(self) - self.replaceBox.setFont(self.boxFont) self.replaceBox.setPlaceholderText(self.tr("Replace with")) self.replaceBox.returnPressed.connect(self._doReplace) @@ -2431,12 +2440,9 @@ class GuiDocEditSearch(QFrame): self.searchOpt.setContentsMargins(0, 0, 0, 0) self.searchLabel = QLabel(self.tr("Search"), self) - self.searchLabel.setFont(self.boxFont) self.searchLabel.setIndent(CONFIG.pxInt(6)) self.resultLabel = QLabel("?/?", self) - self.resultLabel.setFont(self.boxFont) - self.resultLabel.setMinimumWidth(SHARED.theme.getTextWidth("?/?", self.boxFont)) self.toggleCase = QAction(self.tr("Case Sensitive"), self) self.toggleCase.setCheckable(True) @@ -2521,6 +2527,7 @@ class GuiDocEditSearch(QFrame): self.replaceButton.setVisible(False) self.adjustSize() + self.updateFont() self.updateTheme() logger.debug("Ready: GuiDocEditSearch") @@ -2590,7 +2597,9 @@ class GuiDocEditSearch(QFrame): numCount = f"{lim:n}+" if (resCount or 0) > lim else f"{resCount:n}" sCurrRes = "?" if currRes is None else str(currRes) sResCount = "?" if resCount is None else numCount - minWidth = SHARED.theme.getTextWidth(f"{sResCount}//{sResCount}", self.boxFont) + minWidth = SHARED.theme.getTextWidth( + f"{sResCount}//{sResCount}", SHARED.theme.guiFontSmall + ) self.resultLabel.setText(f"{sCurrRes}/{sResCount}") self.resultLabel.setMinimumWidth(minWidth) self.adjustSize() @@ -2601,6 +2610,18 @@ class GuiDocEditSearch(QFrame): # Methods ## + def updateFont(self) -> None: + """Update the font settings.""" + self.setFont(SHARED.theme.guiFont) + self.searchBox.setFont(SHARED.theme.guiFontSmall) + self.replaceBox.setFont(SHARED.theme.guiFontSmall) + self.searchLabel.setFont(SHARED.theme.guiFontSmall) + self.resultLabel.setFont(SHARED.theme.guiFontSmall) + self.resultLabel.setMinimumWidth( + SHARED.theme.getTextWidth("?/?", SHARED.theme.guiFontSmall) + ) + return + def updateTheme(self) -> None: """Update theme elements.""" qPalette = QApplication.palette() @@ -2783,10 +2804,6 @@ class GuiDocEditHeader(QWidget): self.itemTitle.setAlignment(QtAlignCenterTop) self.itemTitle.setFixedHeight(iPx) - lblFont = self.itemTitle.font() - lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) - self.itemTitle.setFont(lblFont) - # Other Widgets self.outlineMenu = QMenu(self) @@ -2840,6 +2857,7 @@ class GuiDocEditHeader(QWidget): self.setContentsMargins(0, 0, 0, 0) self.setMinimumHeight(iPx + 2*mPx) + self.updateFont() self.updateTheme() logger.debug("Ready: GuiDocEditHeader") @@ -2878,6 +2896,12 @@ class GuiDocEditHeader(QWidget): logger.debug("Document outline updated in %.3f ms", 1000*(time() - tStart)) return + def updateFont(self) -> None: + """Update the font settings.""" + self.setFont(SHARED.theme.guiFont) + self.itemTitle.setFont(SHARED.theme.guiFontSmall) + return + def updateTheme(self) -> None: """Update theme elements.""" self.tbButton.setThemeIcon("menu") @@ -2989,9 +3013,6 @@ class GuiDocEditFooter(QWidget): bSp = CONFIG.pxInt(4) hSp = CONFIG.pxInt(6) - lblFont = self.font() - lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) - # Cached Translations self._trLineCount = self.tr("Line: {0} ({1})") self._trWordCount = self.tr("Words: {0} ({1})") @@ -3014,7 +3035,6 @@ class GuiDocEditFooter(QWidget): self.statusText.setAutoFillBackground(True) self.statusText.setFixedHeight(fPx) self.statusText.setAlignment(QtAlignLeftTop) - self.statusText.setFont(lblFont) # Lines self.linesIcon = QLabel("", self) @@ -3029,7 +3049,6 @@ class GuiDocEditFooter(QWidget): self.linesText.setAutoFillBackground(True) self.linesText.setFixedHeight(fPx) self.linesText.setAlignment(QtAlignLeftTop) - self.linesText.setFont(lblFont) # Words self.wordsIcon = QLabel("", self) @@ -3044,7 +3063,6 @@ class GuiDocEditFooter(QWidget): self.wordsText.setAutoFillBackground(True) self.wordsText.setFixedHeight(fPx) self.wordsText.setAlignment(QtAlignLeftTop) - self.wordsText.setFont(lblFont) # Assemble Layout self.outerBox = QHBoxLayout() @@ -3067,6 +3085,7 @@ class GuiDocEditFooter(QWidget): self.setMinimumHeight(fPx + 2*mPx) # Fix the Colours + self.updateFont() self.updateTheme() # Initialise Info @@ -3080,6 +3099,14 @@ class GuiDocEditFooter(QWidget): # Methods ## + def updateFont(self) -> None: + """Update the font settings.""" + self.setFont(SHARED.theme.guiFont) + self.statusText.setFont(SHARED.theme.guiFontSmall) + self.linesText.setFont(SHARED.theme.guiFontSmall) + self.wordsText.setFont(SHARED.theme.guiFontSmall) + return + def updateTheme(self) -> None: """Update theme elements.""" iPx = round(0.9*SHARED.theme.baseIconHeight) diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index fbcb5ab0..b6423b29 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -31,10 +31,7 @@ import logging from enum import Enum from PyQt5.QtCore import QPoint, Qt, QUrl, pyqtSignal, pyqtSlot -from PyQt5.QtGui import ( - QCursor, QFont, QMouseEvent, QPalette, QResizeEvent, QTextCursor, - QTextOption -) +from PyQt5.QtGui import QCursor, QMouseEvent, QPalette, QResizeEvent, QTextCursor, QTextOption from PyQt5.QtWidgets import ( QAction, QApplication, QFrame, QHBoxLayout, QLabel, QMenu, QTextBrowser, QToolButton, QWidget @@ -141,12 +138,7 @@ class GuiDocViewer(QTextBrowser): def initViewer(self) -> None: """Set editor settings from main config.""" self._makeStyleSheet() - - # Set Font - font = QFont() - font.setFamily(CONFIG.textFont) - font.setPointSize(CONFIG.textSize) - self.document().setDefaultFont(font) + self.initFont() # Set the widget colours to match syntax theme mainPalette = self.palette() @@ -189,6 +181,22 @@ class GuiDocViewer(QTextBrowser): return + def initFont(self) -> None: + """Set the font of the main widget and sub-widgets. This needs + special attention since there appears to be a bug in Qt 5.15.3. + See issues #1862 and #1875. + """ + font = self.font() + font.setFamily(CONFIG.textFont) + font.setPointSize(CONFIG.textSize) + self.setFont(font) + + # Reset sub-widget font to GUI font + self.docHeader.updateFont() + self.docFooter.updateFont() + + return + def loadText(self, tHandle: str, updateHistory: bool = True) -> bool: """Load text into the viewer from an item handle.""" if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE): @@ -645,10 +653,6 @@ class GuiDocViewHeader(QWidget): self.itemTitle.setAlignment(QtAlignCenterTop) self.itemTitle.setFixedHeight(iPx) - lblFont = self.itemTitle.font() - lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) - self.itemTitle.setFont(lblFont) - # Other Widgets self.outlineMenu = QMenu(self) @@ -699,7 +703,7 @@ class GuiDocViewHeader(QWidget): self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx) self.setMinimumHeight(iPx + 2*mPx) - # Fix the Colours + self.updateFont() self.updateTheme() logger.debug("Ready: GuiDocViewHeader") @@ -744,6 +748,12 @@ class GuiDocViewHeader(QWidget): self._docOutline = data return + def updateFont(self) -> None: + """Update the font settings.""" + self.setFont(SHARED.theme.guiFont) + self.itemTitle.setFont(SHARED.theme.guiFontSmall) + return + def updateTheme(self) -> None: """Update theme elements.""" self.outlineButton.setThemeIcon("list") @@ -883,11 +893,6 @@ class GuiDocViewFooter(QWidget): self.showSynopsis.toggled.connect(self._doToggleSynopsis) self.showSynopsis.setToolTip(self.tr("Show Synopsis Comments")) - lblFont = self.font() - lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) - self.showComments.setFont(lblFont) - self.showSynopsis.setFont(lblFont) - # Assemble Layout self.outerBox = QHBoxLayout() self.outerBox.addWidget(self.showHide, 0) @@ -903,7 +908,7 @@ class GuiDocViewFooter(QWidget): self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx) self.setMinimumHeight(iPx + 2*mPx) - # Fix the Colours + self.updateFont() self.updateTheme() logger.debug("Ready: GuiDocViewFooter") @@ -914,6 +919,13 @@ class GuiDocViewFooter(QWidget): # Methods ## + def updateFont(self) -> None: + """Update the font settings.""" + self.setFont(SHARED.theme.guiFont) + self.showComments.setFont(SHARED.theme.guiFontSmall) + self.showSynopsis.setFont(SHARED.theme.guiFontSmall) + return + def updateTheme(self) -> None: """Update theme elements.""" # Icons diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index f9c26b2e..59cc5a20 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -152,6 +152,8 @@ class GuiTheme: self.guiFont = QApplication.font() self.guiFontB = QApplication.font() self.guiFontB.setBold(True) + self.guiFontSmall = QApplication.font() + self.guiFontSmall.setPointSizeF(0.9*self.guiFont.pointSizeF()) qMetric = QFontMetrics(self.guiFont) fHeight = qMetric.height() diff --git a/novelwriter/tools/dictionaries.py b/novelwriter/tools/dictionaries.py index 9acc57f5..27ed8035 100644 --- a/novelwriter/tools/dictionaries.py +++ b/novelwriter/tools/dictionaries.py @@ -143,11 +143,12 @@ class GuiDictionaries(NNonBlockingDialog): try: import enchant path = Path(enchant.get_user_config_dir()) + self._installPath = Path(path).resolve() + self._installPath.mkdir(exist_ok=True, parents=True) except Exception: logger.error("Could not get enchant path") return False - self._installPath = Path(path).resolve() if path.is_dir(): self.inPath.setText(str(path)) hunspell = path / "hunspell" @@ -199,9 +200,9 @@ class GuiDictionaries(NNonBlockingDialog): if self._installPath: temp = self.huInput.text() if temp and (path := Path(temp)).is_file(): - hunspell = self._installPath / "hunspell" - hunspell.mkdir(exist_ok=True) try: + hunspell = self._installPath / "hunspell" + hunspell.mkdir(exist_ok=True) nAff, nDic = self._extractDicts(path, hunspell) if nAff == 0 or nDic == 0: self._appendLog(procErr, err=True) diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 231766cf..5c39b7a5 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -31,7 +31,7 @@ from time import time from typing import TYPE_CHECKING from PyQt5.QtCore import Qt, QTimer, QUrl, pyqtSignal, pyqtSlot -from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent +from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QPalette, QResizeEvent from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog from PyQt5.QtWidgets import ( QAbstractItemView, QApplication, QFormLayout, QGridLayout, QHBoxLayout, @@ -747,7 +747,6 @@ class _PreviewWidget(QTextBrowser): self.setPalette(dPalette) self.setMinimumWidth(40*SHARED.theme.textNWidth) - self.setTextFont(CONFIG.textFont, CONFIG.textSize) self.setTabStopDistance(CONFIG.getTabWidth()) self.setOpenExternalLinks(False) @@ -788,6 +787,8 @@ class _PreviewWidget(QTextBrowser): self._updateDocMargins() self._updateBuildAge() + self.setTextFont(CONFIG.textFont, CONFIG.textSize) + # Age Timer self.ageTimer = QTimer(self) self.ageTimer.setInterval(10000) @@ -817,12 +818,17 @@ class _PreviewWidget(QTextBrowser): return def setTextFont(self, family: str, size: int) -> None: - """Set the text font properties.""" - if family: - font = QFont() + """Set the text font properties and then reset for sub-widgets. + This needs special attention since there appears to be a bug in + Qt 5.15.3. See issues #1862 and #1875. + """ + if family and size > 4: + font = self.font() font.setFamily(family) font.setPointSize(size) - self.document().setDefaultFont(font) + self.setFont(font) + self.buildProgress.setFont(SHARED.theme.guiFont) + self.ageLabel.setFont(SHARED.theme.guiFontSmall) return ## diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 5aa8cdba..b3251487 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -58,7 +58,11 @@ Chapter One +<<<<<<< HEAD +======= + +>>>>>>> release Making a Scene diff --git a/tests/test_tools/test_tools_dictionaries.py b/tests/test_tools/test_tools_dictionaries.py index 1b3bd4f8..933f7ed2 100644 --- a/tests/test_tools/test_tools_dictionaries.py +++ b/tests/test_tools/test_tools_dictionaries.py @@ -37,7 +37,9 @@ from tests.mocked import causeException @pytest.mark.gui def testToolDictionaries_Main(qtbot, monkeypatch, nwGUI, fncPath): """Test the Dictionaries downloader tool.""" - monkeypatch.setattr(enchant, "get_user_config_dir", lambda *a: str(fncPath)) + # Must also create the enchant folder, see issue #1874 + enchPath = fncPath / "enchant" + monkeypatch.setattr(enchant, "get_user_config_dir", lambda *a: str(enchPath)) # Fail to open with monkeypatch.context() as mp: @@ -52,7 +54,7 @@ def testToolDictionaries_Main(qtbot, monkeypatch, nwGUI, fncPath): nwDicts = SHARED.findTopLevelWidget(GuiDictionaries) assert isinstance(nwDicts, GuiDictionaries) assert nwDicts.isVisible() - assert nwDicts.inPath.text() == str(fncPath) + assert nwDicts.inPath.text() == str(enchPath) # Allow Open Dir SHARED._lastAlert = "" @@ -98,9 +100,9 @@ def testToolDictionaries_Main(qtbot, monkeypatch, nwGUI, fncPath): nwDicts._doBrowseHunspell() assert nwDicts.huInput.text() == str(foDict) nwDicts._doImportHunspell() - assert (fncPath / "hunspell").is_dir() - assert (fncPath / "hunspell" / "en_GB.aff").is_file() - assert (fncPath / "hunspell" / "en_GB.dic").is_file() + assert (enchPath / "hunspell").is_dir() + assert (enchPath / "hunspell" / "en_GB.aff").is_file() + assert (enchPath / "hunspell" / "en_GB.dic").is_file() assert nwDicts.infoBox.blockCount() == 3 # Import Libre Office Dictionary @@ -109,9 +111,9 @@ def testToolDictionaries_Main(qtbot, monkeypatch, nwGUI, fncPath): nwDicts._doBrowseHunspell() assert nwDicts.huInput.text() == str(loDict) nwDicts._doImportHunspell() - assert (fncPath / "hunspell").is_dir() - assert (fncPath / "hunspell" / "en_US.aff").is_file() - assert (fncPath / "hunspell" / "en_US.dic").is_file() + assert (enchPath / "hunspell").is_dir() + assert (enchPath / "hunspell" / "en_US.aff").is_file() + assert (enchPath / "hunspell" / "en_US.dic").is_file() assert nwDicts.infoBox.blockCount() == 5 # Handle Unreadable File