Add an ignoreCase setting for autoReplace

This commit is contained in:
Veronica Berglyd Olsen
2026-11-01 18:03:19 +01:00
parent f5ca209c2b
commit 8430ac1e6c
4 changed files with 72 additions and 16 deletions

View File

@@ -61,6 +61,7 @@ class ProjectData:
"_author",
"_autoCount",
"_autoReplace",
"_autoReplaceIgnoreCase",
"_currCounts",
"_dailyGoal",
"_dailyGoalAuto",
@@ -110,6 +111,7 @@ class ProjectData:
self._rtl = False
self._spellCheck = False
self._spellLang = None
self._autoReplaceIgnoreCase = False
# Writing Target
self._targetCount = 0
@@ -285,6 +287,11 @@ class ProjectData:
"""Return the auto-replace dictionary."""
return self._autoReplace
@property
def autoReplaceIgnoreCase(self) -> bool:
"""Return True if the auto replace should ignore case."""
return self._autoReplaceIgnoreCase
@property
def itemStatus(self) -> ItemStatus:
"""Return the status settings object."""
@@ -462,8 +469,9 @@ class ProjectData:
if cNotes is not None:
self._currCounts[3] = checkInt(cNotes, 0)
def setAutoReplace(self, value: dict) -> None:
def setAutoReplace(self, value: dict, ignoreCase: Any) -> None:
"""Set the auto-replace dictionary."""
self._autoReplaceIgnoreCase = checkBool(ignoreCase, False)
if isinstance(value, dict):
self._autoReplace = {}
for key, entry in value.items():

View File

@@ -52,7 +52,7 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
FILE_VERSION = "1.6" # The current project file format version
FILE_REVISION = "0" # The current project file format revision
FILE_REVISION = "1" # The current project file format revision
HEX_VERSION = 0x0106 # The current project file format version as hex
LAST_BREAKING = 0x260200B2 # Last project format breaking change (not XML)
@@ -127,6 +127,8 @@ class ProjectXMLReader:
1.6 The actual format released for 26.2. All documents are now
stored with the .md file extension and a toml header. The
project XML was not changed.
Rev 1: Added ignoreCase attribute to autoReplace (26.3).
"""
def __init__(self, path: str | Path) -> None:
@@ -297,9 +299,9 @@ class ProjectXMLReader:
data.setInitLastHandles(self._parseDictKeyText(xItem))
elif xItem.tag == "autoReplace":
if self._version >= 0x0102:
data.setAutoReplace(self._parseDictKeyText(xItem))
data.setAutoReplace(self._parseDictKeyText(xItem), xItem.attrib.get("ignoreCase"))
else: # Pre 1.2 format
data.setAutoReplace(self._parseDictTagText(xItem))
data.setAutoReplace(self._parseDictTagText(xItem), False)
else:
logger.warning("Ignored <root/settings/%s> in XML", xItem.tag)
@@ -546,7 +548,12 @@ class ProjectXMLWriter:
self._packSingleValue(xSettings, "dailyTarget", str(data.dailyGoal), attrib=dailyAttr)
self._packListValues(xSettings, "targetSkipRoots", data.targetSkipRoots)
self._packDictKeyValue(xSettings, "lastHandle", data.lastHandle)
self._packDictKeyValue(xSettings, "autoReplace", data.autoReplace)
self._packDictKeyValue(
xSettings,
"autoReplace",
data.autoReplace,
attrib={"ignoreCase": yesNo(data.autoReplaceIgnoreCase)},
)
# Save Status/Importance
xStatus = ET.SubElement(xSettings, "status")
@@ -594,22 +601,40 @@ class ProjectXMLWriter:
# Internal Functions
##
def _packSingleValue(self, xParent: ET.Element, name: str, value: str | None, attrib: dict | None = None) -> None:
def _packSingleValue(
self,
xParent: ET.Element,
name: str,
value: str | None,
attrib: dict | None = None,
) -> None:
"""Pack a single value into an XML element."""
xItem = ET.SubElement(xParent, name, attrib=attrib or {})
xItem.text = str(value) or ""
def _packListValues(self, xParent: ET.Element, name: str, data: list | tuple | set) -> None:
def _packListValues(
self,
xParent: ET.Element,
name: str,
data: list | tuple | set,
attrib: dict | None = None,
) -> None:
"""Pack the entries of a sequence into an XML element."""
xItem = ET.SubElement(xParent, name)
xItem = ET.SubElement(xParent, name, attrib=attrib or {})
for value in data:
if len(str(value)) > 0: # pragma: no branch
xEntry = ET.SubElement(xItem, "entry")
xEntry.text = str(value) or ""
def _packDictKeyValue(self, xParent: ET.Element, name: str, data: Mapping[str, str | None]) -> None:
def _packDictKeyValue(
self,
xParent: ET.Element,
name: str,
data: Mapping[str, str | None],
attrib: dict | None = None,
) -> None:
"""Pack the entries of a dictionary into an XML element."""
xItem = ET.SubElement(xParent, name)
xItem = ET.SubElement(xParent, name, attrib=attrib or {})
for key, value in data.items():
if len(key) > 0: # pragma: no branch
xEntry = ET.SubElement(xItem, "entry", attrib={"key": key})

View File

@@ -230,9 +230,11 @@ class GuiProjectSettings(NDialog):
logger.debug("Updating importance labels")
project.updateStatus("i", self.importPage.getNewList())
autoReplaceIgnoreCase = self.replacePage.swtIgnoreCase.isChecked()
if self.replacePage.changed:
logger.debug("Updating auto-replace settings")
project.data.setAutoReplace(self.replacePage.getNewList())
project.data.setAutoReplace(self.replacePage.getNewList(), autoReplaceIgnoreCase)
self.newProjectSettingsReady.emit()
self.close()
@@ -839,6 +841,7 @@ class _ReplacePage(NFixedPage):
self._changed = False
wCol0 = SHARED.project.options.getInt("GuiProjectSettings", "replaceColW", 130)
iPx = SHARED.theme.baseIconHeight
# Title
self.pageTitle = NColorLabel(
@@ -872,6 +875,14 @@ class _ReplacePage(NFixedPage):
self.delButton = SHARED.theme.getFlatButton(nwToolButton.REMOVE, self)
self.delButton.clicked.connect(self._onEntryDeleted)
# Settings
self.swtIgnoreCase = NSwitch(self, height=iPx)
self.swtIgnoreCase.setChecked(SHARED.project.data.autoReplaceIgnoreCase)
self.swtIgnoreCase.toggled.connect(self._onIgnoreCaseToggled)
self.lblIgnoreCase = QLabel(self.tr("Ignore case for keywords"), self)
self.lblIgnoreCase.setBuddy(self.swtIgnoreCase)
# Edit Form
self.editKey = QLineEdit(self)
self.editKey.setPlaceholderText(self.tr("Select item to edit"))
@@ -890,6 +901,12 @@ class _ReplacePage(NFixedPage):
self.listControls.addWidget(self.delButton)
self.listControls.addStretch(1)
self.optsBox = QHBoxLayout()
self.optsBox.addWidget(self.lblIgnoreCase)
self.optsBox.addWidget(self.swtIgnoreCase)
self.optsBox.setSpacing(12)
self.optsBox.addStretch(1)
self.editBox = QHBoxLayout()
self.editBox.addWidget(self.editKey, 4)
self.editBox.addWidget(self.editValue, 5)
@@ -897,6 +914,7 @@ class _ReplacePage(NFixedPage):
self.mainBox = QVBoxLayout()
self.mainBox.addWidget(self.listBox, 1)
self.mainBox.addLayout(self.editBox, 0)
self.mainBox.addLayout(self.optsBox, 0)
self.innerBox = QHBoxLayout()
self.innerBox.addLayout(self.mainBox)
@@ -947,6 +965,11 @@ class _ReplacePage(NFixedPage):
item.setText(self.C_REPL, text)
self._changed = True
@pyqtSlot(bool)
def _onIgnoreCaseToggled(self, state: bool) -> None:
"""Mark the settings as changed."""
self._changed = True
@pyqtSlot()
def _onSelectionChanged(self) -> None:
"""Extract the details from the selected item and populate the

