Merge remote-tracking branch 'origin/unicode_fix' into unicode_fix
This commit is contained in:
@@ -1,5 +1,35 @@
|
||||
# novelWriter Changelog
|
||||
|
||||
## Version 2.7.3 [2025-07-07]
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a patch release that fixes a bug in, and makes a few improvement to, Preferences.
|
||||
|
||||
### Detailed Changelog
|
||||
|
||||
**Bugfixes**
|
||||
|
||||
* Fixes an issue where novelWriter would crash if a `%` was added to any of the free text settings
|
||||
fields in Preferences. Issue #2455. PR #2456.
|
||||
|
||||
**Improvements**
|
||||
|
||||
* The "Dialogue line symbols" setting in Preferences now has an "Add" button with a dropdown menu
|
||||
for all the symbols allowed in this field. Since virtually none of them are available on a
|
||||
regular keyboard, this makes it easier to add them and more transparent which symbols are allowed
|
||||
in the box. Issue #2453. PR #2457.
|
||||
* The switch for the "Allow open-ended dialogue" setting in Preferences has been moved up one line
|
||||
so it is clearer that it only applies to quoted dialogue, and not to the alternative dialogue
|
||||
settings. Issue #2454. PR #2457.
|
||||
|
||||
**Packaging**
|
||||
|
||||
* Package license information has been updated to also list the licenses for the new icon themes.
|
||||
Issue #2434. PRs #2435 and #2458.
|
||||
|
||||
----
|
||||
|
||||
## Version 2.7.2 [2025-06-24]
|
||||
|
||||
### Release Notes
|
||||
|
||||
@@ -699,11 +699,12 @@ class NWConfigParser(ConfigParser):
|
||||
"""Common: Adapted Config Parser
|
||||
|
||||
This is a subclass of the standard config parser that adds type safe
|
||||
helper functions, and support for lists.
|
||||
helper functions, and support for lists. It also turns off
|
||||
interpolation, which would require % symbols to be escaped (#2455).
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
super().__init__(interpolation=None)
|
||||
return
|
||||
|
||||
def rdStr(self, section: str, option: str, default: str) -> str:
|
||||
|
||||
@@ -441,9 +441,9 @@ class ProjectXMLReader:
|
||||
for xEntry in xItem:
|
||||
if xEntry.tag == "entry":
|
||||
key = xEntry.attrib.get("key", None)
|
||||
red = checkInt(xEntry.attrib.get("red", 0), 0) # Deprecated in 1.5 R6
|
||||
green = checkInt(xEntry.attrib.get("green", 0), 0) # Deprecated in 1.5 R6
|
||||
blue = checkInt(xEntry.attrib.get("blue", 0), 0) # Deprecated in 1.5 R6
|
||||
red = checkInt(xEntry.attrib.get("red", 0), 0) # Removed in 1.5 R6
|
||||
green = checkInt(xEntry.attrib.get("green", 0), 0) # Removed in 1.5 R6
|
||||
blue = checkInt(xEntry.attrib.get("blue", 0), 0) # Removed in 1.5 R6
|
||||
color = xEntry.attrib.get("color") # Added in 1.5 R6
|
||||
count = checkInt(xEntry.attrib.get("count", 0), 0)
|
||||
shape = xEntry.attrib.get("shape", "")
|
||||
|
||||
@@ -192,7 +192,8 @@ class NWStatus:
|
||||
def refreshIcons(self) -> None:
|
||||
"""Refresh all icons."""
|
||||
for entry in self._store.values():
|
||||
entry.color = SHARED.theme.parseColor(entry.theme)
|
||||
if entry.theme != CUSTOM_COL:
|
||||
entry.color = SHARED.theme.parseColor(entry.theme)
|
||||
entry.icon = NWStatus.createIcon(self._height, entry.color, entry.shape)
|
||||
return
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import logging
|
||||
from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot
|
||||
from PyQt6.QtGui import QAction, QCloseEvent, QKeyEvent, QKeySequence
|
||||
from PyQt6.QtWidgets import (
|
||||
QCompleter, QDialogButtonBox, QFileDialog, QHBoxLayout, QLineEdit,
|
||||
QCompleter, QDialogButtonBox, QFileDialog, QHBoxLayout, QLineEdit, QMenu,
|
||||
QPushButton, QVBoxLayout, QWidget
|
||||
)
|
||||
|
||||
@@ -608,6 +608,7 @@ class GuiPreferences(NDialog):
|
||||
self.sidebar.addButton(title, section)
|
||||
self.mainForm.addGroupLabel(title, section)
|
||||
|
||||
# Dialogue Quotes
|
||||
self.dialogStyle = NComboBox(self)
|
||||
self.dialogStyle.addItem(self.tr("None"), 0)
|
||||
self.dialogStyle.addItem(self.tr("Single Quotes"), 1)
|
||||
@@ -619,6 +620,15 @@ class GuiPreferences(NDialog):
|
||||
self.tr("Applies to the selected quote styles.")
|
||||
)
|
||||
|
||||
# Open-Ended Dialogue
|
||||
self.allowOpenDial = NSwitch(self)
|
||||
self.allowOpenDial.setChecked(CONFIG.allowOpenDial)
|
||||
self.mainForm.addRow(
|
||||
self.tr("Allow open-ended dialogue"), self.allowOpenDial,
|
||||
self.tr("Highlight dialogue line with no closing quote.")
|
||||
)
|
||||
|
||||
# Alternative Dialogue
|
||||
self.altDialogOpen = QLineEdit(self)
|
||||
self.altDialogOpen.setMaxLength(4)
|
||||
self.altDialogOpen.setFixedWidth(boxFixed)
|
||||
@@ -636,23 +646,30 @@ class GuiPreferences(NDialog):
|
||||
self.tr("Custom highlighting of dialogue text.")
|
||||
)
|
||||
|
||||
self.allowOpenDial = NSwitch(self)
|
||||
self.allowOpenDial.setChecked(CONFIG.allowOpenDial)
|
||||
self.mainForm.addRow(
|
||||
self.tr("Allow open-ended dialogue"), self.allowOpenDial,
|
||||
self.tr("Highlight dialogue line with no closing quote.")
|
||||
)
|
||||
# Dialogue Line
|
||||
self.mnLineSymbols = QMenu(self)
|
||||
for symbol in nwQuotes.ALLOWED:
|
||||
label = trConst(nwQuotes.SYMBOLS.get(symbol, nwQuotes.DASHES.get(symbol, "None")))
|
||||
self.mnLineSymbols.addAction(
|
||||
f"[ {symbol } ] {label}",
|
||||
lambda symbol=symbol: self._insertDialogLineSymbol(symbol)
|
||||
)
|
||||
|
||||
self.dialogLine = QLineEdit(self)
|
||||
self.dialogLine.setMaxLength(4)
|
||||
self.dialogLine.setFixedWidth(boxFixed)
|
||||
self.dialogLine.setMinimumWidth(100)
|
||||
self.dialogLine.setAlignment(QtAlignCenter)
|
||||
self.dialogLine.setText(CONFIG.dialogLine)
|
||||
self.dialogLine.setText(" ".join(CONFIG.dialogLine))
|
||||
|
||||
self.dialogLineButton = NIconToolButton(self, iSz, "add", "green")
|
||||
self.dialogLineButton.setMenu(self.mnLineSymbols)
|
||||
|
||||
self.mainForm.addRow(
|
||||
self.tr("Dialogue line symbols"), self.dialogLine,
|
||||
self.tr("Lines starting with any of these symbols are dialogue.")
|
||||
self.tr("Lines starting with any of these symbols are dialogue."),
|
||||
button=self.dialogLineButton
|
||||
)
|
||||
|
||||
# Narrator Break
|
||||
self.narratorBreak = NComboBox(self)
|
||||
self.narratorDialog = NComboBox(self)
|
||||
for key, value in nwQuotes.DASHES.items():
|
||||
@@ -672,6 +689,7 @@ class GuiPreferences(NDialog):
|
||||
self.tr("Alternates dialogue highlighting within any paragraph.")
|
||||
)
|
||||
|
||||
# Emphasis
|
||||
self.highlightEmph = NSwitch(self)
|
||||
self.highlightEmph.setChecked(CONFIG.highlightEmph)
|
||||
self.mainForm.addRow(
|
||||
@@ -679,6 +697,7 @@ class GuiPreferences(NDialog):
|
||||
self.tr("Applies to the document editor only.")
|
||||
)
|
||||
|
||||
# Additional Spaces
|
||||
self.showMultiSpaces = NSwitch(self)
|
||||
self.showMultiSpaces.setChecked(CONFIG.showMultiSpaces)
|
||||
self.mainForm.addRow(
|
||||
@@ -914,6 +933,14 @@ class GuiPreferences(NDialog):
|
||||
self.askBeforeBackup.setEnabled(state)
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _insertDialogLineSymbol(self, symbol: str) -> None:
|
||||
"""Insert a symbol in the dialogue line box."""
|
||||
current = self.dialogLine.text()
|
||||
values = processDialogSymbols(f"{current} {symbol}")
|
||||
self.dialogLine.setText(" ".join(values))
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _toggleAutoReplaceMain(self, state: bool) -> None:
|
||||
"""Toggle switches controlled by the auto replace switch."""
|
||||
|
||||
@@ -324,23 +324,55 @@ def testCoreStatus_Entries(mockGUI, mockRnd):
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreStatus_RefreshIcons(mockGUIwithTheme, mockRnd):
|
||||
"""Test refreshing the icons of the NWStatus class."""
|
||||
def testCoreStatus_RefreshIcons_Theme(mockGUIwithTheme, mockRnd):
|
||||
"""Test refreshing the icons with theme colours."""
|
||||
nStatus = NWStatus(NWStatus.STATUS)
|
||||
nStatus.add(None, "New", "default", "SQUARE", 0)
|
||||
nStatus.add(None, "Note", "red", "CIRCLE", 0)
|
||||
nStatus.add(None, "Draft", "yellow", "SQUARE", 0)
|
||||
nStatus.add(None, "Finished", "green", "SQUARE", 0)
|
||||
|
||||
beforeIcons = [nStatus[statusKeys[i]].icon for i in range(4)]
|
||||
iconsA = [nStatus[statusKeys[i]].icon for i in range(4)]
|
||||
themeA = [nStatus[statusKeys[i]].theme for i in range(4)]
|
||||
|
||||
# Refreshing the icons should generate new ones
|
||||
nStatus.refreshIcons()
|
||||
afterIcons = [nStatus[statusKeys[i]].icon for i in range(4)]
|
||||
iconsB = [nStatus[statusKeys[i]].icon for i in range(4)]
|
||||
themeB = [nStatus[statusKeys[i]].theme for i in range(4)]
|
||||
|
||||
for before, after in zip(beforeIcons, afterIcons, strict=False):
|
||||
for before, after in zip(iconsA, iconsB, strict=False):
|
||||
assert before is not after
|
||||
|
||||
assert themeA == themeB
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreStatus_RefreshIcons_Custom(mockGUIwithTheme, mockRnd):
|
||||
"""Test refreshing the icons with custom colours."""
|
||||
nStatus = NWStatus(NWStatus.STATUS)
|
||||
nStatus.add(None, "New", "#707070", "SQUARE", 0)
|
||||
nStatus.add(None, "Note", "#ff0000", "CIRCLE", 0)
|
||||
nStatus.add(None, "Draft", "#ffff00", "SQUARE", 0)
|
||||
nStatus.add(None, "Finished", "#00ff00", "SQUARE", 0)
|
||||
|
||||
iconsA = [nStatus[statusKeys[i]].icon for i in range(4)]
|
||||
themeA = [nStatus[statusKeys[i]].theme for i in range(4)]
|
||||
colorA = [nStatus[statusKeys[i]].color.getRgb() for i in range(4)]
|
||||
|
||||
# Refreshing the icons should generate new ones
|
||||
nStatus.refreshIcons()
|
||||
iconsB = [nStatus[statusKeys[i]].icon for i in range(4)]
|
||||
themeB = [nStatus[statusKeys[i]].theme for i in range(4)]
|
||||
colorB = [nStatus[statusKeys[i]].color.getRgb() for i in range(4)]
|
||||
|
||||
for before, after in zip(iconsA, iconsB, strict=False):
|
||||
assert before is not after
|
||||
|
||||
# But they should have the same colour value (#2452)
|
||||
assert themeA == [CUSTOM_COL, CUSTOM_COL, CUSTOM_COL, CUSTOM_COL]
|
||||
assert themeB == [CUSTOM_COL, CUSTOM_COL, CUSTOM_COL, CUSTOM_COL]
|
||||
assert colorA == colorB
|
||||
|
||||
|
||||
@pytest.mark.core
|
||||
def testCoreStatus_Pack(mockGUIwithTheme, mockRnd):
|
||||
|
||||
@@ -273,14 +273,17 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
|
||||
# Text Highlighting
|
||||
prefs.dialogStyle.setCurrentData(3, 0)
|
||||
prefs.allowOpenDial.setChecked(False)
|
||||
prefs.dialogLine.setText("–")
|
||||
prefs.narratorBreak.setCurrentData("–", "")
|
||||
prefs.narratorDialog.setCurrentData("–", "")
|
||||
prefs.altDialogOpen.setText("<")
|
||||
prefs.altDialogClose.setText(">")
|
||||
prefs.dialogLine.setText(nwUnicode.U_EMDASH)
|
||||
prefs.narratorBreak.setCurrentData(nwUnicode.U_EMDASH, "")
|
||||
prefs.narratorDialog.setCurrentData(nwUnicode.U_EMDASH, "")
|
||||
prefs.altDialogOpen.setText("%") # Symbol also tests for #2455
|
||||
prefs.altDialogClose.setText("%") # Symbol also tests for #2455
|
||||
prefs.highlightEmph.setChecked(False)
|
||||
prefs.showMultiSpaces.setChecked(False)
|
||||
|
||||
prefs._insertDialogLineSymbol(nwUnicode.U_ENDASH)
|
||||
assert prefs.dialogLine.text() == f"{nwUnicode.U_ENDASH} {nwUnicode.U_EMDASH}"
|
||||
|
||||
assert CONFIG.dialogStyle == 2
|
||||
assert CONFIG.allowOpenDial is True
|
||||
assert CONFIG.dialogLine == ""
|
||||
@@ -404,11 +407,11 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
|
||||
# Text Highlighting
|
||||
assert CONFIG.dialogStyle == 3
|
||||
assert CONFIG.allowOpenDial is False
|
||||
assert CONFIG.dialogLine == "–"
|
||||
assert CONFIG.narratorBreak == "–"
|
||||
assert CONFIG.narratorDialog == "–"
|
||||
assert CONFIG.altDialogOpen == "<"
|
||||
assert CONFIG.altDialogClose == ">"
|
||||
assert CONFIG.dialogLine == f"{nwUnicode.U_ENDASH}{nwUnicode.U_EMDASH}"
|
||||
assert CONFIG.narratorBreak == nwUnicode.U_EMDASH
|
||||
assert CONFIG.narratorDialog == nwUnicode.U_EMDASH
|
||||
assert CONFIG.altDialogOpen == "%"
|
||||
assert CONFIG.altDialogClose == "%"
|
||||
assert CONFIG.highlightEmph is False
|
||||
assert CONFIG.showMultiSpaces is False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user