From 00017e80650517f0394acdb0cc9b861dcface367 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:36:35 +0100 Subject: [PATCH 01/11] Reset to default font style after emphasis in QTextDocuments (#2121) --- novelwriter/formats/toqdoc.py | 39 +++++++++++++++++++-------- tests/test_base/test_base_error.py | 2 +- tests/test_formats/test_fmt_toqdoc.py | 18 ++++++------- 3 files changed, 38 insertions(+), 21 deletions(-) diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py index 1787413e0..e4231dfd6 100644 --- a/novelwriter/formats/toqdoc.py +++ b/novelwriter/formats/toqdoc.py @@ -74,11 +74,15 @@ def __init__(self, project: NWProject) -> None: self._usedFields: list[tuple[int, str]] = [] self._init = False - self._bold = QFont.Weight.Bold - self._normal = QFont.Weight.Normal self._newPage = False self._anchors = True + self._hWeight = QFont.Weight.Bold + self._dWeight = QFont.Weight.Normal + self._dItalic = False + self._dStrike = False + self._dUnderline = False + self._pageSize = QPageSize(QPageSize.PageSizeId.A4) self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0) @@ -128,12 +132,21 @@ def initDocument(self) -> None: self._document.clear() self._document.setDefaultFont(self._textFont) - fPt = self._textFont.pointSizeF() - fPx = fPt*96.0/72.0 # 1 em in pixels + # Default Styles + self._dWeight = self._textFont.weight() + self._dItalic = self._textFont.italic() + self._dStrike = self._textFont.strikeOut() + self._dUnderline = self._textFont.underline() + + # Header Weight + self._hWeight = QFont.Weight.Bold if self._boldHeads else self._dWeight # Scaled Sizes # ============ + fPt = self._textFont.pointSizeF() + fPx = fPt*4.0/3.0 # 1 em in pixels + self._mHead = { BlockTyp.TITLE: (fPx * self._marginTitle[0], fPx * self._marginTitle[1]), BlockTyp.HEAD1: (fPx * self._marginHead1[0], fPx * self._marginHead1[1]), @@ -318,21 +331,21 @@ def _insertFragments( # Construct next format if fmt == TextFmt.B_B: - cFmt.setFontWeight(self._bold) + cFmt.setFontWeight(QFont.Weight.Bold) elif fmt == TextFmt.B_E: - cFmt.setFontWeight(self._normal) + cFmt.setFontWeight(self._dWeight) elif fmt == TextFmt.I_B: cFmt.setFontItalic(True) elif fmt == TextFmt.I_E: - cFmt.setFontItalic(False) + cFmt.setFontItalic(self._dItalic) elif fmt == TextFmt.D_B: cFmt.setFontStrikeOut(True) elif fmt == TextFmt.D_E: - cFmt.setFontStrikeOut(False) + cFmt.setFontStrikeOut(self._dStrike) elif fmt == TextFmt.U_B: cFmt.setFontUnderline(True) elif fmt == TextFmt.U_E: - cFmt.setFontUnderline(False) + cFmt.setFontUnderline(self._dUnderline) elif fmt == TextFmt.M_B: cFmt.setBackground(self._theme.highlight) elif fmt == TextFmt.M_E: @@ -376,7 +389,7 @@ def _insertFragments( cFmt.setAnchorHref(data) elif fmt == TextFmt.HRF_E: cFmt.setForeground(primary or self._theme.text) - cFmt.setFontUnderline(False) + cFmt.setFontUnderline(self._dUnderline) cFmt.setAnchor(False) cFmt.setAnchorHref("") elif fmt == TextFmt.FNOTE: @@ -422,6 +435,10 @@ def _insertNewPageMarker(self, cursor: QTextCursor) -> None: bFmt.setLineHeight(75.0, QtPropLineHeight) cFmt = QTextCharFormat(self._charFmt) + cFmt.setFontItalic(False) + cFmt.setFontUnderline(False) + cFmt.setFontStrikeOut(False) + cFmt.setFontWeight(QFont.Weight.Normal) cFmt.setFontPointSize(0.75*self._textFont.pointSizeF()) cFmt.setForeground(self._theme.comment) @@ -440,7 +457,7 @@ def _genHeadStyle(self, hType: BlockTyp, hKey: str, rFmt: QTextBlockFormat) -> T hCol = self._colorHeads and hType != BlockTyp.TITLE cFmt = QTextCharFormat(self._charFmt) cFmt.setForeground(self._theme.head if hCol else self._theme.text) - cFmt.setFontWeight(self._bold if self._boldHeads else self._normal) + cFmt.setFontWeight(self._hWeight) cFmt.setFontPointSize(self._sHead.get(hType, 1.0)) if hKey and self._anchors: cFmt.setAnchorNames([hKey]) diff --git a/tests/test_base/test_base_error.py b/tests/test_base/test_base_error.py index 05aff7925..266243f6c 100644 --- a/tests/test_base/test_base_error.py +++ b/tests/test_base/test_base_error.py @@ -101,7 +101,7 @@ def testBaseError_Handler(qtbot, monkeypatch, nwGUI): with monkeypatch.context() as mp: mp.setattr(NWErrorMessage, "exec", lambda *a: None) mp.setattr("PyQt5.QtWidgets.QApplication.exit", lambda *a: None) - mp.setattr(nwGUI, "closeMain", causeException) + mp.setattr("novelwriter.guimain.GuiMain.closeMain", causeException) exceptionHandler(Exception, "Error Message", None) # type: ignore nwGUI.closeMain() diff --git a/tests/test_formats/test_fmt_toqdoc.py b/tests/test_formats/test_fmt_toqdoc.py index 743876532..e1b361fc1 100644 --- a/tests/test_formats/test_fmt_toqdoc.py +++ b/tests/test_formats/test_fmt_toqdoc.py @@ -22,7 +22,7 @@ import pytest -from PyQt5.QtGui import QTextBlock, QTextCharFormat, QTextCursor +from PyQt5.QtGui import QFont, QTextBlock, QTextCharFormat, QTextCursor from novelwriter import CONFIG from novelwriter.constants import nwUnicode @@ -71,7 +71,7 @@ def testFmtToQTextDocument_ConvertHeaders(mockGUI): assert bFmt.topMargin() == doc._mHead[BlockTyp.TITLE][0] assert bFmt.bottomMargin() == doc._mHead[BlockTyp.TITLE][1] cFmt = charFmtInBlock(block, 1) - assert cFmt.fontWeight() == doc._bold + assert cFmt.fontWeight() == QFont.Weight.Bold assert cFmt.fontPointSize() == doc._sHead[BlockTyp.TITLE] assert cFmt.foreground().color() == THEME.text @@ -82,7 +82,7 @@ def testFmtToQTextDocument_ConvertHeaders(mockGUI): assert bFmt.topMargin() == doc._mHead[BlockTyp.HEAD1][0] assert bFmt.bottomMargin() == doc._mHead[BlockTyp.HEAD1][1] cFmt = charFmtInBlock(block, 1) - assert cFmt.fontWeight() == doc._bold + assert cFmt.fontWeight() == QFont.Weight.Bold assert cFmt.fontPointSize() == doc._sHead[BlockTyp.HEAD1] assert cFmt.foreground().color() == THEME.head @@ -93,7 +93,7 @@ def testFmtToQTextDocument_ConvertHeaders(mockGUI): assert bFmt.topMargin() == doc._mHead[BlockTyp.HEAD2][0] assert bFmt.bottomMargin() == doc._mHead[BlockTyp.HEAD2][1] cFmt = charFmtInBlock(block, 1) - assert cFmt.fontWeight() == doc._bold + assert cFmt.fontWeight() == QFont.Weight.Bold assert cFmt.fontPointSize() == doc._sHead[BlockTyp.HEAD2] assert cFmt.foreground().color() == THEME.head @@ -104,7 +104,7 @@ def testFmtToQTextDocument_ConvertHeaders(mockGUI): assert bFmt.topMargin() == doc._mHead[BlockTyp.HEAD3][0] assert bFmt.bottomMargin() == doc._mHead[BlockTyp.HEAD3][1] cFmt = charFmtInBlock(block, 1) - assert cFmt.fontWeight() == doc._bold + assert cFmt.fontWeight() == QFont.Weight.Bold assert cFmt.fontPointSize() == doc._sHead[BlockTyp.HEAD3] assert cFmt.foreground().color() == THEME.head @@ -115,7 +115,7 @@ def testFmtToQTextDocument_ConvertHeaders(mockGUI): assert bFmt.topMargin() == doc._mHead[BlockTyp.HEAD4][0] assert bFmt.bottomMargin() == doc._mHead[BlockTyp.HEAD4][1] cFmt = charFmtInBlock(block, 1) - assert cFmt.fontWeight() == doc._bold + assert cFmt.fontWeight() == QFont.Weight.Bold assert cFmt.fontPointSize() == doc._sHead[BlockTyp.HEAD4] assert cFmt.foreground().color() == THEME.head @@ -478,11 +478,11 @@ def testFmtToQTextDocument_TextCharFormats(mockGUI): block = doc.document.findBlockByNumber(1) assert block.text() == "With bold text" cFmt = charFmtInBlock(block, 1) - assert cFmt.fontWeight() == doc._normal + assert cFmt.fontWeight() == doc._dWeight cFmt = charFmtInBlock(block, 6) - assert cFmt.fontWeight() == doc._bold + assert cFmt.fontWeight() == QFont.Weight.Bold cFmt = charFmtInBlock(block, 10) - assert cFmt.fontWeight() == doc._normal + assert cFmt.fontWeight() == doc._dWeight # 2: Italic block = doc.document.findBlockByNumber(2) From 05b1fdd7f2442a91102afa030b50cf85db0301f8 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 25 Nov 2024 23:43:38 +0100 Subject: [PATCH 02/11] Drop relative font weights in ODT output --- novelwriter/formats/toodt.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/novelwriter/formats/toodt.py b/novelwriter/formats/toodt.py index 37ba5943e..dedec194e 100644 --- a/novelwriter/formats/toodt.py +++ b/novelwriter/formats/toodt.py @@ -43,7 +43,7 @@ from novelwriter.core.project import NWProject from novelwriter.formats.shared import BlockFmt, BlockTyp, TextFmt, stripEscape from novelwriter.formats.tokenizer import Tokenizer -from novelwriter.types import FONT_STYLE, FONT_WEIGHTS, QtHexRgb +from novelwriter.types import FONT_STYLE, QtHexRgb logger = logging.getLogger(__name__) @@ -220,20 +220,14 @@ def initDocument(self) -> None: # Initialise Variables # ==================== - intWeight = FONT_WEIGHTS.get(self._textFont.weight(), 400) - fontWeight = str(intWeight) - fontBold = str(min(intWeight + 300, 900)) - lang, _, country = self._dLocale.name().partition("_") self._dLanguage = lang or self._dLanguage self._dCountry = country or self._dCountry self._fontFamily = self._textFont.family() self._fontSize = self._textFont.pointSize() - self._fontWeight = FONT_WEIGHT_MAP.get(fontWeight, fontWeight) self._fontStyle = FONT_STYLE.get(self._textFont.style(), "normal") self._fontPitch = "fixed" if self._textFont.fixedPitch() else "variable" - self._fontBold = FONT_WEIGHT_MAP.get(fontBold, fontBold) self._headWeight = self._fontBold if self._boldHeads else None self._fBlockIndent = self._emToCm(self._blockIndent) From 2808c367538710c521474084e05c0ab592eb4884 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 26 Nov 2024 00:40:09 +0100 Subject: [PATCH 03/11] Rename tokenizer setFont to setTextFont --- novelwriter/core/docbuild.py | 2 +- novelwriter/formats/tokenizer.py | 2 +- novelwriter/gui/docviewer.py | 2 +- tests/test_formats/test_fmt_tokenizer.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index 5cbdfc0a4..4d17ebd6b 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -218,7 +218,7 @@ def _setupBuild(self, bldObj: Tokenizer) -> dict: textFont = QFont(CONFIG.textFont) textFont.fromString(self._build.getStr("format.textFont")) - bldObj.setFont(textFont) + bldObj.setTextFont(textFont) bldObj.setLanguage(self._project.data.language) bldObj.setPartitionFormat( diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index 9af172164..d230757bd 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -302,7 +302,7 @@ def setSceneStyle(self, center: bool, pageBreak: bool) -> None: self._sceneStyle |= BlockFmt.PBB if pageBreak else BlockFmt.NONE return - def setFont(self, font: QFont) -> None: + def setTextFont(self, font: QFont) -> None: """Set the build font.""" self._textFont = font return diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index 9e1b6f5f3..d57303a6f 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -219,7 +219,7 @@ def loadText(self, tHandle: str, updateHistory: bool = True) -> bool: qDoc = ToQTextDocument(SHARED.project) qDoc.setJustify(CONFIG.doJustify) qDoc.setDialogHighlight(True) - qDoc.setFont(CONFIG.textFont) + qDoc.setTextFont(CONFIG.textFont) qDoc.setTheme(self._docTheme) qDoc.initDocument() qDoc.setKeywords(True) diff --git a/tests/test_formats/test_fmt_tokenizer.py b/tests/test_formats/test_fmt_tokenizer.py index bab57bfca..cbfdb0aea 100644 --- a/tests/test_formats/test_fmt_tokenizer.py +++ b/tests/test_formats/test_fmt_tokenizer.py @@ -110,7 +110,7 @@ def testFmtToken_Setters(mockGUI): tokens.setSceneFormat(f"S: {nwHeadFmt.TITLE}", True) tokens.setHardSceneFormat(f"H: {nwHeadFmt.TITLE}", True) tokens.setSectionFormat(f"X: {nwHeadFmt.TITLE}", True) - tokens.setFont(QFont("Monospace", 10)) + tokens.setTextFont(QFont("Monospace", 10)) tokens.setLineHeight(2.0) tokens.setBlockIndent(6.0) tokens.setJustify(True) From 84631883360054ee2eae291115b67b5717a85cc3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 26 Nov 2024 01:06:47 +0100 Subject: [PATCH 04/11] Fix default font in editor --- novelwriter/gui/doceditor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index a77869ca2..83609ffa1 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -349,6 +349,7 @@ def initEditor(self) -> None: # Set the font. See issues #1862 and #1875. self.setFont(CONFIG.textFont) + self._qDocument.setDefaultFont(CONFIG.textFont) self.docHeader.updateFont() self.docFooter.updateFont() self.docSearch.updateFont() From ff470028be9d3c8bde073c86862b707a6f102931 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 26 Nov 2024 02:03:09 +0100 Subject: [PATCH 05/11] Add a font matcher for text fonts (#2118) --- novelwriter/common.py | 23 ++++++++++++++++++++++- novelwriter/formats/tokenizer.py | 4 ++-- novelwriter/gui/doceditor.py | 7 ++++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/novelwriter/common.py b/novelwriter/common.py index 64a4de1b7..7cc5f67b5 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -38,7 +38,7 @@ from urllib.request import pathname2url from PyQt5.QtCore import QCoreApplication, QMimeData, QUrl -from PyQt5.QtGui import QColor, QDesktopServices, QFont, QFontInfo +from PyQt5.QtGui import QColor, QDesktopServices, QFont, QFontDatabase, QFontInfo from novelwriter.constants import nwConst, nwLabels, nwUnicode, trConst from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType @@ -434,6 +434,27 @@ def describeFont(font: QFont) -> str: return "Error" +def fontMatcher(font: QFont) -> QFont: + """Make sure the font is the correct family, if possible. This + ensures that Qt doesn't use the GUI or document font instead. + """ + info = QFontInfo(font) + if (famRequest := font.family()) != (famActual := info.family()): + logger.warning("Font mismatch: %s != %s", famRequest, famActual) + db = QFontDatabase() + if famRequest in db.families(): + styleRequest, sizeRequest = font.styleName(), font.pointSize() + logger.info("Lookup: %s, %s, %d pt", famRequest, styleRequest, sizeRequest) + temp = db.font(famRequest, styleRequest, sizeRequest) + famFound, styleFound, sizeFound = temp.family(), temp.styleName(), temp.pointSize() + if famFound == famRequest: + logger.info("Found: %s, %s, %d pt", famFound, styleFound, sizeFound) + return temp + logger.warning("Could not find a font match") + logger.warning("You may need to restart the application") + return font + + def qtLambda(func: Callable, *args: Any, **kwargs: Any) -> Callable: """A replacement for Python lambdas that works for Qt slots.""" def wrapper(*a_: Any) -> None: diff --git a/novelwriter/formats/tokenizer.py b/novelwriter/formats/tokenizer.py index d230757bd..2b75130db 100644 --- a/novelwriter/formats/tokenizer.py +++ b/novelwriter/formats/tokenizer.py @@ -35,7 +35,7 @@ from PyQt5.QtGui import QColor, QFont from novelwriter import CONFIG -from novelwriter.common import checkInt, numberToRoman +from novelwriter.common import checkInt, fontMatcher, numberToRoman from novelwriter.constants import ( nwHeadFmt, nwKeyWords, nwLabels, nwShortcode, nwStats, nwStyles, nwUnicode, trConst @@ -304,7 +304,7 @@ def setSceneStyle(self, center: bool, pageBreak: bool) -> None: def setTextFont(self, font: QFont) -> None: """Set the build font.""" - self._textFont = font + self._textFont = fontMatcher(font) return def setLineHeight(self, height: float) -> None: diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 83609ffa1..b5f76cd29 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -53,7 +53,7 @@ ) from novelwriter import CONFIG, SHARED -from novelwriter.common import decodeMimeHandles, minmax, qtLambda, transferCase +from novelwriter.common import decodeMimeHandles, fontMatcher, minmax, qtLambda, transferCase from novelwriter.constants import nwConst, nwKeyWords, nwShortcode, nwUnicode from novelwriter.core.document import NWDocument from novelwriter.enum import ( @@ -348,8 +348,9 @@ def initEditor(self) -> None: SHARED.updateSpellCheckLanguage() # Set the font. See issues #1862 and #1875. - self.setFont(CONFIG.textFont) - self._qDocument.setDefaultFont(CONFIG.textFont) + font = fontMatcher(CONFIG.textFont) + self.setFont(font) + self._qDocument.setDefaultFont(font) self.docHeader.updateFont() self.docFooter.updateFont() self.docSearch.updateFont() From 18c70ffe3ea8e8d0f0e81a9af9e2464cd1f8686e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 26 Nov 2024 02:22:36 +0100 Subject: [PATCH 06/11] Allow setting resolution for ToQTextDocument converter --- novelwriter/core/docbuild.py | 2 +- novelwriter/formats/toqdoc.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index 4d17ebd6b..f21fa1b98 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -178,7 +178,7 @@ def iterBuildDocument(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[i makeObj = ToQTextDocument(self._project) makeObj.disableAnchors() filtered = self._setupBuild(makeObj) - makeObj.initDocument() + makeObj.initDocument(resolution=1200) yield from self._iterBuild(makeObj, filtered) makeObj.closeDocument() diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py index e4231dfd6..0030a3bed 100644 --- a/novelwriter/formats/toqdoc.py +++ b/novelwriter/formats/toqdoc.py @@ -85,6 +85,7 @@ def __init__(self, project: NWProject) -> None: self._pageSize = QPageSize(QPageSize.PageSizeId.A4) self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0) + self._resolution = 96 return @@ -123,9 +124,10 @@ def disableAnchors(self) -> None: # Class Methods ## - def initDocument(self) -> None: + def initDocument(self, resolution: int = 96) -> None: """Initialise all computed values of the document.""" super().initDocument() + self._resolution = resolution self._document.setUndoRedoEnabled(False) self._document.blockSignals(True) @@ -168,8 +170,8 @@ def initDocument(self) -> None: self._mMeta = (fPx * self._marginMeta[0], fPx * self._marginMeta[1]) self._mSep = (fPx * self._marginSep[0], fPx * self._marginSep[1]) - self._mIndent = fPx * 2.0 - self._tIndent = fPx * self._firstWidth + self._mIndent = fPx * self._resolution/96 * 2.0 + self._tIndent = fPx * self._resolution/96 * self._firstWidth # Text Formats # ============ @@ -265,6 +267,7 @@ def saveDocument(self, path: Path) -> None: printer = QPrinter(QPrinter.PrinterMode.HighResolution) printer.setDocName(self._project.data.name) printer.setCreator(f"novelWriter/{__version__}") + printer.setResolution(self._resolution) printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat) printer.setPageSize(self._pageSize) printer.setPageMargins(m.left(), m.top(), m.right(), m.bottom(), QPrinter.Unit.Millimeter) From c091e0b9bb89e074da4a5229c49e0873a10348b2 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:06:34 +0100 Subject: [PATCH 07/11] Apply the font matcher closer to config read --- novelwriter/common.py | 10 ++++++---- novelwriter/config.py | 27 +++++++++++++++------------ novelwriter/formats/toqdoc.py | 5 +++-- novelwriter/guimain.py | 10 +++++----- novelwriter/tools/manussettings.py | 11 ++++++----- 5 files changed, 35 insertions(+), 28 deletions(-) diff --git a/novelwriter/common.py b/novelwriter/common.py index 7cc5f67b5..8e946cee4 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -436,11 +436,13 @@ def describeFont(font: QFont) -> str: def fontMatcher(font: QFont) -> QFont: """Make sure the font is the correct family, if possible. This - ensures that Qt doesn't use the GUI or document font instead. + ensures that Qt doesn't reuse another font under the hood. The + default Qt5 font matching algorithm doesn't handle well changing + fonts at runtime. """ info = QFontInfo(font) if (famRequest := font.family()) != (famActual := info.family()): - logger.warning("Font mismatch: %s != %s", famRequest, famActual) + logger.warning("Font mismatch: Requested '%s', but got '%s'", famRequest, famActual) db = QFontDatabase() if famRequest in db.families(): styleRequest, sizeRequest = font.styleName(), font.pointSize() @@ -450,8 +452,8 @@ def fontMatcher(font: QFont) -> QFont: if famFound == famRequest: logger.info("Found: %s, %s, %d pt", famFound, styleFound, sizeFound) return temp - logger.warning("Could not find a font match") - logger.warning("You may need to restart the application") + logger.warning("Could not find a font match in the font database") + logger.warning("If you just changed font, you may need to restart the application") return font diff --git a/novelwriter/config.py b/novelwriter/config.py index 45dc01d97..d1ae57b5e 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -40,7 +40,10 @@ from PyQt5.QtGui import QFont, QFontDatabase from PyQt5.QtWidgets import QApplication -from novelwriter.common import NWConfigParser, checkInt, checkPath, describeFont, formatTimeStamp +from novelwriter.common import ( + NWConfigParser, checkInt, checkPath, describeFont, fontMatcher, + formatTimeStamp +) from novelwriter.constants import nwFiles, nwUnicode from novelwriter.error import formatException, logException @@ -369,10 +372,11 @@ def setBackupPath(self, path: Path | str) -> None: def setGuiFont(self, value: QFont | str | None) -> None: """Update the GUI's font style from settings.""" if isinstance(value, QFont): - self.guiFont = value + self.guiFont = fontMatcher(value) elif value and isinstance(value, str): - self.guiFont = QFont() - self.guiFont.fromString(value) + font = QFont() + font.fromString(value) + self.guiFont = fontMatcher(font) else: font = QFont() fontDB = QFontDatabase() @@ -382,11 +386,9 @@ def setGuiFont(self, value: QFont | str | None) -> None: font.setPointSize(10) else: font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont) - self.guiFont = font + self.guiFont = fontMatcher(font) logger.debug("GUI font set to: %s", describeFont(font)) - QApplication.setFont(self.guiFont) - return def setTextFont(self, value: QFont | str | None) -> None: @@ -394,10 +396,11 @@ def setTextFont(self, value: QFont | str | None) -> None: set to default font. """ if isinstance(value, QFont): - self.textFont = value + self.textFont = fontMatcher(value) elif value and isinstance(value, str): - self.textFont = QFont() - self.textFont.fromString(value) + font = QFont() + font.fromString(value) + self.textFont = fontMatcher(font) else: fontDB = QFontDatabase() fontFam = fontDB.families() @@ -411,8 +414,8 @@ def setTextFont(self, value: QFont | str | None) -> None: font.setPointSize(12) else: font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont) - self.textFont = font - logger.debug("Text font set to: %s", describeFont(font)) + self.textFont = fontMatcher(font) + logger.debug("Text font set to: %s", describeFont(self.textFont)) return ## diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py index 0030a3bed..59331bacf 100644 --- a/novelwriter/formats/toqdoc.py +++ b/novelwriter/formats/toqdoc.py @@ -148,6 +148,7 @@ def initDocument(self, resolution: int = 96) -> None: fPt = self._textFont.pointSizeF() fPx = fPt*4.0/3.0 # 1 em in pixels + fDt = fPx * self._resolution/96.0 # In dots for a given resolution self._mHead = { BlockTyp.TITLE: (fPx * self._marginTitle[0], fPx * self._marginTitle[1]), @@ -170,8 +171,8 @@ def initDocument(self, resolution: int = 96) -> None: self._mMeta = (fPx * self._marginMeta[0], fPx * self._marginMeta[1]) self._mSep = (fPx * self._marginSep[0], fPx * self._marginSep[1]) - self._mIndent = fPx * self._resolution/96 * 2.0 - self._tIndent = fPx * self._resolution/96 * self._firstWidth + self._mIndent = fDt * 2.0 + self._tIndent = fDt * self._firstWidth # Text Formats # ============ diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 8d83bd926..d0f74bdce 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -1051,11 +1051,6 @@ def _processConfigChanges(self, restart: bool, tree: bool, theme: bool, syntax: self.initMain() self.saveDocument() - if restart: - SHARED.info(self.tr( - "Some changes will not be applied until novelWriter has been restarted." - )) - if tree: SHARED.project.tree.refreshAllItems() @@ -1088,6 +1083,11 @@ def _processConfigChanges(self, restart: bool, tree: bool, theme: bool, syntax: self._lastTotalCount = 0 self._updateStatusWordCount() + if restart: + SHARED.info(self.tr( + "Some changes will not be applied until novelWriter has been restarted." + )) + return @pyqtSlot() diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index e5221cd24..dc64a316e 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -37,7 +37,7 @@ ) from novelwriter import CONFIG, SHARED -from novelwriter.common import describeFont, qtLambda +from novelwriter.common import describeFont, fontMatcher, qtLambda from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwStyles, trConst from novelwriter.core.buildsettings import BuildSettings, FilterMode from novelwriter.extensions.configlayout import ( @@ -1281,8 +1281,9 @@ def loadContent(self) -> None: # Text Format # =========== - self._textFont = QFont() - self._textFont.fromString(self._build.getStr("format.textFont")) + font = QFont() + font.fromString(self._build.getStr("format.textFont")) + self._textFont = fontMatcher(font) self.textFont.setText(describeFont(self._textFont)) self.textFont.setCursorPosition(0) @@ -1437,9 +1438,9 @@ def _selectFont(self) -> None: """Open the QFontDialog and set a font for the font style.""" font, status = SHARED.getFont(self._textFont, CONFIG.nativeFont) if status: - self.textFont.setText(describeFont(font)) + self._textFont = fontMatcher(font) + self.textFont.setText(describeFont(self._textFont)) self.textFont.setCursorPosition(0) - self._textFont = font return @pyqtSlot(int) From 43d95462e02eefe63462c0e01459712bf031a974 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:07:02 +0100 Subject: [PATCH 08/11] Optimise a few details and add some tests --- novelwriter/common.py | 11 +++++-- novelwriter/core/itemmodel.py | 6 ++-- novelwriter/gui/doceditor.py | 6 ++-- tests/test_base/test_base_common.py | 47 +++++++++++++++++++++++------ 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/novelwriter/common.py b/novelwriter/common.py index 8e946cee4..1dd2c64c8 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -436,9 +436,9 @@ def describeFont(font: QFont) -> str: def fontMatcher(font: QFont) -> QFont: """Make sure the font is the correct family, if possible. This - ensures that Qt doesn't reuse another font under the hood. The + ensures that Qt doesn't re-use another font under the hood. The default Qt5 font matching algorithm doesn't handle well changing - fonts at runtime. + application fonts at runtime. """ info = QFontInfo(font) if (famRequest := font.family()) != (famActual := info.family()): @@ -448,6 +448,7 @@ def fontMatcher(font: QFont) -> QFont: styleRequest, sizeRequest = font.styleName(), font.pointSize() logger.info("Lookup: %s, %s, %d pt", famRequest, styleRequest, sizeRequest) temp = db.font(famRequest, styleRequest, sizeRequest) + temp.setPointSize(sizeRequest) # Make sure it isn't changed famFound, styleFound, sizeFound = temp.family(), temp.styleName(), temp.pointSize() if famFound == famRequest: logger.info("Found: %s, %s, %d pt", famFound, styleFound, sizeFound) @@ -464,6 +465,12 @@ def wrapper(*a_: Any) -> None: return wrapper +def encodeMimeHandles(mimeData: QMimeData, handles: list[str]) -> None: + """Encode handles into a mime data object.""" + mimeData.setData(nwConst.MIME_HANDLE, b"|".join(h.encode() for h in handles)) + return + + def decodeMimeHandles(mimeData: QMimeData) -> list[str]: """Decode and split a mime data object with handles.""" return mimeData.data(nwConst.MIME_HANDLE).data().decode().split("|") diff --git a/novelwriter/core/itemmodel.py b/novelwriter/core/itemmodel.py index df4d33c14..f8784ac23 100644 --- a/novelwriter/core/itemmodel.py +++ b/novelwriter/core/itemmodel.py @@ -31,7 +31,7 @@ from PyQt5.QtCore import QAbstractItemModel, QMimeData, QModelIndex, Qt from PyQt5.QtGui import QFont, QIcon -from novelwriter.common import decodeMimeHandles, minmax +from novelwriter.common import decodeMimeHandles, encodeMimeHandles, minmax from novelwriter.constants import nwConst from novelwriter.core.item import NWItem from novelwriter.enum import nwItemClass @@ -367,11 +367,11 @@ def mimeTypes(self) -> list[str]: def mimeData(self, indices: list[QModelIndex]) -> QMimeData: """Encode mime data about a selection.""" handles = [ - i.internalPointer().item.itemHandle.encode() + i.internalPointer().item.itemHandle for i in indices if i.isValid() and i.column() == 0 ] mime = QMimeData() - mime.setData(nwConst.MIME_HANDLE, b"|".join(handles)) + encodeMimeHandles(mime, handles) return mime def canDropMimeData( diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index b5f76cd29..1e53c7f74 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -1279,11 +1279,13 @@ def _updateDocCounts(self, cCount: int, wCount: int, pCount: int) -> None: """Process the word counter's finished signal.""" if self._docHandle and self._nwItem: logger.debug("Updating word count") + needsRefresh = wCount != self._nwItem.wordCount self._nwItem.setCharCount(cCount) self._nwItem.setWordCount(wCount) self._nwItem.setParaCount(pCount) - self._nwItem.notifyToRefresh() - self.docFooter.updateWordCount(wCount, False) + if needsRefresh: + self._nwItem.notifyToRefresh() + self.docFooter.updateWordCount(wCount, False) return @pyqtSlot() diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index 8eae1ba0f..ead951c8e 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -27,18 +27,19 @@ import pytest -from PyQt5.QtCore import QUrl -from PyQt5.QtGui import QColor, QDesktopServices, QFontDatabase +from PyQt5.QtCore import QMimeData, QUrl +from PyQt5.QtGui import QColor, QDesktopServices, QFont, QFontDatabase, QFontInfo from novelwriter.common import ( NWConfigParser, checkBool, checkFloat, checkInt, checkIntTuple, checkPath, - checkString, checkStringNone, checkUuid, compact, cssCol, describeFont, - elide, firstFloat, formatFileFilter, formatInt, formatTime, - formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, - isItemClass, isItemLayout, isItemType, isListInstance, isTitleTag, - jsonEncode, makeFileNameSafe, minmax, numberToRoman, openExternalPath, - readTextFile, simplified, transferCase, uniqueCompact, xmlElement, - xmlIndent, xmlSubElem, yesNo + checkString, checkStringNone, checkUuid, compact, cssCol, + decodeMimeHandles, describeFont, elide, encodeMimeHandles, firstFloat, + fontMatcher, formatFileFilter, formatInt, formatTime, formatTimeStamp, + formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, + isItemLayout, isItemType, isListInstance, isTitleTag, jsonEncode, + makeFileNameSafe, minmax, numberToRoman, openExternalPath, readTextFile, + simplified, transferCase, uniqueCompact, xmlElement, xmlIndent, xmlSubElem, + yesNo ) from tests.mocked import causeOSError @@ -528,6 +529,34 @@ def testBaseCommon_describeFont(): assert describeFont(None) == "Error" # type: ignore +@pytest.mark.base +def testBaseCommon_fontMatcher(monkeypatch): + """Test the fontMatcher function.""" + # Nonsense font is just returned + nonsense = QFont("nonesense", 10) + assert fontMatcher(nonsense) is nonsense + + # General font + fontDB = QFontDatabase() + if len(fontDB.families()) > 1: + fontOne = QFont(fontDB.families()[0]) + fontTwo = QFont(fontDB.families()[1]) + check = QFont(fontOne) + check.setFamily(fontTwo.family()) + with monkeypatch.context() as mp: + mp.setattr(QFontInfo, "family", lambda *a: "nonesense") + assert fontMatcher(check).family() == fontTwo.family() + + +@pytest.mark.base +def testBaseCommon_encodeDecodeMimeHandles(monkeypatch): + """Test the encodeMimeHandles and decodeMimeHandles functions.""" + handles = ["0123456789abc", "123456789abcd", "23456789abcde"] + mimeData = QMimeData() + encodeMimeHandles(mimeData, handles) + assert decodeMimeHandles(mimeData) == handles + + @pytest.mark.base def testBaseCommon_jsonEncode(): """Test the jsonEncode function.""" From 07c61450c7a53c5185e25aae329504a6db09e843 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:57:07 +0100 Subject: [PATCH 09/11] Try forcing resolution for PDF printer --- novelwriter/core/docbuild.py | 2 +- novelwriter/formats/toqdoc.py | 31 +++++++++++++++---------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index f21fa1b98..a98da986a 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -178,7 +178,7 @@ def iterBuildDocument(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[i makeObj = ToQTextDocument(self._project) makeObj.disableAnchors() filtered = self._setupBuild(makeObj) - makeObj.initDocument(resolution=1200) + makeObj.initDocument(pdf=True) yield from self._iterBuild(makeObj, filtered) makeObj.closeDocument() diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py index 59331bacf..dfd343ff8 100644 --- a/novelwriter/formats/toqdoc.py +++ b/novelwriter/formats/toqdoc.py @@ -83,9 +83,9 @@ def __init__(self, project: NWProject) -> None: self._dStrike = False self._dUnderline = False + self._dpi = 96 self._pageSize = QPageSize(QPageSize.PageSizeId.A4) self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0) - self._resolution = 96 return @@ -124,11 +124,11 @@ def disableAnchors(self) -> None: # Class Methods ## - def initDocument(self, resolution: int = 96) -> None: + def initDocument(self, pdf: bool = False) -> None: """Initialise all computed values of the document.""" super().initDocument() - self._resolution = resolution + self._dpi = 72 if pdf else 96 self._document.setUndoRedoEnabled(False) self._document.blockSignals(True) self._document.clear() @@ -147,15 +147,14 @@ def initDocument(self, resolution: int = 96) -> None: # ============ fPt = self._textFont.pointSizeF() - fPx = fPt*4.0/3.0 # 1 em in pixels - fDt = fPx * self._resolution/96.0 # In dots for a given resolution + mSc = fPt * self._dpi/72.0 self._mHead = { - BlockTyp.TITLE: (fPx * self._marginTitle[0], fPx * self._marginTitle[1]), - BlockTyp.HEAD1: (fPx * self._marginHead1[0], fPx * self._marginHead1[1]), - BlockTyp.HEAD2: (fPx * self._marginHead2[0], fPx * self._marginHead2[1]), - BlockTyp.HEAD3: (fPx * self._marginHead3[0], fPx * self._marginHead3[1]), - BlockTyp.HEAD4: (fPx * self._marginHead4[0], fPx * self._marginHead4[1]), + BlockTyp.TITLE: (mSc * self._marginTitle[0], mSc * self._marginTitle[1]), + BlockTyp.HEAD1: (mSc * self._marginHead1[0], mSc * self._marginHead1[1]), + BlockTyp.HEAD2: (mSc * self._marginHead2[0], mSc * self._marginHead2[1]), + BlockTyp.HEAD3: (mSc * self._marginHead3[0], mSc * self._marginHead3[1]), + BlockTyp.HEAD4: (mSc * self._marginHead4[0], mSc * self._marginHead4[1]), } hScale = self._scaleHeads @@ -167,12 +166,12 @@ def initDocument(self, resolution: int = 96) -> None: BlockTyp.HEAD4: (nwStyles.H_SIZES.get(4, 1.0) * fPt) if hScale else fPt, } - self._mText = (fPx * self._marginText[0], fPx * self._marginText[1]) - self._mMeta = (fPx * self._marginMeta[0], fPx * self._marginMeta[1]) - self._mSep = (fPx * self._marginSep[0], fPx * self._marginSep[1]) + self._mText = (mSc * self._marginText[0], mSc * self._marginText[1]) + self._mMeta = (mSc * self._marginMeta[0], mSc * self._marginMeta[1]) + self._mSep = (mSc * self._marginSep[0], mSc * self._marginSep[1]) - self._mIndent = fDt * 2.0 - self._tIndent = fDt * self._firstWidth + self._mIndent = mSc * 2.0 + self._tIndent = mSc * self._firstWidth # Text Formats # ============ @@ -268,7 +267,7 @@ def saveDocument(self, path: Path) -> None: printer = QPrinter(QPrinter.PrinterMode.HighResolution) printer.setDocName(self._project.data.name) printer.setCreator(f"novelWriter/{__version__}") - printer.setResolution(self._resolution) + printer.setResolution(self._dpi) printer.setOutputFormat(QPrinter.OutputFormat.PdfFormat) printer.setPageSize(self._pageSize) printer.setPageMargins(m.left(), m.top(), m.right(), m.bottom(), QPrinter.Unit.Millimeter) From bcac05ca20c6fe687dcf531e8ceda76df8cecce2 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 26 Nov 2024 21:42:54 +0100 Subject: [PATCH 10/11] Only use 1200 DPI for scalable fonts --- novelwriter/formats/toqdoc.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py index dfd343ff8..f0f495e25 100644 --- a/novelwriter/formats/toqdoc.py +++ b/novelwriter/formats/toqdoc.py @@ -29,8 +29,8 @@ from PyQt5.QtCore import QMarginsF, QSizeF from PyQt5.QtGui import ( - QColor, QFont, QPageSize, QTextBlockFormat, QTextCharFormat, QTextCursor, - QTextDocument + QColor, QFont, QFontDatabase, QPageSize, QTextBlockFormat, QTextCharFormat, + QTextCursor, QTextDocument ) from PyQt5.QtPrintSupport import QPrinter @@ -128,7 +128,12 @@ def initDocument(self, pdf: bool = False) -> None: """Initialise all computed values of the document.""" super().initDocument() - self._dpi = 72 if pdf else 96 + if pdf: + fontDB = QFontDatabase() + family = self._textFont.family() + style = self._textFont.styleName() + self._dpi = 1200 if fontDB.isScalable(family, style) else 72 + self._document.setUndoRedoEnabled(False) self._document.blockSignals(True) self._document.clear() @@ -147,14 +152,15 @@ def initDocument(self, pdf: bool = False) -> None: # ============ fPt = self._textFont.pointSizeF() - mSc = fPt * self._dpi/72.0 + fPx = fPt*96.0/72.0 # 1 em in pixels + mPx = fPx * self._dpi/96.0 self._mHead = { - BlockTyp.TITLE: (mSc * self._marginTitle[0], mSc * self._marginTitle[1]), - BlockTyp.HEAD1: (mSc * self._marginHead1[0], mSc * self._marginHead1[1]), - BlockTyp.HEAD2: (mSc * self._marginHead2[0], mSc * self._marginHead2[1]), - BlockTyp.HEAD3: (mSc * self._marginHead3[0], mSc * self._marginHead3[1]), - BlockTyp.HEAD4: (mSc * self._marginHead4[0], mSc * self._marginHead4[1]), + BlockTyp.TITLE: (fPx * self._marginTitle[0], fPx * self._marginTitle[1]), + BlockTyp.HEAD1: (fPx * self._marginHead1[0], fPx * self._marginHead1[1]), + BlockTyp.HEAD2: (fPx * self._marginHead2[0], fPx * self._marginHead2[1]), + BlockTyp.HEAD3: (fPx * self._marginHead3[0], fPx * self._marginHead3[1]), + BlockTyp.HEAD4: (fPx * self._marginHead4[0], fPx * self._marginHead4[1]), } hScale = self._scaleHeads @@ -166,12 +172,12 @@ def initDocument(self, pdf: bool = False) -> None: BlockTyp.HEAD4: (nwStyles.H_SIZES.get(4, 1.0) * fPt) if hScale else fPt, } - self._mText = (mSc * self._marginText[0], mSc * self._marginText[1]) - self._mMeta = (mSc * self._marginMeta[0], mSc * self._marginMeta[1]) - self._mSep = (mSc * self._marginSep[0], mSc * self._marginSep[1]) + self._mText = (fPx * self._marginText[0], fPx * self._marginText[1]) + self._mMeta = (fPx * self._marginMeta[0], fPx * self._marginMeta[1]) + self._mSep = (fPx * self._marginSep[0], fPx * self._marginSep[1]) - self._mIndent = mSc * 2.0 - self._tIndent = mSc * self._firstWidth + self._mIndent = mPx * 2.0 + self._tIndent = mPx * self._firstWidth # Text Formats # ============ @@ -263,6 +269,7 @@ def doConvert(self) -> None: def saveDocument(self, path: Path) -> None: """Save the document as a PDF file.""" m = self._pageMargins + logger.info("Writing PDF at %d DPI", self._dpi) printer = QPrinter(QPrinter.PrinterMode.HighResolution) printer.setDocName(self._project.data.name) From 3b0086e75e1d7e85ac3f8430ef38c319e4ec1336 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 26 Nov 2024 23:56:31 +0100 Subject: [PATCH 11/11] Use a frame for page break marker in manuscript preview --- novelwriter/formats/toqdoc.py | 26 +++++++++++++++-------- tests/test_tools/test_tools_manuscript.py | 2 +- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/novelwriter/formats/toqdoc.py b/novelwriter/formats/toqdoc.py index f0f495e25..0c11168ff 100644 --- a/novelwriter/formats/toqdoc.py +++ b/novelwriter/formats/toqdoc.py @@ -30,7 +30,7 @@ from PyQt5.QtCore import QMarginsF, QSizeF from PyQt5.QtGui import ( QColor, QFont, QFontDatabase, QPageSize, QTextBlockFormat, QTextCharFormat, - QTextCursor, QTextDocument + QTextCursor, QTextDocument, QTextFrameFormat ) from PyQt5.QtPrintSupport import QPrinter @@ -432,17 +432,22 @@ def _insertFragments( def _insertNewPageMarker(self, cursor: QTextCursor) -> None: """Insert a new page marker.""" if self._newPage: - cursor.insertHtml("