View File

@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="26.3b1" hexVersion="0x260300b1" fileVersion="1.6" fileRevision="0" timeStamp="2026-11-01 16:58:33">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2433" autoCount="437" editTime="112772" indexRevision="239">
<novelWriterXML appVersion="26.3b1" hexVersion="0x260300b1" fileVersion="1.6" fileRevision="1" timeStamp="2026-11-01 18:00:41">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2441" autoCount="437" editTime="112928" indexRevision="240">
<name>Sample Project</name>
<author>Jane Smith</author>
</project>
@@ -19,7 +19,7 @@
<entry key="novel">7031beac91f75</entry>
<entry key="story">7031beac91f75</entry>
</lastHandle>
<autoReplace>
<autoReplace ignoreCase="no">
<entry key="A">B</entry>
<entry key="B">E</entry>
<entry key="C">D</entry>
@@ -48,7 +48,7 @@
<name status="sc24b8f" import="ia857f0">Novel</name>
</item>
<item handle="53b69b83cdafc" parent="7031beac91f75" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H1" charCount="283" wordCount="50" paraCount="5" cursorPos="413" />
<meta expanded="no" heading="H1" charCount="283" wordCount="50" paraCount="5" cursorPos="79" />
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
</item>
<item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -68,7 +68,7 @@
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
</item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="548" wordCount="108" paraCount="3" cursorPos="650" />
<meta expanded="no" heading="H3" charCount="548" wordCount="108" paraCount="3" cursorPos="18" />
<name status="s90e6c9" import="ia857f0" active="yes">Another Scene</name>
</item>
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT">