diff --git a/novelwriter/common.py b/novelwriter/common.py index 181a79a1..44463ca6 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -279,6 +279,16 @@ def simplified(text: str) -> str: return " ".join(str(text).strip().split()) +def compact(text: str) -> str: + """Compact a string by removing spaces.""" + return "".join(str(text).split()) + + +def uniqueCompact(text: str) -> str: + """Return a unique, compact and sorted string.""" + return "".join(sorted(set(compact(text)))) + + def elide(text: str, length: int) -> str: """Elide a piece of text to a maximum length.""" if len(text) > (cut := max(4, length)): diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index f433529e..519f6b73 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -34,7 +34,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.common import describeFont +from novelwriter.common import describeFont, uniqueCompact from novelwriter.constants import nwUnicode from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.extensions.configlayout import NColourLabel, NScrollableForm @@ -952,8 +952,8 @@ class GuiPreferences(NDialog): # Text Highlighting dialogueStyle = self.dialogStyle.currentData() allowOpenDial = self.allowOpenDial.isChecked() - narratorBreak = self.narratorBreak.text() - dialogueLine = self.dialogLine.text() + narratorBreak = self.narratorBreak.text().strip() + dialogueLine = self.dialogLine.text().strip() altDialogOpen = self.altDialogOpen.text() altDialogClose = self.altDialogClose.text() highlightEmph = self.highlightEmph.isChecked() @@ -983,8 +983,8 @@ class GuiPreferences(NDialog): CONFIG.doReplaceDQuote = self.doReplaceDQuote.isChecked() CONFIG.doReplaceDash = self.doReplaceDash.isChecked() CONFIG.doReplaceDots = self.doReplaceDots.isChecked() - CONFIG.fmtPadBefore = self.fmtPadBefore.text().strip() - CONFIG.fmtPadAfter = self.fmtPadAfter.text().strip() + CONFIG.fmtPadBefore = uniqueCompact(self.fmtPadBefore.text()) + CONFIG.fmtPadAfter = uniqueCompact(self.fmtPadAfter.text()) CONFIG.fmtPadThin = self.fmtPadThin.isChecked() # Quotation Style diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 8e25e25d..bae89b43 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -526,8 +526,14 @@ class GuiMain(QMainWindow): self.novelView.setActiveHandle(None) return - def openDocument(self, tHandle: str | None, tLine: int | None = None, - changeFocus: bool = True, doScroll: bool = False) -> bool: + def openDocument( + self, + tHandle: str | None, + tLine: int | None = None, + sTitle: str | None = None, + changeFocus: bool = True, + doScroll: bool = False + ) -> bool: """Open a specific document, optionally at a given line.""" if not SHARED.hasProject: logger.error("No project open") @@ -537,9 +543,12 @@ class GuiMain(QMainWindow): logger.debug("Requested item '%s' is not a document", tHandle) return False + if sTitle and tLine is None: + if hItem := SHARED.project.index.getItemHeading(tHandle, sTitle): + tLine = hItem.line + self._changeView(nwView.EDITOR) - cHandle = self.docEditor.docHandle - if cHandle == tHandle: + if tHandle == self.docEditor.docHandle: self.docEditor.setCursorLine(tLine) if changeFocus: self.docEditor.setFocus() @@ -711,7 +720,6 @@ class GuiMain(QMainWindow): if SHARED.hasProject: tHandle = None sTitle = None - tLine = None if self.projView.treeHasFocus(): tHandle = self.projView.getSelectedHandle() elif self.novelView.treeHasFocus(): @@ -722,11 +730,8 @@ class GuiMain(QMainWindow): logger.warning("No item selected") return - if tHandle and sTitle: - if hItem := SHARED.project.index.getItemHeading(tHandle, sTitle): - tLine = hItem.line if tHandle: - self.openDocument(tHandle, tLine=tLine, changeFocus=False, doScroll=False) + self.openDocument(tHandle, sTitle=sTitle, changeFocus=False, doScroll=False) return @@ -1118,7 +1123,7 @@ class GuiMain(QMainWindow): tHandle, sTitle = self._getTagSource(tag) if tHandle is not None: if mode == nwDocMode.EDIT: - self.openDocument(tHandle) + self.openDocument(tHandle, sTitle=sTitle) elif mode == nwDocMode.VIEW: self.viewDocument(tHandle=tHandle, sTitle=sTitle) return @@ -1137,11 +1142,7 @@ class GuiMain(QMainWindow): """Handle an open document request.""" if tHandle is not None: if mode == nwDocMode.EDIT: - tLine = None - hItem = SHARED.project.index.getItemHeading(tHandle, sTitle) - if hItem is not None: - tLine = hItem.line - self.openDocument(tHandle, tLine=tLine, changeFocus=setFocus) + self.openDocument(tHandle, sTitle=sTitle, changeFocus=setFocus) elif mode == nwDocMode.VIEW: self.viewDocument(tHandle=tHandle, sTitle=sTitle) return diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index b46007b5..faf48dd3 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -32,12 +32,12 @@ from PyQt5.QtGui import QColor, QDesktopServices, QFontDatabase from novelwriter.common import ( NWConfigParser, checkBool, checkFloat, checkInt, checkIntTuple, checkPath, - checkString, checkStringNone, checkUuid, cssCol, describeFont, elide, - formatFileFilter, formatInt, formatTime, formatTimeStamp, formatVersion, - fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, isItemLayout, - isItemType, isListInstance, isTitleTag, jsonEncode, makeFileNameSafe, - minmax, numberToRoman, openExternalPath, readTextFile, simplified, - transferCase, xmlIndent, yesNo + checkString, checkStringNone, checkUuid, compact, cssCol, describeFont, + elide, formatFileFilter, formatInt, formatTime, formatTimeStamp, + formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, + isItemLayout, isItemType, isListInstance, isTitleTag, jsonEncode, + makeFileNameSafe, minmax, numberToRoman, openExternalPath, readTextFile, + simplified, transferCase, uniqueCompact, xmlIndent, yesNo ) from tests.mocked import causeOSError @@ -346,6 +346,27 @@ def testBaseCommon_simplified(): assert simplified("\tHello\n\r\tWorld") == "Hello World" +@pytest.mark.base +def testBaseCommon_compact(): + """Test the compact function.""" + assert compact("! ! !") == "!!!" + assert compact("1\t2\t3") == "123" + assert compact("1\n2\n3") == "123" + assert compact("1\r2\r3") == "123" + assert compact("1\u00a02\u00a03") == "123" + + +@pytest.mark.base +def testBaseCommon_uniqueCompact(): + """Test the uniqueCompact function.""" + assert uniqueCompact("! ! !") == "!" + assert uniqueCompact("1\t2\t3") == "123" + assert uniqueCompact("1\n2\n3") == "123" + assert uniqueCompact("1\r2\r3") == "123" + assert uniqueCompact("1\u00a02\u00a03") == "123" + assert uniqueCompact("3 2 1") == "123" + + @pytest.mark.base def testBaseCommon_elide(): """Test the elide function.""" diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index 1d186d83..036621df 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -379,7 +379,7 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, tstPaths): assert CONFIG.doReplaceDQuote is False assert CONFIG.doReplaceDash is False assert CONFIG.doReplaceDots is False - assert CONFIG.fmtPadBefore == "!?:" + assert CONFIG.fmtPadBefore == "!:?" assert CONFIG.fmtPadAfter == "¡¿" assert CONFIG.fmtPadThin is True diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 80c3cf57..b0b3c321 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -39,6 +39,7 @@ from novelwriter.gui.outline import GuiOutlineView from novelwriter.gui.projtree import GuiProjectTree from novelwriter.tools.welcome import GuiWelcome +from tests.mocked import causeOSError from tests.tools import NWD_IGNORE, XML_IGNORE, C, buildTestProject, cmpFiles KEY_DELAY = 1 @@ -651,7 +652,7 @@ def testGuiMain_Viewing(qtbot, monkeypatch, nwGUI, projPath, mockRnd): @pytest.mark.gui -def testGuiMain_Features(qtbot, nwGUI, projPath, mockRnd): +def testGuiMain_Features(qtbot, monkeypatch, nwGUI, projPath, mockRnd): """Test various features of the main window.""" buildTestProject(nwGUI, projPath) assert SHARED.focusMode is False @@ -672,6 +673,7 @@ def testGuiMain_Features(qtbot, nwGUI, projPath, mockRnd): # Enable focus mode nwGUI.toggleFocusMode() + assert SHARED.focusMode is True assert nwGUI.treePane.isVisible() is False assert nwGUI.mainStatus.isVisible() is False assert nwGUI.mainMenu.isVisible() is False @@ -680,12 +682,19 @@ def testGuiMain_Features(qtbot, nwGUI, projPath, mockRnd): # Disable focus mode nwGUI.toggleFocusMode() + assert SHARED.focusMode is False assert nwGUI.treePane.isVisible() is True assert nwGUI.mainStatus.isVisible() is True assert nwGUI.mainMenu.isVisible() is True assert nwGUI.sideBar.isVisible() is True assert nwGUI.splitView.isVisible() is True + # Closing editor disables focus mode + nwGUI.toggleFocusMode() + assert SHARED.focusMode is True + nwGUI.closeDocument() + assert SHARED.focusMode is False + # Full Screen Mode # ================ @@ -702,6 +711,17 @@ def testGuiMain_Features(qtbot, nwGUI, projPath, mockRnd): nwGUI.sideBar.mSettings.show() nwGUI.sideBar.mSettings.hide() + # Document Open Errors + # ==================== + + # Cannot edit a folder + assert nwGUI.openDocument(C.hChapterDir) is False + + # Handle I/O error + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + assert nwGUI.openDocument(C.hChapterDoc) is False + # qtbot.stop()