From 2c48e02558e7096e4847968c6da32d0f38ac252a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 11 Jun 2020 22:52:11 +0200 Subject: [PATCH 01/72] Bumped dev branch to version 0.9rc1 --- CHANGELOG.md | 3 +++ docs/source/conf.py | 4 ++-- nw/__init__.py | 4 ++-- setup.py | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ea7f124..fb40548e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # novelWriter ChangeLog +## Version 0.9rc1 [2020-xx-xx] + + ## Version 0.8 [2020-xx-xx] **User Interface** diff --git a/docs/source/conf.py b/docs/source/conf.py index 6c122457..ddaebd44 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -24,9 +24,9 @@ copyright = "2018-2020, Veronica Berglyd Olsen" author = "Veronica Berglyd Olsen" # The short X.Y version -version = "0.8.0" +version = "0.9" # The full version, including alpha/beta/rc tags -release = "0.8.0rc1" +release = "0.9.0rc1" # -- General configuration --------------------------------------------------- diff --git a/nw/__init__.py b/nw/__init__.py index 98436cca..24d3a261 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -40,8 +40,8 @@ __package__ = "novelWriter" __author__ = "Veronica Berglyd Olsen" __copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen" __license__ = "GPLv3" -__version__ = "0.8.0rc1" -__hexversion__ = "0x000800c1" +__version__ = "0.9.0rc1" +__hexversion__ = "0x000900c1" __date__ = "2020-06-01" __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" diff --git a/setup.py b/setup.py index 137ae661..b8246fd5 100755 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ with open("README.md", "r") as inFile: setuptools.setup( name = "novelWriter", - version = "0.8rc1", + version = "0.9rc1", author = "Veronica Berglyd Olsen", author_email = "code@vkbo.net", description = "A markdown-like document editor for writing novels", From 8e38637250e6de403ff809643e47314e0b586417 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Jun 2020 17:07:55 +0200 Subject: [PATCH 02/72] No ned to explicitly declare sets --- nw/core/index.py | 4 ++-- nw/core/options.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/nw/core/index.py b/nw/core/index.py index 456b415c..632375a7 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -41,7 +41,7 @@ logger = logging.getLogger(__name__) class NWIndex(): - VALID_KEYS = set([ + VALID_KEYS = { nwKeyWords.TAG_KEY, nwKeyWords.PLOT_KEY, nwKeyWords.POV_KEY, @@ -51,7 +51,7 @@ class NWIndex(): nwKeyWords.OBJECT_KEY, nwKeyWords.ENTITY_KEY, nwKeyWords.CUSTOM_KEY - ]) + } TAG_CLASS = { nwKeyWords.CHAR_KEY : nwItemClass.CHARACTER, nwKeyWords.POV_KEY : nwItemClass.CHARACTER, diff --git a/nw/core/options.py b/nw/core/options.py index abab6a65..25bf1fb4 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -45,7 +45,7 @@ class OptionState(): self.theState = {} self.validMap = { - "GuiSession": set([ + "GuiSession": { "widthCol0", "widthCol1", "widthCol2", @@ -53,11 +53,11 @@ class OptionState(): "sortOrder", "hideZeros", "hideNegative", - ]), - "GuiDocSplit": set([ + }, + "GuiDocSplit": { "spLevel", - ]), - "GuiBuildNovel": set([ + }, + "GuiBuildNovel": { "winWidth", "winHeight", "addNovel", @@ -72,12 +72,12 @@ class OptionState(): "incComments", "incKeywords", "incBodyText", - ]), - "GuiOutline": set([ + }, + "GuiOutline": { "headerOrder", "columnWidth", "columnHidden", - ]), + }, } return From 3fc17c543984402867298508a26a76e5804285e8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Jun 2020 17:08:18 +0200 Subject: [PATCH 03/72] Save more meta data to the top of files --- nw/core/document.py | 34 +++++++++++++++++++++++++++++++--- nw/core/project.py | 6 +++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/nw/core/document.py b/nw/core/document.py index 52f1517b..b9d3de83 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -30,8 +30,10 @@ import nw from os import path, mkdir, rename, unlink +from nw.core.item import NWItem from nw.constants import nwAlert from nw.common import isHandle +from nw.constants import nwItemLayout, nwItemClass logger = logging.getLogger(__name__) @@ -139,8 +141,18 @@ class NWDoc(): docPath = path.join(self.theProject.projContent, docFile) docTemp = path.join(self.theProject.projContent, docFile+"~") - itemPath = self.theProject.projTree.getItemPath(self.docHandle) - docMeta = "%%~ "+":".join(itemPath)+":"+self.theItem.itemName+"\n" + if isinstance(self.theItem, NWItem): + itemPath = self.theProject.projTree.getItemPath(self.docHandle) + docMeta = ( + "%%~ {handlepath:s}:{itemclass:s}:{itemlayout:s}:{itemname:s}\n" + ).format( + handlepath = ":".join(itemPath), + itemclass = self.theItem.itemClass.name, + itemlayout = self.theItem.itemLayout.name, + itemname = self.theItem.itemName, + ) + else: + docMeta = "" try: with open(docTemp, mode="w", encoding="utf8") as outFile: @@ -213,6 +225,22 @@ class NWDoc(): else: break - return theMeta, thePath + theClass = nwItemClass.NO_CLASS + theLayout = nwItemLayout.NO_LAYOUT + theData = theMeta.split(":") + + if (n := len(theData)) > 1: + if theData[0] in nwItemClass.__members__: + theClass = nwItemClass[theData[0]] + if n > 2: + if theData[1] in nwItemLayout.__members__: + theLayout = nwItemLayout[theData[1]] + theName = theData[2] + else: + theName = theData[1] + else: + theName = theData[0] + + return theName, thePath, theClass, theLayout # END Class NWDoc diff --git a/nw/core/project.py b/nw/core/project.py index 0784fc7a..53286656 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1075,7 +1075,7 @@ class NWProject(): # Look for meta data oName = "" if aDoc.openDocument(oHandle, showStatus=False, isOrphan=True): - oName, oPath = aDoc.getMeta() + oName, oPath, oClass, oLayout = aDoc.getMeta() if oName == "": nOrph += 1 @@ -1084,8 +1084,8 @@ class NWProject(): orphItem = NWItem(self) orphItem.setName(oName) orphItem.setType(nwItemType.FILE) - orphItem.setClass(nwItemClass.NO_CLASS) - orphItem.setLayout(nwItemLayout.NO_LAYOUT) + orphItem.setClass(oClass) + orphItem.setLayout(oLayout) self.projTree.append(oHandle, None, orphItem) return From 66c8931ecef9f82a0fc400fa5664145f820e0ac0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Jun 2020 17:22:32 +0200 Subject: [PATCH 04/72] Updated sample project --- sample/content/14298de4d9524.nwd | 2 +- sample/content/53b69b83cdafc.nwd | 2 +- sample/content/5eaea4e8cdee8.nwd | 2 +- sample/content/636b6aa9b697b.nwd | 2 +- sample/content/6a2d6d5f4f401.nwd | 2 +- sample/content/88706ddc78b1b.nwd | 2 +- sample/content/96b68994dfa3d.nwd | 2 +- sample/content/974e400180a99.nwd | 2 +- sample/content/ae7339df26ded.nwd | 2 +- sample/content/b3e74dbc1f584.nwd | 2 +- sample/content/b8136a5a774a0.nwd | 2 +- sample/content/ba8a28a246524.nwd | 2 +- sample/content/bb2c23b3c42cc.nwd | 2 +- sample/content/bc0cbd2a407f3.nwd | 2 +- sample/content/edca4be2fcaf8.nwd | 2 +- sample/content/f1471bef9f2ae.nwd | 2 +- sample/nwProject.nwx | 20 ++++++++++---------- 17 files changed, 26 insertions(+), 26 deletions(-) diff --git a/sample/content/14298de4d9524.nwd b/sample/content/14298de4d9524.nwd index 106f640e..af6c4e50 100644 --- a/sample/content/14298de4d9524.nwd +++ b/sample/content/14298de4d9524.nwd @@ -1,4 +1,4 @@ -%%~ 14298de4d9524:f7e2d9f330615:f6622b4617424:John Smith +%%~ 14298de4d9524:f7e2d9f330615:f6622b4617424:CHARACTER:NOTE:John Smith # John Smith @tag: John diff --git a/sample/content/53b69b83cdafc.nwd b/sample/content/53b69b83cdafc.nwd index 51c7c8d9..b25ce403 100644 --- a/sample/content/53b69b83cdafc.nwd +++ b/sample/content/53b69b83cdafc.nwd @@ -1,4 +1,4 @@ -%%~ 53b69b83cdafc:7031beac91f75:Title Page +%%~ 53b69b83cdafc:7031beac91f75:NOVEL:TITLE:Title Page # My Novel **By Jane Doh** diff --git a/sample/content/5eaea4e8cdee8.nwd b/sample/content/5eaea4e8cdee8.nwd index ac8ce5dd..ba951a2b 100644 --- a/sample/content/5eaea4e8cdee8.nwd +++ b/sample/content/5eaea4e8cdee8.nwd @@ -1,4 +1,4 @@ -%%~ 5eaea4e8cdee8:15c4492bd5107:Mars +%%~ 5eaea4e8cdee8:15c4492bd5107:WORLD:NOTE:Mars # Mars @tag: Mars diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index ea69cd21..e93bbb57 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -1,4 +1,4 @@ -%%~ 636b6aa9b697b:e7ded148d6e4a:7031beac91f75:Making a Scene +%%~ 636b6aa9b697b:e7ded148d6e4a:7031beac91f75:NOVEL:SCENE:Making a Scene ### Making a Scene @pov: Jane diff --git a/sample/content/6a2d6d5f4f401.nwd b/sample/content/6a2d6d5f4f401.nwd index 5e5d8566..583f503f 100644 --- a/sample/content/6a2d6d5f4f401.nwd +++ b/sample/content/6a2d6d5f4f401.nwd @@ -1,4 +1,4 @@ -%%~ 6a2d6d5f4f401:e7ded148d6e4a:7031beac91f75:Chapter One +%%~ 6a2d6d5f4f401:e7ded148d6e4a:7031beac91f75:NOVEL:CHAPTER:Chapter One ## So it Begins @pov: Jane diff --git a/sample/content/88706ddc78b1b.nwd b/sample/content/88706ddc78b1b.nwd index 94b563cf..4f9a8d43 100644 --- a/sample/content/88706ddc78b1b.nwd +++ b/sample/content/88706ddc78b1b.nwd @@ -1,4 +1,4 @@ -%%~ 88706ddc78b1b:e7ded148d6e4a:7031beac91f75:Chapter Two +%%~ 88706ddc78b1b:e7ded148d6e4a:7031beac91f75:NOVEL:CHAPTER:Chapter Two ## Where has John Gone? @pov: Jane diff --git a/sample/content/96b68994dfa3d.nwd b/sample/content/96b68994dfa3d.nwd index 29818dc1..6ab833fd 100644 --- a/sample/content/96b68994dfa3d.nwd +++ b/sample/content/96b68994dfa3d.nwd @@ -1,4 +1,4 @@ -%%~ 96b68994dfa3d:e7ded148d6e4a:7031beac91f75:A Note on Structure +%%~ 96b68994dfa3d:e7ded148d6e4a:7031beac91f75:NOVEL:NOTE:A Note on Structure # A Note on Structure This file is just a note. You can save notes anywhere you like in the project tree. Notes can be filtered out when you export the project. diff --git a/sample/content/974e400180a99.nwd b/sample/content/974e400180a99.nwd index 303d5232..07ca7817 100644 --- a/sample/content/974e400180a99.nwd +++ b/sample/content/974e400180a99.nwd @@ -1,4 +1,4 @@ -%%~ 974e400180a99:7031beac91f75:Page +%%~ 974e400180a99:7031beac91f75:NOVEL:PAGE:Page This is a plain page with some text on it. This file should receive no special formatting, but the text will always be left aligned and the content will always start on a fresh page when the project is exported. diff --git a/sample/content/ae7339df26ded.nwd b/sample/content/ae7339df26ded.nwd index 05ed60d5..ea4bb9ce 100644 --- a/sample/content/ae7339df26ded.nwd +++ b/sample/content/ae7339df26ded.nwd @@ -1,4 +1,4 @@ -%%~ ae7339df26ded:e7ded148d6e4a:7031beac91f75:We Found John! +%%~ ae7339df26ded:e7ded148d6e4a:7031beac91f75:NOVEL:SCENE:We Found John! ### We Found John! @pov: John diff --git a/sample/content/b3e74dbc1f584.nwd b/sample/content/b3e74dbc1f584.nwd index 3f81b275..a7a8ba8a 100644 --- a/sample/content/b3e74dbc1f584.nwd +++ b/sample/content/b3e74dbc1f584.nwd @@ -1,4 +1,4 @@ -%%~ b3e74dbc1f584:15c4492bd5107:Earth +%%~ b3e74dbc1f584:15c4492bd5107:WORLD:NOTE:Earth # Earth @tag: Earth diff --git a/sample/content/b8136a5a774a0.nwd b/sample/content/b8136a5a774a0.nwd index 4e63373d..8c6ddb77 100644 --- a/sample/content/b8136a5a774a0.nwd +++ b/sample/content/b8136a5a774a0.nwd @@ -1,4 +1,4 @@ -%%~ b8136a5a774a0:98acd8c76c93a:Delete Me! +%%~ b8136a5a774a0:98acd8c76c93a:NOVEL:SCENE:Delete Me! ### Delete Me! This scene is trash. \ No newline at end of file diff --git a/sample/content/ba8a28a246524.nwd b/sample/content/ba8a28a246524.nwd index b30cb63b..8cc5ae6c 100644 --- a/sample/content/ba8a28a246524.nwd +++ b/sample/content/ba8a28a246524.nwd @@ -1,4 +1,4 @@ -%%~ ba8a28a246524:e7ded148d6e4a:7031beac91f75:Interlude +%%~ ba8a28a246524:e7ded148d6e4a:7031beac91f75:NOVEL:UNNUMBERED:Interlude ## Interlude % Notice that this is a file with the flag ‘N.Un’. The ‘N’ means it’s a novel file, and the ‘Un’ means it’s an unnumbered chapter. Unnumbered chapters can be treated separately from numbered chapters during export. Perfect for when you want to add an interlude, or for a prologue or epilogue. diff --git a/sample/content/bb2c23b3c42cc.nwd b/sample/content/bb2c23b3c42cc.nwd index 869118d7..126f549b 100644 --- a/sample/content/bb2c23b3c42cc.nwd +++ b/sample/content/bb2c23b3c42cc.nwd @@ -1,4 +1,4 @@ -%%~ bb2c23b3c42cc:f7e2d9f330615:f6622b4617424:Jane Smith +%%~ bb2c23b3c42cc:f7e2d9f330615:f6622b4617424:CHARACTER:NOTE:Jane Smith # Jane Smith @tag: Jane diff --git a/sample/content/bc0cbd2a407f3.nwd b/sample/content/bc0cbd2a407f3.nwd index 1fe75e50..94b036c0 100644 --- a/sample/content/bc0cbd2a407f3.nwd +++ b/sample/content/bc0cbd2a407f3.nwd @@ -1,4 +1,4 @@ -%%~ bc0cbd2a407f3:e7ded148d6e4a:7031beac91f75:Another Scene +%%~ bc0cbd2a407f3:e7ded148d6e4a:7031beac91f75:NOVEL:SCENE:Another Scene ### Another Scene @pov: John diff --git a/sample/content/edca4be2fcaf8.nwd b/sample/content/edca4be2fcaf8.nwd index 679e4e27..0200ab75 100644 --- a/sample/content/edca4be2fcaf8.nwd +++ b/sample/content/edca4be2fcaf8.nwd @@ -1,4 +1,4 @@ -%%~ edca4be2fcaf8:7031beac91f75:Part 1 +%%~ edca4be2fcaf8:7031beac91f75:NOVEL:PARTITION:Part One # Part One The first part. \ No newline at end of file diff --git a/sample/content/f1471bef9f2ae.nwd b/sample/content/f1471bef9f2ae.nwd index 9b2d9368..32db9cb0 100644 --- a/sample/content/f1471bef9f2ae.nwd +++ b/sample/content/f1471bef9f2ae.nwd @@ -1,4 +1,4 @@ -%%~ f1471bef9f2ae:15c4492bd5107:Space +%%~ f1471bef9f2ae:15c4492bd5107:WORLD:NOTE:Space # Space @tag: Space diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 88ce6d2f..cee9b2fa 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,21 +1,21 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 273 - 40 - 4125 + 280 + 41 + 4312 False True True - 636b6aa9b697b + bc0cbd2a407f3 b3e74dbc1f584 - 914 + 920 B E @@ -117,7 +117,7 @@ 1199 216 7 - 1266 + 950 Another Scene @@ -274,9 +274,9 @@ New True SCENE - 0 - 0 - 0 + 30 + 6 + 1 36 From ad0ccf8a568690e31a26c1672933f43736d9992d Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Jun 2020 17:22:48 +0200 Subject: [PATCH 05/72] Fixed return of no meta data --- nw/core/document.py | 2 +- nw/core/project.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/nw/core/document.py b/nw/core/document.py index b9d3de83..835ecdba 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -206,7 +206,7 @@ class NWDoc(): """ if len(self.docMeta) < 14: # Not enough information - return "", [] + return "", [], None, None theMeta = self.docMeta diff --git a/nw/core/project.py b/nw/core/project.py index 53286656..3db93bf2 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1081,6 +1081,11 @@ class NWProject(): nOrph += 1 oName = "Orphaned File %d" % nOrph + if oClass is None: + oClass = nwItemClass.NO_CLASS + if oLayout is None: + oLayout = nwItemLayout.NO_LAYOUT + orphItem = NWItem(self) orphItem.setName(oName) orphItem.setType(nwItemType.FILE) From 06ac3bdaba4252c5f22da9292321161ed8312af1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Jun 2020 17:22:57 +0200 Subject: [PATCH 06/72] Fixed tests --- tests/reference/gui/1_0e17daca5f3e1.nwd | 2 +- tests/reference/gui/1_1a6562590ef19.nwd | 2 +- tests/reference/gui/1_31489056e0916.nwd | 2 +- tests/reference/gui/1_98010bd9270f9.nwd | 2 +- tests/reference/gui/4_73475cb40a568.nwd | 2 +- tests/reference/gui/5_031b4af5197ec.nwd | 2 +- tests/reference/gui/5_25fc0e7096fc6.nwd | 2 +- tests/reference/gui/5_2858dcd1057d3.nwd | 2 +- tests/reference/gui/5_2fca346db6561.nwd | 2 +- tests/reference/gui/5_31489056e0916.nwd | 2 +- tests/reference/gui/5_41cfc0d1f2d12.nwd | 2 +- tests/reference/gui/5_98010bd9270f9.nwd | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/reference/gui/1_0e17daca5f3e1.nwd b/tests/reference/gui/1_0e17daca5f3e1.nwd index 0d9e8ec4..ecfbf8ff 100644 --- a/tests/reference/gui/1_0e17daca5f3e1.nwd +++ b/tests/reference/gui/1_0e17daca5f3e1.nwd @@ -1,4 +1,4 @@ -%%~ 0e17daca5f3e1:71ee45a3c0db9:New File +%%~ 0e17daca5f3e1:71ee45a3c0db9:PLOT:NOTE:New File # Main Plot @tag: MainPlot diff --git a/tests/reference/gui/1_1a6562590ef19.nwd b/tests/reference/gui/1_1a6562590ef19.nwd index def80bfd..7c6d19a7 100644 --- a/tests/reference/gui/1_1a6562590ef19.nwd +++ b/tests/reference/gui/1_1a6562590ef19.nwd @@ -1,4 +1,4 @@ -%%~ 1a6562590ef19:811786ad1ae74:New File +%%~ 1a6562590ef19:811786ad1ae74:WORLD:NOTE:New File # Main Location @tag: Home diff --git a/tests/reference/gui/1_31489056e0916.nwd b/tests/reference/gui/1_31489056e0916.nwd index fba0c438..0a238e45 100644 --- a/tests/reference/gui/1_31489056e0916.nwd +++ b/tests/reference/gui/1_31489056e0916.nwd @@ -1,4 +1,4 @@ -%%~ 31489056e0916:25fc0e7096fc6:73475cb40a568:New Scene +%%~ 31489056e0916:25fc0e7096fc6:73475cb40a568:NOVEL:SCENE:New Scene # Novel ## Chapter diff --git a/tests/reference/gui/1_98010bd9270f9.nwd b/tests/reference/gui/1_98010bd9270f9.nwd index 3b0477ca..632f53cd 100644 --- a/tests/reference/gui/1_98010bd9270f9.nwd +++ b/tests/reference/gui/1_98010bd9270f9.nwd @@ -1,4 +1,4 @@ -%%~ 98010bd9270f9:44cb730c42048:New File +%%~ 98010bd9270f9:44cb730c42048:CHARACTER:NOTE:New File # Jane Doe @tag: Jane diff --git a/tests/reference/gui/4_73475cb40a568.nwd b/tests/reference/gui/4_73475cb40a568.nwd index cb8cf6ba..5d18b581 100644 --- a/tests/reference/gui/4_73475cb40a568.nwd +++ b/tests/reference/gui/4_73475cb40a568.nwd @@ -1,4 +1,4 @@ -%%~ 73475cb40a568:b3643d0f92e32:Chapter One +%%~ 73475cb40a568:b3643d0f92e32:NOVEL:SCENE:Chapter One ## Chapter One @pov: Bod diff --git a/tests/reference/gui/5_031b4af5197ec.nwd b/tests/reference/gui/5_031b4af5197ec.nwd index 8ef13aab..bc9d9f7d 100644 --- a/tests/reference/gui/5_031b4af5197ec.nwd +++ b/tests/reference/gui/5_031b4af5197ec.nwd @@ -1,4 +1,4 @@ -%%~ 031b4af5197ec:0e17daca5f3e1:b3643d0f92e32:Scene One +%%~ 031b4af5197ec:0e17daca5f3e1:b3643d0f92e32:NOVEL:SCENE:Scene One ### Scene One @pov: Bod diff --git a/tests/reference/gui/5_25fc0e7096fc6.nwd b/tests/reference/gui/5_25fc0e7096fc6.nwd index eb32f417..d84c563a 100644 --- a/tests/reference/gui/5_25fc0e7096fc6.nwd +++ b/tests/reference/gui/5_25fc0e7096fc6.nwd @@ -1,4 +1,4 @@ -%%~ 25fc0e7096fc6:811786ad1ae74:b3643d0f92e32:Chapter One +%%~ 25fc0e7096fc6:811786ad1ae74:b3643d0f92e32:NOVEL:CHAPTER:Chapter One ## Chapter One @pov: Bod diff --git a/tests/reference/gui/5_2858dcd1057d3.nwd b/tests/reference/gui/5_2858dcd1057d3.nwd index 3c3297f0..198fd30a 100644 --- a/tests/reference/gui/5_2858dcd1057d3.nwd +++ b/tests/reference/gui/5_2858dcd1057d3.nwd @@ -1,4 +1,4 @@ -%%~ 2858dcd1057d3:0e17daca5f3e1:b3643d0f92e32:Scene Two +%%~ 2858dcd1057d3:0e17daca5f3e1:b3643d0f92e32:NOVEL:SCENE:Scene Two ### Scene Two @pov: Bod diff --git a/tests/reference/gui/5_2fca346db6561.nwd b/tests/reference/gui/5_2fca346db6561.nwd index 538fa126..93d611ff 100644 --- a/tests/reference/gui/5_2fca346db6561.nwd +++ b/tests/reference/gui/5_2fca346db6561.nwd @@ -1,4 +1,4 @@ -%%~ 2fca346db6561:0e17daca5f3e1:b3643d0f92e32:Scene Two, Section Two +%%~ 2fca346db6561:0e17daca5f3e1:b3643d0f92e32:NOVEL:SCENE:Scene Two, Section Two #### Scene Two, Section Two Suspendisse potenti. Fusce tempus lorem nec laoreet suscipit. Fusce vulputate nisl ac diam tincidunt, nec malesuada quam pellentesque. Maecenas congue, tellus quis commodo rutrum, magna leo egestas arcu, quis suscipit ex risus id ligula. Suspendisse potenti. Morbi blandit lacus vitae laoreet vulputate. Donec vitae tellus eleifend, lobortis eros eu, tincidunt enim. Nullam et ullamcorper nisi. Vivamus tellus ex, lobortis quis rutrum ut, dapibus sit amet turpis. Phasellus pellentesque metus diam, commodo tristique ante commodo ac. Ut mollis ipsum nec diam blandit sollicitudin. Duis bibendum lacus nec commodo dapibus. Sed condimentum luctus ante, id ultricies urna varius nec. Nam convallis magna nec bibendum ultrices. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed auctor pharetra quam, vitae porta ex bibendum eu. diff --git a/tests/reference/gui/5_31489056e0916.nwd b/tests/reference/gui/5_31489056e0916.nwd index 53799705..7c26cdb5 100644 --- a/tests/reference/gui/5_31489056e0916.nwd +++ b/tests/reference/gui/5_31489056e0916.nwd @@ -1,4 +1,4 @@ -%%~ 31489056e0916:811786ad1ae74:b3643d0f92e32:Scene One +%%~ 31489056e0916:811786ad1ae74:b3643d0f92e32:NOVEL:SCENE:Scene One ### Scene One @pov: Bod diff --git a/tests/reference/gui/5_41cfc0d1f2d12.nwd b/tests/reference/gui/5_41cfc0d1f2d12.nwd index 1860ad52..a39fc1c1 100644 --- a/tests/reference/gui/5_41cfc0d1f2d12.nwd +++ b/tests/reference/gui/5_41cfc0d1f2d12.nwd @@ -1,4 +1,4 @@ -%%~ 41cfc0d1f2d12:0e17daca5f3e1:b3643d0f92e32:Scene One, Section Two +%%~ 41cfc0d1f2d12:0e17daca5f3e1:b3643d0f92e32:NOVEL:SCENE:Scene One, Section Two #### Scene One, Section Two Integer vel libero ipsum. Donec varius aliquam libero, sit amet commodo urna hendrerit non. Nullam quis erat mollis nunc viverra volutpat tincidunt in odio. Nam vitae quam sem. Aliquam suscipit nulla non lorem pharetra semper. Ut suscipit erat eu ligula accumsan ultrices. Phasellus nisl tellus, placerat sed laoreet id, consectetur nec dolor. Sed fringilla ipsum id dapibus posuere. Aenean finibus pharetra tincidunt. Ut molestie malesuada nulla, id posuere lorem tincidunt eu. Aliquam tempor eros a est vulputate, scelerisque pulvinar ipsum fermentum. In hac habitasse platea dictumst. diff --git a/tests/reference/gui/5_98010bd9270f9.nwd b/tests/reference/gui/5_98010bd9270f9.nwd index 116a3fb3..4afe14ea 100644 --- a/tests/reference/gui/5_98010bd9270f9.nwd +++ b/tests/reference/gui/5_98010bd9270f9.nwd @@ -1,4 +1,4 @@ -%%~ 98010bd9270f9:811786ad1ae74:b3643d0f92e32:Scene Two +%%~ 98010bd9270f9:811786ad1ae74:b3643d0f92e32:NOVEL:SCENE:Scene Two ### Scene Two @pov: Bod From 5358bf5e6b7b341aeca22bf13792950de3a7159f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Jun 2020 17:45:27 +0200 Subject: [PATCH 07/72] Trying another method of parsing the meta string --- nw/core/document.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/nw/core/document.py b/nw/core/document.py index 835ecdba..560815e6 100644 --- a/nw/core/document.py +++ b/nw/core/document.py @@ -226,21 +226,17 @@ class NWDoc(): break theClass = nwItemClass.NO_CLASS + for aClass in nwItemClass: + if theMeta.startswith(aClass.name): + theClass = aClass + theMeta = theMeta.lstrip(aClass.name+":") + theLayout = nwItemLayout.NO_LAYOUT - theData = theMeta.split(":") + for aLayout in nwItemLayout: + if theMeta.startswith(aLayout.name): + theLayout = aLayout + theMeta = theMeta.lstrip(aLayout.name+":") - if (n := len(theData)) > 1: - if theData[0] in nwItemClass.__members__: - theClass = nwItemClass[theData[0]] - if n > 2: - if theData[1] in nwItemLayout.__members__: - theLayout = nwItemLayout[theData[1]] - theName = theData[2] - else: - theName = theData[1] - else: - theName = theData[0] - - return theName, thePath, theClass, theLayout + return theMeta, thePath, theClass, theLayout # END Class NWDoc From f7363c8465f0852e126ede7063ec48a0d2f01c01 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Jun 2020 17:51:04 +0200 Subject: [PATCH 08/72] Added PR #308 to yhe changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb40548e..ece5d486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Version 0.9rc1 [2020-xx-xx] +**Other Changes** + +* Added the file's class and layout to the meta data string added as the first line of saved document files. This meta data is only used to restore the file meta information into the project if it was lost from the project file. It is also useful information when reading the file in external tools. PR #308. + ## Version 0.8 [2020-xx-xx] From 19474dbc5e9b82ff5f2a0005b049f8515f3980f7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Jun 2020 20:10:00 +0200 Subject: [PATCH 09/72] Pop a dialog box before removing an item from the recent entries list --- nw/gui/projload.py | 62 +++++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/nw/gui/projload.py b/nw/gui/projload.py index 06a32f72..3fabfde5 100644 --- a/nw/gui/projload.py +++ b/nw/gui/projload.py @@ -36,7 +36,7 @@ from PyQt5.QtGui import QKeySequence from PyQt5.QtWidgets import ( QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QTreeWidget, QAbstractItemView, QTreeWidgetItem, QDialogButtonBox, QLabel, QShortcut, - QFileDialog, QLineEdit + QFileDialog, QLineEdit, QMessageBox ) from nw.common import formatInt @@ -50,6 +50,10 @@ class GuiProjectLoad(QDialog): NEW_STATE = 1 OPEN_STATE = 2 + C_NAME = 0 + C_COUNT = 1 + C_TIME = 2 + def __init__(self, theParent): QDialog.__init__(self, theParent) @@ -92,8 +96,8 @@ class GuiProjectLoad(QDialog): self.listBox.setIconSize(QSize(iPx, iPx)) treeHead = self.listBox.headerItem() - treeHead.setTextAlignment(1, Qt.AlignRight) - treeHead.setTextAlignment(2, Qt.AlignRight) + treeHead.setTextAlignment(self.C_COUNT, Qt.AlignRight) + treeHead.setTextAlignment(self.C_TIME, Qt.AlignRight) self.lblRecent = QLabel("Recently Opened Projects") self.lblPath = QLabel("Path") @@ -150,7 +154,7 @@ class GuiProjectLoad(QDialog): self._saveDialogState() selItems = self.listBox.selectedItems() if selItems: - self.openPath = selItems[0].data(0, Qt.UserRole) + self.openPath = selItems[0].data(self.C_NAME, Qt.UserRole) self.openState = self.OPEN_STATE self.accept() else: @@ -163,7 +167,7 @@ class GuiProjectLoad(QDialog): """ selList = self.listBox.selectedItems() if selList: - self.selPath.setText(selList[0].data(0, Qt.UserRole)) + self.selPath.setText(selList[0].data(self.C_NAME, Qt.UserRole)) return def _doBrowse(self): @@ -210,8 +214,23 @@ class GuiProjectLoad(QDialog): """ selList = self.listBox.selectedItems() if selList: - self.mainConf.removeFromRecentCache(selList[0].text(3)) - self._populateList() + doRemove = False + if self.mainConf.showGUI: + msgBox = QMessageBox() + msgRes = msgBox.question( + self, "Remove Entry", + "Remove the selected entry from the recent projects list?" + ) + doRemove = (msgRes == QMessageBox.Yes) + else: + doRemove = True + + if doRemove: + self.mainConf.removeFromRecentCache( + selList[0].data(self.C_NAME, Qt.UserRole) + ) + self._populateList() + return ## @@ -221,9 +240,10 @@ class GuiProjectLoad(QDialog): def _saveDialogState(self): """Save the changes made to the dialog. """ - colWidths = [50]*3 - for i in range(3): - colWidths[i] = self.listBox.columnWidth(i) + colWidths = [0, 0, 0] + colWidths[self.C_NAME] = self.listBox.columnWidth(self.C_NAME) + colWidths[self.C_COUNT] = self.listBox.columnWidth(self.C_COUNT) + colWidths[self.C_TIME] = self.listBox.columnWidth(self.C_TIME) self.mainConf.setProjColWidths(colWidths) return @@ -251,22 +271,24 @@ class GuiProjectLoad(QDialog): hasSelection = False for timeStamp in sorted(listOrder, reverse=True): newItem = QTreeWidgetItem([""]*4) - newItem.setIcon(0, self.theParent.theTheme.getIcon("proj_nwx")) - newItem.setText(0, listData[timeStamp][0]) - newItem.setData(0, Qt.UserRole, listData[timeStamp][2]) - newItem.setText(1, formatInt(listData[timeStamp][1])) - newItem.setText(2, datetime.fromtimestamp(timeStamp).strftime("%x %X")) - newItem.setTextAlignment(0, Qt.AlignLeft | Qt.AlignVCenter) - newItem.setTextAlignment(1, Qt.AlignRight | Qt.AlignVCenter) - newItem.setTextAlignment(2, Qt.AlignRight | Qt.AlignVCenter) + newItem.setIcon(self.C_NAME, self.theParent.theTheme.getIcon("proj_nwx")) + newItem.setText(self.C_NAME, listData[timeStamp][0]) + newItem.setData(self.C_NAME, Qt.UserRole, listData[timeStamp][2]) + newItem.setText(self.C_COUNT, formatInt(listData[timeStamp][1])) + newItem.setText(self.C_TIME, datetime.fromtimestamp(timeStamp).strftime("%x %X")) + newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter) + newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter) + newItem.setTextAlignment(self.C_TIME, Qt.AlignRight | Qt.AlignVCenter) self.listBox.addTopLevelItem(newItem) if not hasSelection: newItem.setSelected(True) hasSelection = True projColWidth = self.mainConf.getProjColWidths() - for i in range(3): - self.listBox.setColumnWidth(i, projColWidth[i]) + if len(projColWidth) == 3: + self.listBox.setColumnWidth(self.C_NAME, projColWidth[self.C_NAME]) + self.listBox.setColumnWidth(self.C_COUNT, projColWidth[self.C_COUNT]) + self.listBox.setColumnWidth(self.C_TIME, projColWidth[self.C_TIME]) return From ee303491e48a6ab9afebd1f6f5edf77da6f06b6f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Jun 2020 20:18:58 +0200 Subject: [PATCH 10/72] Only one standard icon size --- nw/gui/itemdetails.py | 4 ++-- nw/gui/outline.py | 2 +- nw/gui/projload.py | 6 +++--- nw/gui/projsettings.py | 2 +- nw/gui/statusbar.py | 2 +- nw/gui/theme.py | 2 -- 6 files changed, 8 insertions(+), 10 deletions(-) diff --git a/nw/gui/itemdetails.py b/nw/gui/itemdetails.py index 182fb3fd..4d0f5641 100644 --- a/nw/gui/itemdetails.py +++ b/nw/gui/itemdetails.py @@ -56,8 +56,8 @@ class GuiItemDetails(QWidget): self.setLayout(self.mainBox) self.pS = 0.9*self.theTheme.fontPointSize - self.iPx = self.theTheme.textIconSize - self.sPx = int(round(0.8*self.theTheme.textIconSize)) + self.iPx = self.theTheme.baseIconSize + self.sPx = int(round(0.8*self.theTheme.baseIconSize)) self.expCheck = self.theTheme.getPixmap("check", (self.iPx, self.iPx)) self.expCross = self.theTheme.getPixmap("cross", (self.iPx, self.iPx)) diff --git a/nw/gui/outline.py b/nw/gui/outline.py index b2c45f86..1b1aa11c 100644 --- a/nw/gui/outline.py +++ b/nw/gui/outline.py @@ -102,7 +102,7 @@ class GuiOutline(QTreeWidget): self.itemDoubleClicked.connect(self._treeDoubleClick) self.itemSelectionChanged.connect(self._itemSelected) - iPx = self.theTheme.textIconSize + iPx = self.theTheme.baseIconSize self.setIconSize(QSize(iPx, iPx)) self.setIndentation(iPx) diff --git a/nw/gui/projload.py b/nw/gui/projload.py index 3fabfde5..738a493c 100644 --- a/nw/gui/projload.py +++ b/nw/gui/projload.py @@ -66,7 +66,8 @@ class GuiProjectLoad(QDialog): self.openPath = None sPx = self.mainConf.pxInt(16) - iPx = self.mainConf.pxInt(96) + nPx = self.mainConf.pxInt(96) + iPx = self.theTheme.baseIconSize self.outerBox = QVBoxLayout() self.innerBox = QHBoxLayout() @@ -78,13 +79,12 @@ class GuiProjectLoad(QDialog): self.setMinimumHeight(self.mainConf.pxInt(400)) self.setModal(True) - self.guiDeco = self.theTheme.loadDecoration("nwicon", (iPx, iPx)) + self.guiDeco = self.theTheme.loadDecoration("nwicon", (nPx, nPx)) self.innerBox.addWidget(self.guiDeco, 0, Qt.AlignTop) self.projectForm = QGridLayout() self.projectForm.setContentsMargins(0, 0, 0, 0) - iPx = self.theTheme.textIconSize self.listBox = QTreeWidget() self.listBox.setSelectionMode(QAbstractItemView.SingleSelection) self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop) diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index 11b6a014..0afaefbf 100644 --- a/nw/gui/projsettings.py +++ b/nw/gui/projsettings.py @@ -282,7 +282,7 @@ class GuiProjectEditStatus(QWidget): self.colChanged = False self.selColour = None - self.iPx = self.theTheme.textIconSize + self.iPx = self.theTheme.baseIconSize self.outerBox = QVBoxLayout() self.mainBox = QHBoxLayout() diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py index a40e4022..c851a218 100644 --- a/nw/gui/statusbar.py +++ b/nw/gui/statusbar.py @@ -61,7 +61,7 @@ class GuiMainStatus(QStatusBar): colTrue = QColor(*self.theTheme.statUnsaved) colFalse = QColor(*self.theTheme.statSaved) - iPx = self.theTheme.textIconSize + iPx = self.theTheme.baseIconSize # Permanent Widgets # ================= diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 98c9a7b9..30bcb621 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -143,7 +143,6 @@ class GuiTheme: self.fontPointSize = self.guiFont.pointSizeF() self.fontPixelSize = int(round(qMetric.height())) self.baseIconSize = int(round(qMetric.ascent())) - self.textIconSize = int(round(qMetric.ascent() + qMetric.leading())) self.textNHeight = qMetric.boundingRect("N").height() self.textNWidth = qMetric.boundingRect("N").width() @@ -151,7 +150,6 @@ class GuiTheme: logger.verbose("GUI Font Point Size: %.2f" % self.fontPointSize) logger.verbose("GUI Font Pixel Size: %d" % self.fontPixelSize) logger.verbose("GUI Base Icon Size: %d" % self.baseIconSize) - logger.verbose("GUI Text Icon Size: %d" % self.textIconSize) logger.verbose("Text 'N' Height: %d" % self.textNHeight) logger.verbose("Text 'N' Width: %d" % self.textNWidth) From 3c8cf6829a008ef215f296565fad1cb8e1e0c679 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Jun 2020 20:28:54 +0200 Subject: [PATCH 11/72] Added changelog entry for PR #309 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ece5d486..8c3a2448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Version 0.9rc1 [2020-xx-xx] +**User Interface** + +* The Open Project dialog will now ask before removing an entry from the recent projects list. PR #309. + **Other Changes** * Added the file's class and layout to the meta data string added as the first line of saved document files. This meta data is only used to restore the file meta information into the project if it was lost from the project file. It is also useful information when reading the file in external tools. PR #308. From 1f434718ecc6f4c82bd736b0f18ffe38bf93fc85 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Jun 2020 22:00:14 +0200 Subject: [PATCH 12/72] Replaced underline format with bold-italic, and all formats now use star, not underscore --- nw/constants/enum.py | 48 ++++++++++++------------ nw/gui/doceditor.py | 89 ++++++++++++++++++++++++++++++++++++++------ nw/gui/mainmenu.py | 24 ++++++------ nw/guimain.py | 4 +- 4 files changed, 116 insertions(+), 49 deletions(-) diff --git a/nw/constants/enum.py b/nw/constants/enum.py index ee33c575..605cc022 100644 --- a/nw/constants/enum.py +++ b/nw/constants/enum.py @@ -68,30 +68,30 @@ class nwItemLayout(Enum): class nwDocAction(Enum): - NO_ACTION = 0 - UNDO = 1 - REDO = 2 - CUT = 3 - COPY = 4 - PASTE = 5 - BOLD = 6 - ITALIC = 7 - U_LINE = 8 - S_QUOTE = 9 - D_QUOTE = 10 - SEL_ALL = 11 - SEL_PARA = 12 - FIND = 13 - REPLACE = 14 - GO_NEXT = 15 - GO_PREV = 16 - REPL_NEXT = 17 - BLOCK_H1 = 18 - BLOCK_H2 = 19 - BLOCK_H3 = 20 - BLOCK_H4 = 21 - BLOCK_COM = 22 - BLOCK_TXT = 23 + NO_ACTION = 0 + UNDO = 1 + REDO = 2 + CUT = 3 + COPY = 4 + PASTE = 5 + ITALIC = 6 + BOLD = 7 + BOLDITALIC = 8 + S_QUOTE = 9 + D_QUOTE = 10 + SEL_ALL = 11 + SEL_PARA = 12 + FIND = 13 + REPLACE = 14 + GO_NEXT = 15 + GO_PREV = 16 + REPL_NEXT = 17 + BLOCK_H1 = 18 + BLOCK_H2 = 19 + BLOCK_H3 = 20 + BLOCK_H4 = 21 + BLOCK_COM = 22 + BLOCK_TXT = 23 # END Enum nwDocAction diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index bd5da37f..e77d89c2 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -509,16 +509,16 @@ class GuiDocEditor(QTextEdit): self.copy() elif theAction == nwDocAction.PASTE: self.paste() - elif theAction == nwDocAction.BOLD: - self._wrapSelection("**","**") elif theAction == nwDocAction.ITALIC: - self._wrapSelection("_","_") - elif theAction == nwDocAction.U_LINE: - self._wrapSelection("__","__") + self._toggleEmph(1) + elif theAction == nwDocAction.BOLD: + self._toggleEmph(2) + elif theAction == nwDocAction.BOLDITALIC: + self._toggleEmph(3) elif theAction == nwDocAction.S_QUOTE: - self._wrapSelection(self.typSQOpen,self.typSQClose) + self._wrapSelection(self.typSQOpen, self.typSQClose) elif theAction == nwDocAction.D_QUOTE: - self._wrapSelection(self.typDQOpen,self.typDQClose) + self._wrapSelection(self.typDQOpen, self.typDQClose) elif theAction == nwDocAction.SEL_ALL: self._makeSelection(QTextCursor.Document) elif theAction == nwDocAction.SEL_PARA: @@ -881,18 +881,20 @@ class GuiDocEditor(QTextEdit): self.bigDoc = False return - def _wrapSelection(self, tBefore, tAfter): + def _wrapSelection(self, tBefore, tAfter=None): """Wraps the selected text in whatever is in tBefore and tAfter. If there is no selection, the autoSelect setting decides the action. AutoSelect will select the word under the cursor before wrapping it. If this feature is disabled, nothing is done. """ - theCursor = self.textCursor() - if self.mainConf.autoSelect and not theCursor.hasSelection(): - theCursor.select(QTextCursor.WordUnderCursor) + if tAfter is None: + tAfter = tBefore + + theCursor = self._autoSelect() if theCursor.hasSelection(): posS = theCursor.selectionStart() posE = theCursor.selectionEnd() + theCursor.clearSelection() theCursor.beginEditBlock() theCursor.setPosition(posE) @@ -900,10 +902,75 @@ class GuiDocEditor(QTextEdit): theCursor.setPosition(posS) theCursor.insertText(tBefore) theCursor.endEditBlock() + + theCursor.setPosition(posE + len(tBefore)) + theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, posE-posS) + self.setTextCursor(theCursor) + else: logger.warning("No selection made, nothing to do") return + def _clearSurrounding(self, theCursor, nChars): + """Clears n characters before and after the cursor. + """ + if theCursor.hasSelection(): + posS = theCursor.selectionStart() + posE = theCursor.selectionEnd() + theCursor.clearSelection() + theCursor.beginEditBlock() + theCursor.setPosition(posS) + for i in range(nChars): + theCursor.deletePreviousChar() + theCursor.setPosition(posE) + for i in range(nChars): + theCursor.deletePreviousChar() + theCursor.endEditBlock() + theCursor.clearSelection() + else: + logger.warning("No selection made, nothing to do") + return + + def _autoSelect(self): + """Returns a cursor which may or may not have a selection based + on user settings and document action. + """ + theCursor = self.textCursor() + if self.mainConf.autoSelect and not theCursor.hasSelection(): + theCursor.select(QTextCursor.WordUnderCursor) + self.setTextCursor(theCursor) + return theCursor + + def _toggleEmph(self, eLevel): + """Toggle emphasis of a given level. + """ + theCursor = self._autoSelect() + if theCursor.hasSelection(): + posS = theCursor.selectionStart() + posE = theCursor.selectionEnd() + + numB = 0 + for n in range(4): + if self.qDocument.characterAt(posS-n-1) == "*": + numB += 1 + else: + break + + numA = 0 + for n in range(4): + if self.qDocument.characterAt(posE+n) == "*": + numA += 1 + else: + break + + cLevel = min(numB, numA) + if cLevel == eLevel: + self._clearSurrounding(theCursor, eLevel) + else: + self._wrapSelection("*"*(eLevel - cLevel)) + + return + def _formatBlock(self, docAction): """Changes the block format of the block under the cursor. """ diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index ece05a58..086261d1 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -515,13 +515,6 @@ class GuiMainMenu(QMenuBar): # Format self.fmtMenu = self.addMenu("&Format") - # Format > Bold Text - self.aFmtBold = QAction("Bold Text", self) - self.aFmtBold.setStatusTip("Make selected text bold") - self.aFmtBold.setShortcut("Ctrl+B") - self.aFmtBold.triggered.connect(lambda: self._docAction(nwDocAction.BOLD)) - self.fmtMenu.addAction(self.aFmtBold) - # Format > Italic Text self.aFmtItalic = QAction("Italic Text", self) self.aFmtItalic.setStatusTip("Make selected text italic") @@ -529,12 +522,19 @@ class GuiMainMenu(QMenuBar): self.aFmtItalic.triggered.connect(lambda: self._docAction(nwDocAction.ITALIC)) self.fmtMenu.addAction(self.aFmtItalic) + # Format > Bold Text + self.aFmtBold = QAction("Bold Text", self) + self.aFmtBold.setStatusTip("Make selected text bold") + self.aFmtBold.setShortcut("Ctrl+B") + self.aFmtBold.triggered.connect(lambda: self._docAction(nwDocAction.BOLD)) + self.fmtMenu.addAction(self.aFmtBold) + # Format > Underline Text - self.aFmtULine = QAction("Underline Text", self) - self.aFmtULine.setStatusTip("Underline selected text") - self.aFmtULine.setShortcut("Ctrl+U") - self.aFmtULine.triggered.connect(lambda: self._docAction(nwDocAction.U_LINE)) - self.fmtMenu.addAction(self.aFmtULine) + self.aFmtBoldIt = QAction("Bold Italic Text", self) + self.aFmtBoldIt.setStatusTip("Make selected text bold and italic") + self.aFmtBoldIt.setShortcut("Ctrl+Shift+B") + self.aFmtBoldIt.triggered.connect(lambda: self._docAction(nwDocAction.BOLDITALIC)) + self.fmtMenu.addAction(self.aFmtBoldIt) # Edit > Separator self.fmtMenu.addSeparator() diff --git a/nw/guimain.py b/nw/guimain.py index 576cd990..cb2f19a2 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -961,9 +961,9 @@ class GuiMain(QMainWindow): self.addAction(self.mainMenu.aEditPaste) self.addAction(self.mainMenu.aSelectAll) self.addAction(self.mainMenu.aSelectPar) - self.addAction(self.mainMenu.aFmtBold) self.addAction(self.mainMenu.aFmtItalic) - self.addAction(self.mainMenu.aFmtULine) + self.addAction(self.mainMenu.aFmtBold) + self.addAction(self.mainMenu.aFmtBoldIt) self.addAction(self.mainMenu.aFmtDQuote) self.addAction(self.mainMenu.aFmtSQuote) self.addAction(self.mainMenu.aFmtHead1) From 013b737452728ef40320f149fb008bc950ffa816 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Jun 2020 22:47:13 +0200 Subject: [PATCH 13/72] Updated syntax highlighter --- nw/gui/dochighlight.py | 62 +++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index afd5a77c..812aca06 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -98,28 +98,28 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.colTrail.setAlpha(64) self.hStyles = { - "header1" : self._makeFormat(self.colHead, "bold",1.8), - "header2" : self._makeFormat(self.colHead, "bold",1.6), - "header3" : self._makeFormat(self.colHead, "bold",1.4), - "header4" : self._makeFormat(self.colHead, "bold",1.2), - "header1h" : self._makeFormat(self.colHeadH,"bold",1.8), - "header2h" : self._makeFormat(self.colHeadH,"bold",1.6), - "header3h" : self._makeFormat(self.colHeadH,"bold",1.4), - "header4h" : self._makeFormat(self.colHeadH,"bold",1.2), - "bold" : self._makeFormat(self.colEmph, "bold"), - "italic" : self._makeFormat(self.colEmph, "italic"), - "strike" : self._makeFormat(self.colEmph, "strike"), - "underline" : self._makeFormat(self.colEmph, "underline"), - "trailing" : self._makeFormat(self.colTrail,"background"), - "nobreak" : self._makeFormat(self.colTrail,"background"), - "dialogue1" : self._makeFormat(self.colDialN), - "dialogue2" : self._makeFormat(self.colDialD), - "dialogue3" : self._makeFormat(self.colDialS), - "replace" : self._makeFormat(self.colRepTag), - "hidden" : self._makeFormat(self.colComm), - "keyword" : self._makeFormat(self.colKey), - "modifier" : self._makeFormat(self.colMod), - "value" : self._makeFormat(self.colVal), + "header1" : self._makeFormat(self.colHead, "bold", 1.8), + "header2" : self._makeFormat(self.colHead, "bold", 1.6), + "header3" : self._makeFormat(self.colHead, "bold", 1.4), + "header4" : self._makeFormat(self.colHead, "bold", 1.2), + "header1h" : self._makeFormat(self.colHeadH, "bold", 1.8), + "header2h" : self._makeFormat(self.colHeadH, "bold", 1.6), + "header3h" : self._makeFormat(self.colHeadH, "bold", 1.4), + "header4h" : self._makeFormat(self.colHeadH, "bold", 1.2), + "bold" : self._makeFormat(self.colEmph, "bold"), + "italic" : self._makeFormat(self.colEmph, "italic"), + "strike" : self._makeFormat(self.colEmph, "strike"), + "bolditalic" : self._makeFormat(self.colEmph, ("bold","italic")), + "trailing" : self._makeFormat(self.colTrail, "background"), + "nobreak" : self._makeFormat(self.colTrail, "background"), + "dialogue1" : self._makeFormat(self.colDialN), + "dialogue2" : self._makeFormat(self.colDialD), + "dialogue3" : self._makeFormat(self.colDialS), + "replace" : self._makeFormat(self.colRepTag), + "hidden" : self._makeFormat(self.colComm), + "keyword" : self._makeFormat(self.colKey), + "modifier" : self._makeFormat(self.colMod), + "value" : self._makeFormat(self.colVal, "underline"), } self.hRules = [] @@ -139,6 +139,13 @@ class GuiDocHighlighter(QSyntaxHighlighter): )) # Markdown + self.hRules.append(( + r"(? Date: Fri, 12 Jun 2020 22:47:54 +0200 Subject: [PATCH 14/72] Improved the emphasis function for alternating between levels --- nw/gui/doceditor.py | 39 ++++++++++++++++++++++++++++---- sample/content/636b6aa9b697b.nwd | 2 +- sample/content/bc0cbd2a407f3.nwd | 3 --- sample/nwProject.nwx | 22 +++++++++--------- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index e77d89c2..26e64c82 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -942,7 +942,28 @@ class GuiDocEditor(QTextEdit): return theCursor def _toggleEmph(self, eLevel): - """Toggle emphasis of a given level. + """Toggle emphasis of a given level between 1 and 3, where 1 is + italic, 2 is bold, and 3 is bold italic. The current level in + the text is cLevel. The rules are as follows: + + cLevel | eLevel | Result + ============+============+============ + None | Italic | Italic + None | Bold | Bold + None | BoldItalic | BoldItalic + ------------+------------+------------ + Italic | Italic | None + Italic | Bold | BoldItalic + Italic | BoldItalic | BoldItalic + ------------+------------+------------ + Bold | Italic | BoldItalic + Bold | Bold | None + Bold | BoldItalic | BoldItalic + ------------+------------+------------ + BoldItalic | Italic | Bold + BoldItalic | Bold | Italic + BoldItalic | BoldItalic | None + """ theCursor = self._autoSelect() if theCursor.hasSelection(): @@ -950,24 +971,32 @@ class GuiDocEditor(QTextEdit): posE = theCursor.selectionEnd() numB = 0 - for n in range(4): + for n in range(3): if self.qDocument.characterAt(posS-n-1) == "*": numB += 1 else: break numA = 0 - for n in range(4): + for n in range(3): if self.qDocument.characterAt(posE+n) == "*": numA += 1 else: break cLevel = min(numB, numA) - if cLevel == eLevel: + if cLevel == 0: + # Has no emphasis, set to desired level + self._wrapSelection("*"*eLevel) + elif cLevel == 3: + # Has max, so reduce by desired level self._clearSurrounding(theCursor, eLevel) + elif cLevel == eLevel: + # Toggle mode, so clear what we had set + self._clearSurrounding(theCursor, cLevel) else: - self._wrapSelection("*"*(eLevel - cLevel)) + # Already at 1 or 2, increase to 3 + self._wrapSelection("*"*(3 - cLevel)) return diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index e93bbb57..7b2d92c6 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -7,7 +7,7 @@ A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree. -Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and __underscore__. +Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, *italic* and ***bold italic***. In addition, the editor supports automatic formatting of “quotes”, both double and ‘single’. Depending on the syntax highlighter, these can be in different colours. diff --git a/sample/content/bc0cbd2a407f3.nwd b/sample/content/bc0cbd2a407f3.nwd index 94b036c0..4a099dfc 100644 --- a/sample/content/bc0cbd2a407f3.nwd +++ b/sample/content/bc0cbd2a407f3.nwd @@ -14,6 +14,3 @@ In fact, if you wish, you can add all the scenes in the chapter file too. All no @location: Earth This is a second scene in the same file as the previous scene. You can always split the files up later. - - - diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index cee9b2fa..0a34f9ff 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,21 +1,21 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 280 - 41 - 4312 + 313 + 47 + 5934 False True True - bc0cbd2a407f3 - b3e74dbc1f584 - 920 + 636b6aa9b697b + 14298de4d9524 + 921 B E @@ -114,10 +114,10 @@ 1st Draft True SCENE - 1199 - 216 + 1202 + 217 7 - 950 + 469 Another Scene @@ -129,7 +129,7 @@ 476 93 3 - 551 + 428 Interlude From 8adda0c4a98af2c014d27c5ea7664fcbe5db23fe Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 13 Jun 2020 00:14:34 +0200 Subject: [PATCH 15/72] Fix regexes to be exclusive for 1, 2 and 3 stars --- nw/core/tohtml.py | 4 ++-- nw/core/tokenizer.py | 14 +++++++------- nw/gui/dochighlight.py | 6 +++--- sample/nwProject.nwx | 12 ++++++------ 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 61541f42..f58c1a7a 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -121,8 +121,8 @@ class ToHtml(Tokenizer): self.FMT_B_E : "", self.FMT_I_B : "", self.FMT_I_E : "", - self.FMT_U_B : "", - self.FMT_U_E : "", + self.FMT_S_B : "", + self.FMT_S_E : "", } if self.isNovel and self.genMode != self.M_PREVIEW: diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 70599d59..1335ed23 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -44,8 +44,8 @@ class Tokenizer(): FMT_B_E = 2 # End bold FMT_I_B = 3 # Begin italics FMT_I_E = 4 # End italics - FMT_U_B = 5 # Begin underline - FMT_U_E = 6 # End underline + FMT_S_B = 5 # Begin bold italic + FMT_S_E = 6 # End bold italic T_EMPTY = 1 # Empty line (new paragraph) T_SYNOPSIS = 2 # Synopsis comment @@ -300,14 +300,14 @@ class Tokenizer(): # RegExes for adding formatting tags within text lines # Keep in sync with the DocHighlighter class rxFormats = [( - QRegularExpression(r"(? - + Sample Project Sample Project Jane Smith Jay Doh - 313 - 47 - 5934 + 319 + 49 + 9675 False True True 636b6aa9b697b - 14298de4d9524 + 636b6aa9b697b 921 B @@ -117,7 +117,7 @@ 1202 217 7 - 469 + 248 Another Scene From 6951b3d2fa49ff891042544416d691954835a307 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 13 Jun 2020 00:39:57 +0200 Subject: [PATCH 16/72] Fixed tests --- tests/lipsum/content/04468803b92e1.nwd | 2 +- tests/lipsum/content/2426c6f0ca922.nwd | 2 +- tests/lipsum/content/441420a886d82.nwd | 2 +- tests/lipsum/content/47666c91c7ccf.nwd | 2 +- tests/lipsum/content/4c4f28287af27.nwd | 2 +- tests/lipsum/content/7a992350f3eb6.nwd | 2 +- tests/lipsum/content/846352075de7d.nwd | 2 +- tests/lipsum/content/88243afbe5ed8.nwd | 2 +- tests/lipsum/content/88d59a277361b.nwd | 4 ++-- tests/lipsum/content/8c58a65414c23.nwd | 2 +- tests/lipsum/content/db7e733775d4d.nwd | 2 +- tests/lipsum/content/eb103bc70c90c.nwd | 2 +- tests/lipsum/content/f8c0562e50f1b.nwd | 2 +- tests/lipsum/content/f96ec11c6a3da.nwd | 2 +- tests/lipsum/content/fb609cd8319dc.nwd | 2 +- tests/lipsum/nwProject.nwx | 28 +++++++++++++------------- tests/reference/build/1_LoremIpsum.nwd | 2 +- tests/reference/build/2_LoremIpsum.nwd | 2 +- 18 files changed, 32 insertions(+), 32 deletions(-) diff --git a/tests/lipsum/content/04468803b92e1.nwd b/tests/lipsum/content/04468803b92e1.nwd index 0f9e6a1f..af7504dc 100644 --- a/tests/lipsum/content/04468803b92e1.nwd +++ b/tests/lipsum/content/04468803b92e1.nwd @@ -1,4 +1,4 @@ -%%~ 04468803b92e1:60bdf227455cc:Ancient Europe +%%~ 04468803b92e1:60bdf227455cc:WORLD:NOTE:Ancient Europe # Ancient Europe @tag: Europe diff --git a/tests/lipsum/content/2426c6f0ca922.nwd b/tests/lipsum/content/2426c6f0ca922.nwd index bd573995..98ea99c3 100644 --- a/tests/lipsum/content/2426c6f0ca922.nwd +++ b/tests/lipsum/content/2426c6f0ca922.nwd @@ -1,4 +1,4 @@ -%%~ 2426c6f0ca922:6c6afb1247750:Main +%%~ 2426c6f0ca922:6c6afb1247750:PLOT:NOTE:Main # Main Plot @tag: Main diff --git a/tests/lipsum/content/441420a886d82.nwd b/tests/lipsum/content/441420a886d82.nwd index 02eb2fef..820862ba 100644 --- a/tests/lipsum/content/441420a886d82.nwd +++ b/tests/lipsum/content/441420a886d82.nwd @@ -1,4 +1,4 @@ -%%~ 441420a886d82:6bd935d2490cd:b3643d0f92e32:Chapter Two +%%~ 441420a886d82:6bd935d2490cd:b3643d0f92e32:NOVEL:CHAPTER:Chapter Two ## Chapter Two @pov: Bod diff --git a/tests/lipsum/content/47666c91c7ccf.nwd b/tests/lipsum/content/47666c91c7ccf.nwd index 1a173bb7..027b20e5 100644 --- a/tests/lipsum/content/47666c91c7ccf.nwd +++ b/tests/lipsum/content/47666c91c7ccf.nwd @@ -1,4 +1,4 @@ -%%~ 47666c91c7ccf:6bd935d2490cd:b3643d0f92e32:Scene Five +%%~ 47666c91c7ccf:6bd935d2490cd:b3643d0f92e32:NOVEL:SCENE:Scene Five ### Scene Five @pov: Bod diff --git a/tests/lipsum/content/4c4f28287af27.nwd b/tests/lipsum/content/4c4f28287af27.nwd index 9d42bd6b..50f646b5 100644 --- a/tests/lipsum/content/4c4f28287af27.nwd +++ b/tests/lipsum/content/4c4f28287af27.nwd @@ -1,4 +1,4 @@ -%%~ 4c4f28287af27:67a8707f2f249:Mr. Nobody +%%~ 4c4f28287af27:67a8707f2f249:CHARACTER:NOTE:Mr. Nobody # Nobody Owens @tag: Bod diff --git a/tests/lipsum/content/7a992350f3eb6.nwd b/tests/lipsum/content/7a992350f3eb6.nwd index a6a7299d..ab7eb619 100644 --- a/tests/lipsum/content/7a992350f3eb6.nwd +++ b/tests/lipsum/content/7a992350f3eb6.nwd @@ -1,4 +1,4 @@ -%%~ 7a992350f3eb6:b3643d0f92e32:Lorem Ipusm +%%~ 7a992350f3eb6:b3643d0f92e32:NOVEL:TITLE:Lorem Ipusm # Lorem Ipsum **By lipsum.com** diff --git a/tests/lipsum/content/846352075de7d.nwd b/tests/lipsum/content/846352075de7d.nwd index e1d4f542..e7dd7e45 100644 --- a/tests/lipsum/content/846352075de7d.nwd +++ b/tests/lipsum/content/846352075de7d.nwd @@ -1,4 +1,4 @@ -%%~ 846352075de7d:b3643d0f92e32:Interlude +%%~ 846352075de7d:b3643d0f92e32:NOVEL:BOOK:Interlude ## Why do we use it? % Exctracted from the lipsum.com website. diff --git a/tests/lipsum/content/88243afbe5ed8.nwd b/tests/lipsum/content/88243afbe5ed8.nwd index 528b8e77..eba37bae 100644 --- a/tests/lipsum/content/88243afbe5ed8.nwd +++ b/tests/lipsum/content/88243afbe5ed8.nwd @@ -1,4 +1,4 @@ -%%~ 88243afbe5ed8:45e6b01ca35c1:b3643d0f92e32:Scene One +%%~ 88243afbe5ed8:45e6b01ca35c1:b3643d0f92e32:NOVEL:SCENE:Scene One ### Scene One @pov: Bod diff --git a/tests/lipsum/content/88d59a277361b.nwd b/tests/lipsum/content/88d59a277361b.nwd index db2169a5..a67f49f1 100644 --- a/tests/lipsum/content/88d59a277361b.nwd +++ b/tests/lipsum/content/88d59a277361b.nwd @@ -1,6 +1,6 @@ -%%~ 88d59a277361b:b3643d0f92e32:Prologue +%%~ 88d59a277361b:b3643d0f92e32:NOVEL:UNNUMBERED:Prologue ## Prologue % Synopsis:Explanation from the lipsum.com website. -_Lorem Ipsum_ is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. +*Lorem Ipsum* is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. diff --git a/tests/lipsum/content/8c58a65414c23.nwd b/tests/lipsum/content/8c58a65414c23.nwd index ad83a6c1..408a3bec 100644 --- a/tests/lipsum/content/8c58a65414c23.nwd +++ b/tests/lipsum/content/8c58a65414c23.nwd @@ -1,4 +1,4 @@ -%%~ 8c58a65414c23:b3643d0f92e32:Front Matter +%%~ 8c58a65414c23:b3643d0f92e32:NOVEL:PAGE:Front Matter % Exctracted from the lipsum.com website. Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of “de Finibus Bonorum et Malorum” (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, “Lorem ipsum dolor sit amet..”, comes from a line in section 1.10.32. diff --git a/tests/lipsum/content/db7e733775d4d.nwd b/tests/lipsum/content/db7e733775d4d.nwd index 7be7896e..7f4de78f 100644 --- a/tests/lipsum/content/db7e733775d4d.nwd +++ b/tests/lipsum/content/db7e733775d4d.nwd @@ -1,4 +1,4 @@ -%%~ db7e733775d4d:b3643d0f92e32:Act One +%%~ db7e733775d4d:b3643d0f92e32:NOVEL:PARTITION:Act One # Act One “Fusce maximus felis libero” \ No newline at end of file diff --git a/tests/lipsum/content/eb103bc70c90c.nwd b/tests/lipsum/content/eb103bc70c90c.nwd index 08bb88e9..db11bbf0 100644 --- a/tests/lipsum/content/eb103bc70c90c.nwd +++ b/tests/lipsum/content/eb103bc70c90c.nwd @@ -1,4 +1,4 @@ -%%~ eb103bc70c90c:6bd935d2490cd:b3643d0f92e32:Scene Three +%%~ eb103bc70c90c:6bd935d2490cd:b3643d0f92e32:NOVEL:SCENE:Scene Three ### Scene Three @pov: Bod diff --git a/tests/lipsum/content/f8c0562e50f1b.nwd b/tests/lipsum/content/f8c0562e50f1b.nwd index 752bd117..15a33bc8 100644 --- a/tests/lipsum/content/f8c0562e50f1b.nwd +++ b/tests/lipsum/content/f8c0562e50f1b.nwd @@ -1,4 +1,4 @@ -%%~ f8c0562e50f1b:6bd935d2490cd:b3643d0f92e32:Scene Four +%%~ f8c0562e50f1b:6bd935d2490cd:b3643d0f92e32:NOVEL:SCENE:Scene Four ### Scene Four @pov: Bod diff --git a/tests/lipsum/content/f96ec11c6a3da.nwd b/tests/lipsum/content/f96ec11c6a3da.nwd index 8390c7bd..b95163d5 100644 --- a/tests/lipsum/content/f96ec11c6a3da.nwd +++ b/tests/lipsum/content/f96ec11c6a3da.nwd @@ -1,4 +1,4 @@ -%%~ f96ec11c6a3da:45e6b01ca35c1:b3643d0f92e32:Scene Two +%%~ f96ec11c6a3da:45e6b01ca35c1:b3643d0f92e32:NOVEL:SCENE:Scene Two ### Scene Two @pov: Bod diff --git a/tests/lipsum/content/fb609cd8319dc.nwd b/tests/lipsum/content/fb609cd8319dc.nwd index d28df127..fa9626f2 100644 --- a/tests/lipsum/content/fb609cd8319dc.nwd +++ b/tests/lipsum/content/fb609cd8319dc.nwd @@ -1,4 +1,4 @@ -%%~ fb609cd8319dc:45e6b01ca35c1:b3643d0f92e32:Chapter One +%%~ fb609cd8319dc:45e6b01ca35c1:b3643d0f92e32:NOVEL:CHAPTER:Chapter One ## Chapter One @pov: Bod diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index ef3a9bbe..ebfbde69 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,17 +1,20 @@ - + Lorem Ipsum Lorem Ipsum lipsum.com - False + 7 + 21 + 1459 + False False True - 88d59a277361b + 04468803b92e1 None - 3397 + 3847 Replace Text 1 Replace Text 2 @@ -22,9 +25,6 @@ %title% * * *
- False - False - False New @@ -81,7 +81,7 @@ 584 92 1 - 35 + 79
Act One @@ -238,9 +238,9 @@ Main True NOTE - 9 - 2 - 0 + 1369 + 195 + 2 1387 @@ -257,9 +257,9 @@ Minor True NOTE - 14 - 2 - 0 + 1770 + 259 + 3 1792 diff --git a/tests/reference/build/1_LoremIpsum.nwd b/tests/reference/build/1_LoremIpsum.nwd index 010f3a80..fbe4f132 100644 --- a/tests/reference/build/1_LoremIpsum.nwd +++ b/tests/reference/build/1_LoremIpsum.nwd @@ -14,7 +14,7 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t ## Prologue -_Lorem Ipsum_ is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. +*Lorem Ipsum* is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. # Act One diff --git a/tests/reference/build/2_LoremIpsum.nwd b/tests/reference/build/2_LoremIpsum.nwd index d58f1021..f2a26740 100644 --- a/tests/reference/build/2_LoremIpsum.nwd +++ b/tests/reference/build/2_LoremIpsum.nwd @@ -16,7 +16,7 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t % Synopsis:Explanation from the lipsum.com website. -_Lorem Ipsum_ is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. +*Lorem Ipsum* is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. # Act One From 49049e19d8ed36d1a063a29538da8293b7348f86 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 13 Jun 2020 10:08:00 +0200 Subject: [PATCH 17/72] Moved the emphasis regexes to a constants class --- nw/constants/__init__.py | 3 ++- nw/constants/constants.py | 8 ++++++++ nw/gui/dochighlight.py | 8 ++++---- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/nw/constants/__init__.py b/nw/constants/__init__.py index 1abd529c..57761e42 100644 --- a/nw/constants/__init__.py +++ b/nw/constants/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from nw.constants.iso import isoLanguage, isoCountry from nw.constants.constants import ( - nwConst, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode + nwConst, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode ) from nw.constants.enum import ( nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline @@ -11,6 +11,7 @@ __all__ = [ "isoLanguage", "isoCountry", "nwConst", + "nwRegEx", "nwFiles", "nwKeyWords", "nwLabels", diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 66b026fc..5c483657 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -34,6 +34,14 @@ class nwConst(): # END Class nwConst +class nwRegEx(): + + FMT_B = r"(? Date: Sat, 13 Jun 2020 10:08:35 +0200 Subject: [PATCH 18/72] Also in the tokenizer --- nw/core/tokenizer.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index 1335ed23..b9c283cc 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -34,7 +34,7 @@ from PyQt5.QtCore import QRegularExpression from nw.core.document import NWDoc from nw.core.tools import numberToWord -from nw.constants import nwItemLayout, nwItemType +from nw.constants import nwItemLayout, nwItemType, nwRegEx logger = logging.getLogger(__name__) @@ -298,17 +298,11 @@ class Tokenizer(): """ # RegExes for adding formatting tags within text lines - # Keep in sync with the DocHighlighter class - rxFormats = [( - QRegularExpression(r"(? Date: Sat, 13 Jun 2020 10:27:02 +0200 Subject: [PATCH 19/72] Added back in strikethrough support --- nw/constants/constants.py | 1 + nw/constants/enum.py | 31 ++++++++++++++++--------------- nw/core/tohtml.py | 30 ++++++++++++++++++++++-------- nw/core/tokenizer.py | 14 +++++++++----- nw/gui/build.py | 4 +++- nw/gui/doceditor.py | 32 ++++++++++++++++++++++++++++++++ nw/gui/dochighlight.py | 7 +++++++ nw/gui/mainmenu.py | 7 +++++++ sample/content/636b6aa9b697b.nwd | 2 +- sample/nwProject.nwx | 16 ++++++++-------- 10 files changed, 106 insertions(+), 38 deletions(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 5c483657..9bd47110 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -39,6 +39,7 @@ class nwRegEx(): FMT_B = r"(?", - self.FMT_B_E : "", - self.FMT_I_B : "", - self.FMT_I_E : "", - self.FMT_S_B : "", - self.FMT_S_E : "", - } + if self.genMode == self.M_PREVIEW: + htmlTags = { # HTML4 + CSS2 + self.FMT_B_B : "", + self.FMT_B_E : "", + self.FMT_I_B : "", + self.FMT_I_E : "", + self.FMT_S_B : "", + self.FMT_S_E : "", + self.FMT_D_B : "", + self.FMT_D_E : "", + } + else: + htmlTags = { # HTML5 + self.FMT_B_B : "", + self.FMT_B_E : "", + self.FMT_I_B : "", + self.FMT_I_E : "", + self.FMT_S_B : "", + self.FMT_S_E : "", + self.FMT_D_B : "", + self.FMT_D_E : "", + } if self.isNovel and self.genMode != self.M_PREVIEW: # For novel files for export, we bump the titles one level diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index b9c283cc..e13feb0e 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -46,6 +46,8 @@ class Tokenizer(): FMT_I_E = 4 # End italics FMT_S_B = 5 # Begin bold italic FMT_S_E = 6 # End bold italic + FMT_D_B = 7 # Begin strikeout + FMT_D_E = 8 # End strikeout T_EMPTY = 1 # Empty line (new paragraph) T_SYNOPSIS = 2 # Synopsis comment @@ -292,16 +294,18 @@ class Tokenizer(): The format of the token list is an entry with a four-tuple for each line in the file. The tuple is as follows: 1: The type of the block, self.T_* - 2: The text content of the block, without leading tags - 3: The internal formatting map of the text, self.FMT_* - 4: The style of the block, self.A_* + 2: The line in file where this block occurred + 3: The text content of the block, without leading tags + 4: The internal formatting map of the text, self.FMT_* + 5: The style of the block, self.A_* """ # RegExes for adding formatting tags within text lines rxFormats = [ - (QRegularExpression(nwRegEx.FMT_BI), [None, self.FMT_S_B, None, self.FMT_S_E]), - (QRegularExpression(nwRegEx.FMT_B), [None, self.FMT_B_B, None, self.FMT_B_E]), (QRegularExpression(nwRegEx.FMT_I), [None, self.FMT_I_B, None, self.FMT_I_E]), + (QRegularExpression(nwRegEx.FMT_B), [None, self.FMT_B_B, None, self.FMT_B_E]), + (QRegularExpression(nwRegEx.FMT_BI), [None, self.FMT_S_B, None, self.FMT_S_E]), + (QRegularExpression(nwRegEx.FMT_ST), [None, self.FMT_D_B, None, self.FMT_D_E]), ] self.theTokens = [] diff --git a/nw/gui/build.py b/nw/gui/build.py index 02841833..70ea0703 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -982,7 +982,9 @@ class GuiBuildNovelDocView(QTextBrowser): """ if isinstance(theText, list): theText = "".join(theText) - theText = theText.replace(" "," "*4) + theText = theText.replace(" ", " "*4) + theText = theText.replace("", "") + theText = theText.replace("", "") self.setHtml(theText) return diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 26e64c82..4c5d5ce7 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -515,6 +515,8 @@ class GuiDocEditor(QTextEdit): self._toggleEmph(2) elif theAction == nwDocAction.BOLDITALIC: self._toggleEmph(3) + elif theAction == nwDocAction.STRIKE: + self._toggleStrike() elif theAction == nwDocAction.S_QUOTE: self._wrapSelection(self.typSQOpen, self.typSQClose) elif theAction == nwDocAction.D_QUOTE: @@ -1000,6 +1002,36 @@ class GuiDocEditor(QTextEdit): return + def _toggleStrike(self): + """Toggle strikethrough text. + """ + theCursor = self._autoSelect() + if theCursor.hasSelection(): + posS = theCursor.selectionStart() + posE = theCursor.selectionEnd() + + numB = 0 + for n in range(2): + if self.qDocument.characterAt(posS-n-1) == "~": + numB += 1 + else: + break + + numA = 0 + for n in range(2): + if self.qDocument.characterAt(posE+n) == "~": + numA += 1 + else: + break + + cLevel = min(numB, numA) + if cLevel == 2: + self._clearSurrounding(theCursor, 2) + else: + self._wrapSelection("~~") + + return + def _formatBlock(self, docAction): """Changes the block format of the block under the cursor. """ diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index e81d9bca..4754110d 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -160,6 +160,13 @@ class GuiDocHighlighter(QSyntaxHighlighter): 3 : self.hStyles["hidden"], } )) + self.hRules.append(( + nwRegEx.FMT_ST, { + 1 : self.hStyles["hidden"], + 2 : self.hStyles["strike"], + 3 : self.hStyles["hidden"], + } + )) # Quoted Strings if self.mainConf.highlightQuotes: diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 086261d1..3ea9a85d 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -536,6 +536,13 @@ class GuiMainMenu(QMenuBar): self.aFmtBoldIt.triggered.connect(lambda: self._docAction(nwDocAction.BOLDITALIC)) self.fmtMenu.addAction(self.aFmtBoldIt) + # Format > Strikethrough + self.aFmtStrike = QAction("Strikethrough Text", self) + self.aFmtStrike.setStatusTip("Strikethrough selected text") + self.aFmtStrike.setShortcut("Ctrl+-") + self.aFmtStrike.triggered.connect(lambda: self._docAction(nwDocAction.STRIKE)) + self.fmtMenu.addAction(self.aFmtStrike) + # Edit > Separator self.fmtMenu.addSeparator() diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 7b2d92c6..4916b753 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -7,7 +7,7 @@ A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree. -Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, *italic* and ***bold italic***. +Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, *italic* and ***bold italic***. You can also ~~strike through~~ text. In addition, the editor supports automatic formatting of “quotes”, both double and ‘single’. Depending on the syntax highlighter, these can be in different colours. diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index a603b373..aeba369e 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 319 - 49 - 9675 + 332 + 56 + 10692 False @@ -15,7 +15,7 @@ True 636b6aa9b697b 636b6aa9b697b - 921 + 927 B E @@ -114,10 +114,10 @@ 1st Draft True SCENE - 1202 - 217 + 1240 + 223 7 - 248 + 403 Another Scene From 95268a8ca554fa8879fa5a42c808334855f7347b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 13 Jun 2020 10:46:58 +0200 Subject: [PATCH 20/72] Updated changelog and documentation --- CHANGELOG.md | 6 ++++++ README.md | 2 +- docs/source/interface.rst | 15 ++++++++++----- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c3a2448..5b8065bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,15 @@ ## Version 0.9rc1 [2020-xx-xx] +**Core Functionality** + +* Underline text formatting has been removed. It is not standard HTML5, not Markdown, and was previously implemented using the double underscore notation that in standard Markdown is renderred as bold text. Instead, novelWriter now renders a single `*` or `-` wrapping a piece of text *within* a paragraphs as italicised text, and a double `**` or `__` as bold text. The keyboard shortcuts and automatic features **only** support the `*` notation. A triple set of `***` are treated as both bold and italicised. PR #310. +* Strikethrough formatting has been added back into novelWriter using the standard Markdown `~~` wrapping. PR #310. + **User Interface** * The Open Project dialog will now ask before removing an entry from the recent projects list. PR #309. +* The text emphasis functions, either selected from the menu or via keyboard shortcuts, will now try to respond to the command in a more meaningful way. That is, the text editor will try to toggle the bold or italics features independently of eachother on the selected text. A feature to apply both at the same time has also been added. PR #310. **Other Changes** diff --git a/README.md b/README.md index bce21ca7..fd119506 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ It allows for a minimal set of formatting needed for writing text documents for These are currently limited to: * Headings level 1 to 4 using the `#` syntax only. -* Bold, italic and underline text. +* Bold, italic and strikethrough text. * Hard line breaks using two or more spaces at the end of a line. That is it. diff --git a/docs/source/interface.rst b/docs/source/interface.rst index 4b4a8662..3174a888 100644 --- a/docs/source/interface.rst +++ b/docs/source/interface.rst @@ -33,8 +33,8 @@ Markdown Format =============== The document editor uses a simplified markdown format. -That is, it supports basic formatting like bold, italics and underline, as well as four levels of headings. -The formats are listed below. +That is, it supports basic formatting like bold, italics and strikethrough, as well as four levels of headings. +The preference of novelWriter is to use `*` for wrapping emphasised text, but `_` is partially supported when typed, but not by the automatic formatting features and keyboard shortcuts. In addition to these standard markdown features, the editor also allows for comments, that is text that is ignored by the word counter and not exported or, optionally, hidden in the document viewer. If the first word of a comment is "Synopsis:" (with the colon), the comment is treated specially, and will show up in the Outline View. @@ -48,9 +48,12 @@ The editor also has a minimal set of keywords used for setting tags and referenc "``## Title``", "Heading level two. The space after the # is mandatory." "``### Title``", "Heading level three. The space after the # is mandatory." "``#### Title``", "Heading level four. The space after the # is mandatory." + "``*text*``", "The text is rendered as italicised text." "``**text**``", "The text is rendered as bold text." - "``_text_``", "The text is rendered as italicized text." - "``__text__``", "The text is rendered as underlined text." + "``***text***``", "The text is rendered as bold italicised text." + "``_text_``", "Alternative format for italicised text." + "``__text__``", "Alternative format for bold text." + "``~~text~~``", "Strikethrough text." "``% text...``", "A comment. The text is not exported by default, seen in viewer, or counted towards word counts." "``% Synopsis: text...``", "A synopsis comment. Shows up in the Synopsis column of the Outline View, but is otherwise treated as a comment." "``@keyword: value``", "A keyword argument followed by a value, or a comma separated list of values." @@ -110,6 +113,7 @@ These are as following: ":kbd:`Ctrl-.`", "Correct word under cursor." ":kbd:`Ctrl-,`", "Open the Preferences dialog." ":kbd:`Ctrl-/`", "Change block format to comment." + ":kbd:`Ctrl--`", "Strikethrough selected text, or word under cursor." ":kbd:`Ctrl-0`", "Remove block formatting for block under cursor." ":kbd:`Ctrl-1`", "Change block format to header level 1." ":kbd:`Ctrl-2`", "Change block format to header level 2." @@ -122,7 +126,7 @@ These are as following: ":kbd:`Ctrl-E`", "If in tree view, edit a document or folder settings. (Same as :kbd:`F2`)" ":kbd:`Ctrl-F`", "Open the search bar and search for selected word, if any is selected." ":kbd:`Ctrl-G`", "Find next occurrence of word in current document. (Same as :kbd:`F3`)" - ":kbd:`Ctrl-H`", "Open the search and replace bar and search for selected word, if any is selected. (On Mac, this is :kbd:`Cmd+=`)" + ":kbd:`Ctrl-H`", "Open the search and replace bar and search for selected word, if any is selected. (On Mac, this is :kbd:`Cmd-=`)" ":kbd:`Ctrl-I`", "Format selected text, or word under cursor, as italic." ":kbd:`Ctrl-N`", "Create new document." ":kbd:`Ctrl-O`", "Open selected document." @@ -143,6 +147,7 @@ These are as following: ":kbd:`Ctrl-Shift-/`", "Remove block formatting for block under cursor." ":kbd:`Ctrl-Shift-1`", "Replace occurrence of word in current document, and search for next occurrence." ":kbd:`Ctrl-Shift-A`", "Select all text in current paragraph." + ":kbd:`Ctrl-Shift-B`", "Format selected text, or word under cursor, as bold and italic." ":kbd:`Ctrl-Shift-D`", "Wrap selected text, or word under cursor, in single quotes." ":kbd:`Ctrl-Shift-G`", "Find previous occurrence of word in current document. (Same as :kbd:`Shift-F3`" ":kbd:`Ctrl-Shift-I`", "Import text to the current document from a text file." From ff6b4bcfa7d242f1bbc4678166f34b1ed6326703 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 13 Jun 2020 20:44:27 +0200 Subject: [PATCH 21/72] Improve the word saparation detection of the spell check highlighting --- nw/gui/dochighlight.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index 4754110d..abf3c122 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -203,9 +203,10 @@ class GuiDocHighlighter(QSyntaxHighlighter): # Build a QRegExp for spell checker # Include additional characters that the highlighter should # consider to be word separators - wordSep = "_+" + wordSep = "_\+" + wordSep += nwUnicode.U_ENDASH wordSep += nwUnicode.U_EMDASH - self.spellRx = QRegularExpression("\\b[^\\s%s]+\\b" % wordSep) + self.spellRx = QRegularExpression(r"\b[^\s"+wordSep+r"]+\b") self.spellRx.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption) return True From bd8a6afd911861f9b94c2735942244a63863ece3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 13 Jun 2020 21:14:04 +0200 Subject: [PATCH 22/72] Minor tweaks in text parsing and highlighting --- nw/core/index.py | 5 +++-- nw/core/tokenizer.py | 5 +++-- nw/gui/dochighlight.py | 20 ++++++++++---------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/nw/core/index.py b/nw/core/index.py index 632375a7..b32c8f38 100644 --- a/nw/core/index.py +++ b/nw/core/index.py @@ -305,11 +305,12 @@ class NWIndex(): elif aLine.startswith(r"%"): if nTitle > 0: - toCheck = aLine[1:].lstrip().lower() + toCheck = aLine[1:].lstrip() + synTag = toCheck[:9].lower() tLen = len(aLine) cLen = len(toCheck) cOff = tLen - cLen - if toCheck.startswith("synopsis:"): + if synTag == "synopsis:": self._indexSynopsis(tHandle, isNovel, aLine[cOff+9:].strip(), nTitle) # Count words for remaining text after last heading diff --git a/nw/core/tokenizer.py b/nw/core/tokenizer.py index e13feb0e..09345e09 100644 --- a/nw/core/tokenizer.py +++ b/nw/core/tokenizer.py @@ -327,8 +327,9 @@ class Tokenizer(): tmpMarkdown.append("\n") elif aLine[0] == "%": - cLine = aLine[1:].strip() - if cLine.lower().startswith("synopsis:"): + cLine = aLine[1:].lstrip() + synTag = cLine[:9].lower() + if synTag == "synopsis:": self.theTokens.append(( self.T_SYNOPSIS, nLine, diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index abf3c122..a76c9646 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -78,7 +78,6 @@ class GuiDocHighlighter(QSyntaxHighlighter): """Initialise the syntax highlighter, setting all the colour rules and building the regexes. """ - logger.debug("Setting up highlighting rules") self.colHead = QColor(*self.theTheme.colHead) @@ -108,8 +107,8 @@ class GuiDocHighlighter(QSyntaxHighlighter): "header4h" : self._makeFormat(self.colHeadH, "bold", 1.2), "bold" : self._makeFormat(self.colEmph, "bold"), "italic" : self._makeFormat(self.colEmph, "italic"), - "strike" : self._makeFormat(self.colEmph, "strike"), "bolditalic" : self._makeFormat(self.colEmph, ("bold","italic")), + "strike" : self._makeFormat(self.colEmph, "strike"), "trailing" : self._makeFormat(self.colTrail, "background"), "nobreak" : self._makeFormat(self.colTrail, "background"), "dialogue1" : self._makeFormat(self.colDialN), @@ -131,7 +130,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): } )) - # Non-breaking Space + # Non-Breaking Spaces self.hRules.append(( "[%s]+" % nwUnicode.U_NBSP, { 0 : self.hStyles["nobreak"], @@ -203,7 +202,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): # Build a QRegExp for spell checker # Include additional characters that the highlighter should # consider to be word separators - wordSep = "_\+" + wordSep = r"_\+" wordSep += nwUnicode.U_ENDASH wordSep += nwUnicode.U_EMDASH self.spellRx = QRegularExpression(r"\b[^\s"+wordSep+r"]+\b") @@ -241,7 +240,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): def highlightBlock(self, theText): """Highlight a single block. Prefer to check first character for all formats that are defined by their initial characters. This - is significantly faster than running the regex checks we use for + is significantly faster than running the regex checks used for text paragraphs. """ if self.theHandle is None or not theText: @@ -252,9 +251,9 @@ class GuiDocHighlighter(QSyntaxHighlighter): isValid, theBits, thePos = self.theIndex.scanThis(theText) isGood = self.theIndex.checkThese(theBits, tItem) if isValid: - for n in range(len(theBits)): + for n, theBit in enumerate(theBits): xPos = thePos[n] - xLen = len(theBits[n]) + xLen = len(theBit) if isGood[n]: if n == 0: self.setFormat(xPos, xLen, self.hStyles["keyword"]) @@ -287,11 +286,12 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.setFormat(4, len(theText), self.hStyles["header4"]) elif theText.startswith("%"): # Comments - toCheck = theText[1:].lstrip().lower() + toCheck = theText[1:].lstrip() + synTag = toCheck[:9].lower() tLen = len(theText) cLen = len(toCheck) cOff = tLen - cLen - if toCheck.startswith("synopsis:"): + if synTag == "synopsis:": self.setFormat(0, cOff+9, self.hStyles["modifier"]) self.setFormat(cOff+9, tLen, self.hStyles["hidden"]) else: @@ -349,7 +349,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): if "underline" in fmtStyle: theFormat.setFontUnderline(True) if "background" in fmtStyle: - theFormat.setBackground(QBrush(fmtCol,Qt.SolidPattern)) + theFormat.setBackground(QBrush(fmtCol, Qt.SolidPattern)) if fmtSize is not None: theFormat.setFontPointSize(round(fmtSize*self.mainConf.textSize)) From aad35e2d7b3fda3dd18dfb0ffe9dfa991dd8900b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 15 Jun 2020 16:35:24 +0200 Subject: [PATCH 23/72] First some cleanup of the document widgets --- nw/gui/doceditor.py | 24 +++++++++++++++--------- nw/gui/docviewer.py | 23 ++++++++++++----------- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 4c5d5ce7..bd491a4f 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -7,7 +7,7 @@ File History: Created: 2018-09-29 [0.0.1] GuiDocEditor - Created: 2019-04-22 [0.0.1] WordCounter + Created: 2019-04-22 [0.0.1] BackgroundWordCounter Created: 2020-04-25 [0.4.5] GuiDocEditHeader This file is a part of novelWriter @@ -126,7 +126,7 @@ class GuiDocEditor(QTextEdit): self.wcTimer.setInterval(int(self.wcInterval*1000)) self.wcTimer.timeout.connect(self._runCounter) - self.wCounter = WordCounter(self) + self.wCounter = BackgroundWordCounter(self) self.wCounter.finished.connect(self._updateCounts) self.initEditor() @@ -338,11 +338,7 @@ class GuiDocEditor(QTextEdit): docFormat = self.qDocument.rootFrame().frameFormat() docFormat.setLeftMargin(tM) - docFormat.setRightMargin(tM) - if tT > 0: - docFormat.setTopMargin(tT) - else: - docFormat.setTopMargin(0) + docFormat.setTopMargin(max(0, tT)) # Updating root frame triggers a QTextDocument->contentsChange # signal, which we do not want as it re-runs the syntax @@ -1205,7 +1201,12 @@ class GuiDocEditor(QTextEdit): # END Class GuiDocEditor -class WordCounter(QThread): +# =============================================================================================== # +# The Off GUI Thread Word Counter +# Runs the word counter in the background for the doCEditor +# =============================================================================================== # + +class BackgroundWordCounter(QThread): def __init__(self, theParent): QThread.__init__(self, theParent) @@ -1228,7 +1229,12 @@ class WordCounter(QThread): return -## END Class WordCounter +## END Class BackgroundWordCounter + +# =============================================================================================== # +# The Embedded Document Header +# Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport +# =============================================================================================== # class GuiDocEditHeader(QWidget): diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 1059c277..8c2ea236 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -236,13 +236,14 @@ class GuiDocViewer(QTextBrowser): else: sW = 0 + cM = self.mainConf.getTextMargin() tB = self.frameWidth() tW = self.width() - 2*tB - sW tH = self.docHeader.height() fH = self.docFooter.height() fY = self.height() - fH - tB - tT = self.mainConf.getTextMargin() - tH - bT = self.mainConf.getTextMargin() - fH + tT = cM - tH + bT = cM - fH self.docHeader.setGeometry(tB, tB, tW, tH) self.docFooter.setGeometry(tB, fY, tW, fH) self.setViewportMargins(0, tH, 0, fH) @@ -558,16 +559,16 @@ class GuiDocViewHeader(QWidget): class GuiDocViewFooter(QWidget): - def __init__(self, theParent): - QWidget.__init__(self, theParent) + def __init__(self, docViewer): + QWidget.__init__(self, docViewer) logger.debug("Initialising GuiDocViewFooter ...") self.mainConf = nw.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.optState = theParent.theProject.optState - self.viewMeta = theParent.theParent.viewMeta + self.docViewer = docViewer + self.theParent = docViewer.theParent + self.theTheme = docViewer.theTheme + self.viewMeta = docViewer.theParent.viewMeta self.theHandle = None # Make a QPalette that matches the Syntax Theme @@ -669,9 +670,9 @@ class GuiDocViewFooter(QWidget): """Toggle the sticky flag for the reference panel. """ logger.verbose("Reference sticky is %s" % str(theState)) - self.theParent.stickyRef = theState - if not theState and self.theParent.theHandle is not None: - self.viewMeta.refreshReferences(self.theParent.theHandle) + self.docViewer.stickyRef = theState + if not theState and self.docViewer.theHandle is not None: + self.viewMeta.refreshReferences(self.docViewer.theHandle) return # END Class GuiDocViewFooter From 7374d0513e1822e3ceb79545985864cba66028d7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 15 Jun 2020 16:39:31 +0200 Subject: [PATCH 24/72] Some minor changes for code consistency --- nw/gui/doceditor.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index bd491a4f..f3b70649 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -1203,14 +1203,14 @@ class GuiDocEditor(QTextEdit): # =============================================================================================== # # The Off GUI Thread Word Counter -# Runs the word counter in the background for the doCEditor +# Runs the word counter in the background for the DocEditor # =============================================================================================== # class BackgroundWordCounter(QThread): - def __init__(self, theParent): - QThread.__init__(self, theParent) - self.theParent = theParent + def __init__(self, docEditor): + QThread.__init__(self, docEditor) + self.docEditor = docEditor self.charCount = 0 self.wordCount = 0 self.paraCount = 0 @@ -1220,13 +1220,11 @@ class BackgroundWordCounter(QThread): """Overloaded run function for the word counter, forwarding the call to the function that does the actual counting. """ - theText = self.theParent.getText() + theText = self.docEditor.getText() cC, wC, pC = countWords(theText) - self.charCount = cC self.wordCount = wC self.paraCount = pC - return ## END Class BackgroundWordCounter From bbb0f5f9ddfa18d1587b009d3ec34aa2abffa7c7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 15 Jun 2020 21:36:04 +0200 Subject: [PATCH 25/72] Moved the search bar code into a new class --- nw/gui/__init__.py | 2 - nw/gui/docbars.py | 168 --------------------------------------- nw/gui/doceditor.py | 189 +++++++++++++++++++++++++++++++++++++++++--- nw/guimain.py | 10 +-- 4 files changed, 180 insertions(+), 189 deletions(-) delete mode 100644 nw/gui/docbars.py diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index 820a2a48..4e96b4ee 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -2,7 +2,6 @@ from nw.gui.about import GuiAbout from nw.gui.build import GuiBuildNovel -from nw.gui.docbars import GuiSearchBar from nw.gui.doceditor import GuiDocEditor from nw.gui.docmerge import GuiDocMerge from nw.gui.docsplit import GuiDocSplit @@ -23,7 +22,6 @@ from nw.gui.theme import GuiIcons, GuiTheme __all__ = [ "GuiAbout", "GuiBuildNovel", - "GuiSearchBar", "GuiDocEditor", "GuiDocMerge", "GuiDocSplit", diff --git a/nw/gui/docbars.py b/nw/gui/docbars.py deleted file mode 100644 index 079f989a..00000000 --- a/nw/gui/docbars.py +++ /dev/null @@ -1,168 +0,0 @@ -# -*- coding: utf-8 -*- -"""novelWriter GUI Main Window SearchBar - - novelWriter – GUI Main Window SearchBar -========================================= - Class holding the main window search bar - - File History: - Created: 2019-09-29 [0.2.1] GuiSearchBar - - This file is a part of novelWriter - Copyright 2020, Veronica Berglyd Olsen - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, but - WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -""" - -import logging -import nw - -from PyQt5.QtCore import Qt, QSize -from PyQt5.QtGui import QPalette, QColor, QIcon -from PyQt5.QtWidgets import ( - qApp, QWidget, QFrame, QGridLayout, QLabel, QLineEdit, QPushButton, - QHBoxLayout, QToolButton, QScrollArea -) - -from nw.constants import nwDocAction, nwUnicode - -logger = logging.getLogger(__name__) - -class GuiSearchBar(QWidget): - - def __init__(self, theParent): - QWidget.__init__(self, theParent) - - logger.debug("Initialising GuiSearchBar ...") - - self.mainConf = nw.CONFIG - self.theParent = theParent - self.theTheme = theParent.theTheme - self.repVisible = False - - self.setContentsMargins(0, 0, 0, 0) - - self.mainBox = QGridLayout(self) - self.setLayout(self.mainBox) - - self.searchBox = QLineEdit() - self.replaceBox = QLineEdit() - self.searchLabel = QLabel("Search") - self.replaceLabel = QLabel("Replace") - self.closeButton = QPushButton(self.theTheme.getIcon("close"),"") - self.searchButton = QPushButton(self.theTheme.getIcon("search"),"") - self.replaceButton = QPushButton(self.theTheme.getIcon("search-replace"),"") - - self.closeButton.clicked.connect(self._doClose) - self.searchButton.clicked.connect(self._doSearch) - self.replaceButton.clicked.connect(self._doReplace) - self.searchBox.returnPressed.connect(self._doSearch) - self.replaceBox.returnPressed.connect(self._doSearch) - - self.mainBox.addWidget(QLabel(""), 0, 0) - self.mainBox.addWidget(self.searchLabel, 0, 1) - self.mainBox.addWidget(self.searchBox, 0, 2) - self.mainBox.addWidget(self.searchButton, 0, 3) - self.mainBox.addWidget(self.closeButton, 0, 4) - self.mainBox.addWidget(self.replaceLabel, 1, 1) - self.mainBox.addWidget(self.replaceBox, 1, 2) - self.mainBox.addWidget(self.replaceButton, 1, 3) - - self.mainBox.setColumnStretch(0, 1) - self.mainBox.setColumnStretch(1, 0) - self.mainBox.setColumnStretch(2, 0) - self.mainBox.setColumnStretch(3, 0) - self.mainBox.setColumnStretch(4, 0) - self.mainBox.setContentsMargins(0, 0, 0, 0) - - boxWidth = 16*self.theTheme.textNWidth - self.searchBox.setMinimumWidth(boxWidth) - self.replaceBox.setMinimumWidth(boxWidth) - - self._replaceVisible(False) - - logger.debug("GuiSearchBar initialisation complete") - - return - - ## - # Get and Set Functions - ## - - def setSearchText(self, theText): - """Open the search bar and set the search text to the text - provided, if any. - """ - if not self.isVisible(): - self.setVisible(True) - self.searchBox.setText(theText) - self.searchBox.setFocus() - logger.verbose("Setting search text to '%s'" % theText) - return True - - def setReplaceText(self, theText): - """Set the replace text. - """ - self._replaceVisible(True) - self.replaceBox.setFocus() - self.replaceBox.setText(theText) - return True - - def getSearchText(self): - """Return the current search text. - """ - return self.searchBox.text() - - def getReplaceText(self): - """Return the current replace text. - """ - return self.replaceBox.text() - - ## - # Internal Functions - ## - - def _doClose(self): - """Hide the search/replace bar. - """ - self._replaceVisible(False) - self.setVisible(False) - return - - def _doSearch(self): - """Call the search action function for the document editor. - """ - modKey = qApp.keyboardModifiers() - if modKey == Qt.ShiftModifier: - self.theParent.docEditor.docAction(nwDocAction.GO_PREV) - else: - self.theParent.docEditor.docAction(nwDocAction.GO_NEXT) - return - - def _doReplace(self): - """Call the replace action function for the document editor. - """ - self.theParent.docEditor.docAction(nwDocAction.REPL_NEXT) - return - - def _replaceVisible(self, isVisible): - """Set the visibility of all the replace widgets. - """ - self.replaceLabel.setVisible(isVisible) - self.replaceBox.setVisible(isVisible) - self.replaceButton.setVisible(isVisible) - self.repVisible = isVisible - return True - -# END Class GuiSearchBar diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index f3b70649..1c0ac459 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -6,9 +6,11 @@ Class holding the document editor File History: - Created: 2018-09-29 [0.0.1] GuiDocEditor - Created: 2019-04-22 [0.0.1] BackgroundWordCounter - Created: 2020-04-25 [0.4.5] GuiDocEditHeader + Created: 2018-09-29 [0.0.1] GuiDocEditor + Created: 2019-04-22 [0.0.1] BackgroundWordCounter + Created: 2019-09-29 [0.2.1] GuiDocEditSearch + Created: 2020-04-25 [0.4.5] GuiDocEditHeader + Rewritten: 2020-06-15 [0.9.0] GuiDocEditSearch This file is a part of novelWriter Copyright 2020, Veronica Berglyd Olsen @@ -39,7 +41,7 @@ from PyQt5.QtGui import ( ) from PyQt5.QtWidgets import ( qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QWidget, QLabel, - QToolButton, QHBoxLayout + QToolButton, QHBoxLayout, QGridLayout, QLineEdit, QPushButton, QFrame, QVBoxLayout ) from nw.core import NWDoc @@ -87,6 +89,7 @@ class GuiDocEditor(QTextEdit): # Document Title self.docHeader = GuiDocEditHeader(self) + self.docSearch = GuiDocEditSearch(self) # Syntax self.hLight = GuiDocHighlighter(self.qDocument, self.theParent) @@ -309,6 +312,7 @@ class GuiDocEditor(QTextEdit): Config.textFixedW is enabled or we're in Zen mode. Otherwise, just ensure the margins are set correctly. """ + wW = self.width() cM = self.mainConf.getTextMargin() vBar = self.verticalScrollBar() @@ -322,7 +326,6 @@ class GuiDocEditor(QTextEdit): tW = self.mainConf.getZenWidth() else: tW = self.mainConf.getTextWidth() - wW = self.width() tM = int((wW - sW - tW)/2) if tM < cM: tM = cM @@ -330,15 +333,21 @@ class GuiDocEditor(QTextEdit): tM = cM tB = self.frameWidth() - tW = self.width() - 2*tB - sW + tW = wW - 2*tB - sW tH = self.docHeader.height() tT = cM - tH + + rH = self.docSearch.height() + rW = self.docSearch.width() + rL = wW - sW - rW - tB + self.docHeader.setGeometry(tB, tB, tW, tH) + self.docSearch.move(rL, tB) self.setViewportMargins(0, tH, 0, 0) docFormat = self.qDocument.rootFrame().frameFormat() docFormat.setLeftMargin(tM) - docFormat.setTopMargin(max(0, tT)) + docFormat.setTopMargin(max(0, tT, rH)) # Updating root frame triggers a QTextDocument->contentsChange # signal, which we do not want as it re-runs the syntax @@ -569,6 +578,13 @@ class GuiDocEditor(QTextEdit): )) return + def closeSearch(self): + """Close the search box. + """ + self.docSearch.setVisible(False) + self.updateDocMargins() + return self.docSearch.isVisible() + ## # Document Events and Maintenance ## @@ -1127,7 +1143,8 @@ class GuiDocEditor(QTextEdit): selText = theCursor.selectedText() else: selText = "" - self.theParent.searchBar.setSearchText(selText) + self.docSearch.setSearchText(selText) + self.updateDocMargins() return def _beginReplace(self): @@ -1135,14 +1152,14 @@ class GuiDocEditor(QTextEdit): text. """ self._beginSearch() - self.theParent.searchBar.setReplaceText("") + self.docSearch.setReplaceText("") return def _findNext(self): """Searches for the next occurrence of the search bar text in the document. Wraps back to the top if not found. """ - searchFor = self.theParent.searchBar.getSearchText() + searchFor = self.docSearch.getSearchText() wasFound = self.find(searchFor) if not wasFound: theCursor = self.textCursor() @@ -1154,7 +1171,7 @@ class GuiDocEditor(QTextEdit): """Searches for the previous occurrence of the search bar text in the document. Wraps back to the end if not found. """ - searchFor = self.theParent.searchBar.getSearchText() + searchFor = self.docSearch.getSearchText() wasFound = self.find(searchFor, QTextDocument.FindBackward) if not wasFound: theCursor = self.textCursor() @@ -1168,8 +1185,8 @@ class GuiDocEditor(QTextEdit): to the top if not found. """ theCursor = self.textCursor() - searchFor = self.theParent.searchBar.getSearchText() - replWith = self.theParent.searchBar.getReplaceText() + searchFor = self.docSearch.getSearchText() + replWith = self.docSearch.getReplaceText() if theCursor.hasSelection() and theCursor.selectedText() == searchFor: xPos = theCursor.selectionStart() theCursor.beginEditBlock() @@ -1229,6 +1246,152 @@ class BackgroundWordCounter(QThread): ## END Class BackgroundWordCounter +# =============================================================================================== # +# The Embedded Document Search/Replace Feature +# Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport +# =============================================================================================== # + +class GuiDocEditSearch(QWidget): + + def __init__(self, docEditor): + QWidget.__init__(self, docEditor) + + logger.debug("Initialising GuiDocEditSearch ...") + + self.mainConf = nw.CONFIG + self.docEditor = docEditor + self.theParent = docEditor.theParent + self.theProject = docEditor.theProject + self.theTheme = docEditor.theTheme + + self.repVisible = False + + fPx = int(0.9*self.theTheme.fontPixelSize) + boxFont = self.theTheme.guiFont + boxFont.setPointSizeF(0.9*self.theTheme.fontPointSize) + + # self.setContentsMargins(0, 0, 0, 0) + # self.setFrameStyle(QFrame.Box) + self.setAutoFillBackground(True) + + self.mainBox = QGridLayout(self) + self.setLayout(self.mainBox) + + self.searchBox = QLineEdit() + self.searchBox.setFont(boxFont) + + self.replaceBox = QLineEdit() + self.replaceBox.setFont(boxFont) + + self.closeButton = QPushButton(self.theTheme.getIcon("close"),"") + self.searchButton = QPushButton(self.theTheme.getIcon("search"),"") + self.replaceButton = QPushButton(self.theTheme.getIcon("search-replace"),"") + + self.closeButton.clicked.connect(self._doClose) + self.searchButton.clicked.connect(self._doSearch) + self.replaceButton.clicked.connect(self._doReplace) + self.searchBox.returnPressed.connect(self._doSearch) + self.replaceBox.returnPressed.connect(self._doSearch) + + self.mainBox.addWidget(self.searchBox, 0, 0) + self.mainBox.addWidget(self.searchButton, 0, 1) + self.mainBox.addWidget(self.closeButton, 0, 2) + self.mainBox.addWidget(self.replaceBox, 1, 0) + self.mainBox.addWidget(self.replaceButton, 1, 1) + + self.mainBox.setColumnStretch(0, 1) + self.mainBox.setColumnStretch(1, 0) + self.mainBox.setColumnStretch(2, 0) + self.mainBox.setColumnStretch(3, 0) + self.mainBox.setColumnStretch(4, 0) + self.mainBox.setVerticalSpacing(0) + self.mainBox.setHorizontalSpacing(2) + # self.mainBox.setContentsMargins(0, 0, 0, 0) + + boxWidth = 16*self.theTheme.textNWidth + self.searchBox.setFixedWidth(boxWidth) + self.replaceBox.setFixedWidth(boxWidth) + + # self._replaceVisible(False) + + logger.debug("GuiDocEditSearch initialisation complete") + + return + + ## + # Get and Set Functions + ## + + def setSearchText(self, theText): + """Open the search bar and set the search text to the text + provided, if any. + """ + if not self.isVisible(): + self.setVisible(True) + self.searchBox.setText(theText) + self.searchBox.setFocus() + logger.verbose("Setting search text to '%s'" % theText) + return True + + def setReplaceText(self, theText): + """Set the replace text. + """ + self._replaceVisible(True) + self.replaceBox.setFocus() + self.replaceBox.setText(theText) + return True + + def getSearchText(self): + """Return the current search text. + """ + return self.searchBox.text() + + def getReplaceText(self): + """Return the current replace text. + """ + return self.replaceBox.text() + + ## + # Slots + ## + + def _doClose(self): + """Hide the search/replace bar. + """ + self._replaceVisible(False) + self.docEditor.closeSearch() + return + + def _doSearch(self): + """Call the search action function for the document editor. + """ + modKey = qApp.keyboardModifiers() + if modKey == Qt.ShiftModifier: + self.docEditor.docAction(nwDocAction.GO_PREV) + else: + self.docEditor.docAction(nwDocAction.GO_NEXT) + return + + def _doReplace(self): + """Call the replace action function for the document editor. + """ + self.docEditor.docAction(nwDocAction.REPL_NEXT) + return + + ## + # Internal Functions + ## + + def _replaceVisible(self, isVisible): + """Set the visibility of all the replace widgets. + """ + self.replaceBox.setVisible(isVisible) + self.replaceButton.setVisible(isVisible) + self.repVisible = isVisible + return True + +# END Class GuiDocEditSearch + # =============================================================================================== # # The Embedded Document Header # Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport diff --git a/nw/guimain.py b/nw/guimain.py index cb2f19a2..1be860f0 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -43,7 +43,7 @@ from nw.gui import ( GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus, GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, - GuiProjectSettings, GuiProjectTree, GuiSearchBar, GuiSessionLogView, GuiTheme + GuiProjectSettings, GuiProjectTree, GuiSessionLogView, GuiTheme ) from nw.core import NWProject, NWDoc, NWIndex from nw.constants import nwFiles, nwItemType, nwAlert @@ -93,7 +93,6 @@ class GuiMain(QMainWindow): self.docEditor = GuiDocEditor(self) self.viewMeta = GuiDocViewDetails(self) self.docViewer = GuiDocViewer(self) - self.searchBar = GuiSearchBar(self) self.treeMeta = GuiItemDetails(self) self.projView = GuiOutline(self) self.projMeta = GuiOutlineDetails(self) @@ -115,7 +114,6 @@ class GuiMain(QMainWindow): self.docEdit = QVBoxLayout() self.docEdit.setContentsMargins(0, 0, 0, 0) self.docEdit.setSpacing(self.mainConf.pxInt(2)) - self.docEdit.addWidget(self.searchBar) self.docEdit.addWidget(self.docEditor) self.editPane.setLayout(self.docEdit) @@ -168,7 +166,7 @@ class GuiMain(QMainWindow): self.splitView.setCollapsible(self.idxViewMeta, False) self.splitView.setVisible(False) - self.searchBar.setVisible(False) + self.docEditor.closeSearch() # Build the Tree View self.treeView.itemSelectionChanged.connect(self._treeSingleClick) @@ -1075,8 +1073,8 @@ class GuiMain(QMainWindow): """When the escape key is pressed somewhere in the main window, do the following, in order: """ - if self.searchBar.isVisible(): - self.searchBar.setVisible(False) + if self.docEditor.docSearch.isVisible(): + self.docEditor.closeSearch() return elif self.isZenMode: self.toggleZenMode() From 65492b13b74d5d51444f33e56bfa72b3db99d9e1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 15 Jun 2020 22:24:26 +0200 Subject: [PATCH 26/72] Some cleanup of unneeded code in main gui --- nw/guimain.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/nw/guimain.py b/nw/guimain.py index 1be860f0..15bcdb18 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -42,8 +42,8 @@ from PyQt5.QtWidgets import ( from nw.gui import ( GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus, - GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, - GuiProjectSettings, GuiProjectTree, GuiSessionLogView, GuiTheme + GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, GuiTheme, + GuiProjectSettings, GuiProjectTree, GuiSessionLogView ) from nw.core import NWProject, NWDoc, NWIndex from nw.constants import nwFiles, nwItemType, nwAlert @@ -110,13 +110,6 @@ class GuiMain(QMainWindow): self.treeBox.addWidget(self.treeMeta) self.treePane.setLayout(self.treeBox) - self.editPane = QWidget() - self.docEdit = QVBoxLayout() - self.docEdit.setContentsMargins(0, 0, 0, 0) - self.docEdit.setSpacing(self.mainConf.pxInt(2)) - self.docEdit.addWidget(self.docEditor) - self.editPane.setLayout(self.docEdit) - self.splitView = QSplitter(Qt.Vertical) self.splitView.addWidget(self.docViewer) self.splitView.addWidget(self.viewMeta) @@ -124,7 +117,7 @@ class GuiMain(QMainWindow): self.splitDocs = QSplitter(Qt.Horizontal) self.splitDocs.setOpaqueResize(False) - self.splitDocs.addWidget(self.editPane) + self.splitDocs.addWidget(self.docEditor) self.splitDocs.addWidget(self.splitView) self.splitOutline = QSplitter(Qt.Vertical) @@ -151,7 +144,7 @@ class GuiMain(QMainWindow): self.idxTree = self.splitMain.indexOf(self.treePane) self.idxMain = self.splitMain.indexOf(self.tabWidget) - self.idxEditor = self.splitDocs.indexOf(self.editPane) + self.idxEditor = self.splitDocs.indexOf(self.docEditor) self.idxViewer = self.splitDocs.indexOf(self.splitView) self.idxViewDoc = self.splitView.indexOf(self.docViewer) self.idxViewMeta = self.splitView.indexOf(self.viewMeta) @@ -599,15 +592,13 @@ class GuiMain(QMainWindow): return True def passDocumentAction(self, theAction): - """Pass on document action theAction to whatever document has - the focus. If no document has focus, the action is discarded. + """Pass on document action theAction to the document viewer if + it has focus, otherwise pass it to the document editor. """ - if self.docEditor.hasFocus(): - self.docEditor.docAction(theAction) - elif self.docViewer.hasFocus(): + if self.docViewer.hasFocus(): self.docViewer.docAction(theAction) else: - logger.debug("Document action requested, but no document has focus") + self.docEditor.docAction(theAction) return True ## From a4108222b01ab8561f60c3120e3faf9f41418aa8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Mon, 15 Jun 2020 22:24:50 +0200 Subject: [PATCH 27/72] Search functionality mostly working again --- nw/gui/doceditor.py | 88 +++++++++++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 23 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 1c0ac459..a9aaff5c 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -41,7 +41,7 @@ from PyQt5.QtGui import ( ) from PyQt5.QtWidgets import ( qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QWidget, QLabel, - QToolButton, QHBoxLayout, QGridLayout, QLineEdit, QPushButton, QFrame, QVBoxLayout + QToolButton, QHBoxLayout, QGridLayout, QLineEdit, QPushButton, QFrame, QVBoxLayout, QSizePolicy ) from nw.core import NWDoc @@ -337,9 +337,13 @@ class GuiDocEditor(QTextEdit): tH = self.docHeader.height() tT = cM - tH - rH = self.docSearch.height() - rW = self.docSearch.width() - rL = wW - sW - rW - tB + if self.docSearch.isVisible(): + rH = self.docSearch.height() + rW = self.docSearch.width() + rL = wW - sW - rW - tB + else: + rH = 0 + rL = 0 self.docHeader.setGeometry(tB, tB, tW, tH) self.docSearch.move(rL, tB) @@ -581,8 +585,7 @@ class GuiDocEditor(QTextEdit): def closeSearch(self): """Close the search box. """ - self.docSearch.setVisible(False) - self.updateDocMargins() + self.docSearch.closeSearch() return self.docSearch.isVisible() ## @@ -1159,24 +1162,36 @@ class GuiDocEditor(QTextEdit): """Searches for the next occurrence of the search bar text in the document. Wraps back to the top if not found. """ + if not self.docSearch.isVisible(): + self._beginSearch() + return + searchFor = self.docSearch.getSearchText() wasFound = self.find(searchFor) if not wasFound: theCursor = self.textCursor() theCursor.movePosition(QTextCursor.Start) self.setTextCursor(theCursor) + self.find(searchFor) + return def _findPrev(self): """Searches for the previous occurrence of the search bar text in the document. Wraps back to the end if not found. """ + if not self.docSearch.isVisible(): + self._beginSearch() + return + searchFor = self.docSearch.getSearchText() wasFound = self.find(searchFor, QTextDocument.FindBackward) if not wasFound: theCursor = self.textCursor() theCursor.movePosition(QTextCursor.End) self.setTextCursor(theCursor) + self.find(searchFor, QTextDocument.FindBackward) + return def _replaceNext(self): @@ -1184,6 +1199,10 @@ class GuiDocEditor(QTextEdit): the document and replaces it with the replace text. Wraps back to the top if not found. """ + if not self.docSearch.isVisible(): + self._beginSearch() + return + theCursor = self.textCursor() searchFor = self.docSearch.getSearchText() replWith = self.docSearch.getReplaceText() @@ -1200,6 +1219,7 @@ class GuiDocEditor(QTextEdit): )) if searchFor != "": self._findNext() + return def _setupSpellChecking(self): @@ -1266,32 +1286,45 @@ class GuiDocEditSearch(QWidget): self.repVisible = False + mPx = self.mainConf.pxInt(6) fPx = int(0.9*self.theTheme.fontPixelSize) boxFont = self.theTheme.guiFont boxFont.setPointSizeF(0.9*self.theTheme.fontPointSize) - # self.setContentsMargins(0, 0, 0, 0) - # self.setFrameStyle(QFrame.Box) + self.setContentsMargins(mPx, mPx, mPx, mPx) self.setAutoFillBackground(True) self.mainBox = QGridLayout(self) self.setLayout(self.mainBox) + # Text Boxes + # ========== self.searchBox = QLineEdit() self.searchBox.setFont(boxFont) + self.searchBox.returnPressed.connect(self._doSearch) - self.replaceBox = QLineEdit() + self.replaceBox = QLineEdit() self.replaceBox.setFont(boxFont) + self.replaceBox.returnPressed.connect(self._doSearch) + + # Buttons + # ======= + bPx = self.searchBox.sizeHint().height() + + self.searchButton = QPushButton(self.theTheme.getIcon("search"),"") + self.searchButton.setFixedSize(QSize(bPx, bPx)) + self.searchButton.setToolTip("Find in current document") + self.searchButton.clicked.connect(self._doSearch) + + self.replaceButton = QPushButton(self.theTheme.getIcon("search-replace"),"") + self.replaceButton.setFixedSize(QSize(bPx, bPx)) + self.replaceButton.setToolTip("Find and replace in current document") + self.replaceButton.clicked.connect(self._doReplace) self.closeButton = QPushButton(self.theTheme.getIcon("close"),"") - self.searchButton = QPushButton(self.theTheme.getIcon("search"),"") - self.replaceButton = QPushButton(self.theTheme.getIcon("search-replace"),"") - + self.closeButton.setFixedSize(QSize(bPx, bPx)) + self.closeButton.setToolTip("Close search tool") self.closeButton.clicked.connect(self._doClose) - self.searchButton.clicked.connect(self._doSearch) - self.replaceButton.clicked.connect(self._doReplace) - self.searchBox.returnPressed.connect(self._doSearch) - self.replaceBox.returnPressed.connect(self._doSearch) self.mainBox.addWidget(self.searchBox, 0, 0) self.mainBox.addWidget(self.searchButton, 0, 1) @@ -1304,20 +1337,29 @@ class GuiDocEditSearch(QWidget): self.mainBox.setColumnStretch(2, 0) self.mainBox.setColumnStretch(3, 0) self.mainBox.setColumnStretch(4, 0) - self.mainBox.setVerticalSpacing(0) - self.mainBox.setHorizontalSpacing(2) - # self.mainBox.setContentsMargins(0, 0, 0, 0) + self.mainBox.setSpacing(self.mainConf.pxInt(2)) + self.mainBox.setContentsMargins(0, 0, 0, 0) - boxWidth = 16*self.theTheme.textNWidth + boxWidth = 18*self.theTheme.textNWidth self.searchBox.setFixedWidth(boxWidth) self.replaceBox.setFixedWidth(boxWidth) + self.adjustSize() - # self._replaceVisible(False) + self._replaceVisible(False) logger.debug("GuiDocEditSearch initialisation complete") return + def closeSearch(self): + """Close the search box. + """ + self._replaceVisible(False) + self.setVisible(False) + self.docEditor.updateDocMargins() + self.docEditor.setFocus() + return + ## # Get and Set Functions ## @@ -1358,8 +1400,7 @@ class GuiDocEditSearch(QWidget): def _doClose(self): """Hide the search/replace bar. """ - self._replaceVisible(False) - self.docEditor.closeSearch() + self.closeSearch() return def _doSearch(self): @@ -1388,6 +1429,7 @@ class GuiDocEditSearch(QWidget): self.replaceBox.setVisible(isVisible) self.replaceButton.setVisible(isVisible) self.repVisible = isVisible + self.adjustSize() return True # END Class GuiDocEditSearch From 2c5692085e3b37a9cab1b6403d8235893705db34 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 16 Jun 2020 00:20:59 +0200 Subject: [PATCH 28/72] Use viewport margins for all scaling of editor doc margins --- nw/gui/doceditor.py | 34 ++++++++++------------------------ nw/guimain.py | 2 -- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index a9aaff5c..53ba7bc9 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -101,6 +101,7 @@ class GuiDocEditor(QTextEdit): # Editor State self.hasSelection = False self.setMinimumWidth(self.mainConf.pxInt(300)) + self.setAutoFillBackground(True) self.setAcceptRichText(False) # Custom Shortcuts @@ -185,12 +186,15 @@ class GuiDocEditor(QTextEdit): self.setFont(theFont) docPalette = self.palette() + docPalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) docPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack)) docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) self.setPalette(docPalette) # Set default text margins - self.qDocument.setDocumentMargin(self.mainConf.getTextMargin()) + cM = self.mainConf.getTextMargin() + self.qDocument.setDocumentMargin(0) + self.setViewportMargins(cM, cM, cM, cM) # Also set the document text options for the document text flow theOpt = QTextOption() @@ -326,7 +330,7 @@ class GuiDocEditor(QTextEdit): tW = self.mainConf.getZenWidth() else: tW = self.mainConf.getTextWidth() - tM = int((wW - sW - tW)/2) + tM = (wW - sW - tW)//2 if tM < cM: tM = cM else: @@ -335,36 +339,18 @@ class GuiDocEditor(QTextEdit): tB = self.frameWidth() tW = wW - 2*tB - sW tH = self.docHeader.height() - tT = cM - tH + self.docHeader.setGeometry(tB, tB, tW, tH) if self.docSearch.isVisible(): rH = self.docSearch.height() rW = self.docSearch.width() rL = wW - sW - rW - tB + self.docSearch.move(rL, tB) else: rH = 0 - rL = 0 - self.docHeader.setGeometry(tB, tB, tW, tH) - self.docSearch.move(rL, tB) - self.setViewportMargins(0, tH, 0, 0) - - docFormat = self.qDocument.rootFrame().frameFormat() - docFormat.setLeftMargin(tM) - docFormat.setTopMargin(max(0, tT, rH)) - - # Updating root frame triggers a QTextDocument->contentsChange - # signal, which we do not want as it re-runs the syntax - # highlighter and spell checker, so we block it briefly. - # We then emit a signal that does not trigger re-highlighting. - self.qDocument.blockSignals(True) - self.qDocument.rootFrame().setFrameFormat(docFormat) - self.qDocument.blockSignals(False) - - # The line below causes issues with large documents as it - # triggers an early repaint that seems to only render a part of - # the document. Leaving it here as a warning for now. - # self.qDocument.contentsChange.emit(0, 0, 0) + # print(tM, tH, rH, max(tM, tH, rH)) + self.setViewportMargins(tM, max(cM, tH, rH), tM, cM) return diff --git a/nw/guimain.py b/nw/guimain.py index 15bcdb18..5bfad545 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -116,7 +116,6 @@ class GuiMain(QMainWindow): self.splitView.setSizes(self.mainConf.getViewPanePos()) self.splitDocs = QSplitter(Qt.Horizontal) - self.splitDocs.setOpaqueResize(False) self.splitDocs.addWidget(self.docEditor) self.splitDocs.addWidget(self.splitView) @@ -135,7 +134,6 @@ class GuiMain(QMainWindow): xCM = self.mainConf.pxInt(4) self.splitMain = QSplitter(Qt.Horizontal) self.splitMain.setContentsMargins(xCM, xCM, xCM, xCM) - self.splitMain.setOpaqueResize(False) self.splitMain.addWidget(self.treePane) self.splitMain.addWidget(self.tabWidget) self.splitMain.setSizes(self.mainConf.getMainPanePos()) From 8755434f768b2ba57ab01e0cef70ccc21364f453 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 16 Jun 2020 20:44:24 +0200 Subject: [PATCH 29/72] Fix weird issue with text editor capturing enter key without having focus --- nw/gui/doceditor.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 53ba7bc9..b2f2e49d 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -591,6 +591,9 @@ class GuiDocEditor(QTextEdit): as it is triggered on every keypress when typing. """ self.hasSelection = self.textCursor().hasSelection() + if not self.hasFocus(): + # Block the event when the focus is on the search bar. + return if keyEvent.modifiers() == Qt.ShiftModifier: theKey = keyEvent.key() From ad8463543ee75b8a28651b39c482d3462e8fc8d7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 16 Jun 2020 21:35:57 +0200 Subject: [PATCH 30/72] Merged find next and prev functions, and added optrions for case sensitivity and whole words --- nw/gui/doceditor.py | 61 +++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index b2f2e49d..b3f913c9 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -34,7 +34,7 @@ import nw from time import time -from PyQt5.QtCore import Qt, QSize, QThread, QTimer, pyqtSlot +from PyQt5.QtCore import Qt, QSize, QThread, QTimer, pyqtSlot, QRegExp from PyQt5.QtGui import ( QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette, QTextDocument, QCursor @@ -527,7 +527,7 @@ class GuiDocEditor(QTextEdit): elif theAction == nwDocAction.GO_NEXT: self._findNext() elif theAction == nwDocAction.GO_PREV: - self._findPrev() + self._findNext(isBackward=True) elif theAction == nwDocAction.REPL_NEXT: self._replaceNext() elif theAction == nwDocAction.BLOCK_H1: @@ -1147,39 +1147,31 @@ class GuiDocEditor(QTextEdit): self.docSearch.setReplaceText("") return - def _findNext(self): - """Searches for the next occurrence of the search bar text in - the document. Wraps back to the top if not found. + def _findNext(self, isBackward=False): + """Searches for the next or previous occurrence of the search + bar text in the document. Wraps around if not found. """ if not self.docSearch.isVisible(): self._beginSearch() return - searchFor = self.docSearch.getSearchText() - wasFound = self.find(searchFor) - if not wasFound: - theCursor = self.textCursor() - theCursor.movePosition(QTextCursor.Start) - self.setTextCursor(theCursor) - self.find(searchFor) - - return - - def _findPrev(self): - """Searches for the previous occurrence of the search bar text - in the document. Wraps back to the end if not found. - """ - if not self.docSearch.isVisible(): - self._beginSearch() - return + findOpt = QTextDocument.FindFlag(0) + if isBackward: + findOpt |= QTextDocument.FindBackward + if self.docSearch.isCaseSense: + findOpt |= QTextDocument.FindCaseSensitively + if self.docSearch.isWholeWord: + findOpt |= QTextDocument.FindWholeWords searchFor = self.docSearch.getSearchText() - wasFound = self.find(searchFor, QTextDocument.FindBackward) + wasFound = self.find(searchFor, findOpt) if not wasFound: theCursor = self.textCursor() - theCursor.movePosition(QTextCursor.End) + theCursor.movePosition( + QTextCursor.End if isBackward else QTextCursor.Start + ) self.setTextCursor(theCursor) - self.find(searchFor, QTextDocument.FindBackward) + self.find(searchFor, findOpt) return @@ -1195,18 +1187,24 @@ class GuiDocEditor(QTextEdit): theCursor = self.textCursor() searchFor = self.docSearch.getSearchText() replWith = self.docSearch.getReplaceText() - if theCursor.hasSelection() and theCursor.selectedText() == searchFor: - xPos = theCursor.selectionStart() + selText = theCursor.selectedText() + + if not self.docSearch.isCaseSense: + searchFor = searchFor.lower() + selText = selText.lower() + + if theCursor.hasSelection() and selText == searchFor: theCursor.beginEditBlock() theCursor.removeSelectedText() theCursor.insertText(replWith) theCursor.endEditBlock() - theCursor.setPosition(xPos) + theCursor.setPosition(theCursor.selectionEnd()) self.setTextCursor(theCursor) logger.verbose("Replaced occurrence of '%s' with '%s' on line %d" % ( searchFor, replWith, theCursor.blockNumber() )) - if searchFor != "": + + if searchFor: self._findNext() return @@ -1273,7 +1271,10 @@ class GuiDocEditSearch(QWidget): self.theProject = docEditor.theProject self.theTheme = docEditor.theTheme - self.repVisible = False + self.repVisible = False + self.isCaseSense = False + self.isWholeWord = False + self.isRegEx = True mPx = self.mainConf.pxInt(6) fPx = int(0.9*self.theTheme.fontPixelSize) From f28a9f452321dfd5d5f6aa8389518e644114cdfa Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 16 Jun 2020 22:32:08 +0200 Subject: [PATCH 31/72] Regex searching also works now --- nw/gui/doceditor.py | 46 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index b3f913c9..8ad72649 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -1274,7 +1274,7 @@ class GuiDocEditSearch(QWidget): self.repVisible = False self.isCaseSense = False self.isWholeWord = False - self.isRegEx = True + self.isRegEx = False mPx = self.mainConf.pxInt(6) fPx = int(0.9*self.theTheme.fontPixelSize) @@ -1337,6 +1337,24 @@ class GuiDocEditSearch(QWidget): self._replaceVisible(False) + # Construct Box Colours + qPalette = self.searchBox.palette() + baseCol = qPalette.base().color() + rCol = baseCol.redF() + 0.1 + gCol = baseCol.greenF() - 0.1 + bCol = baseCol.blueF() - 0.1 + + mCol = max(rCol, gCol, bCol, 1.0) + errCol = QColor() + errCol.setRedF(rCol/mCol) + errCol.setGreenF(gCol/mCol) + errCol.setBlueF(bCol/mCol) + + self.rxCol = { + True : baseCol, + False : errCol + } + logger.debug("GuiDocEditSearch initialisation complete") return @@ -1362,6 +1380,8 @@ class GuiDocEditSearch(QWidget): self.setVisible(True) self.searchBox.setText(theText) self.searchBox.setFocus() + if self.isRegEx: + self._alertSearchValid(True) logger.verbose("Setting search text to '%s'" % theText) return True @@ -1374,9 +1394,20 @@ class GuiDocEditSearch(QWidget): return True def getSearchText(self): - """Return the current search text. + """Return the current search text either as text or as a regular + expression object. """ - return self.searchBox.text() + theText = self.searchBox.text() + if self.isRegEx and self.mainConf.verQtValue >= 50300: + if self.isCaseSense: + rxCase = Qt.CaseSensitive + else: + rxCase = Qt.CaseInsensitive + theRegEx = QRegExp(theText, rxCase) + self._alertSearchValid(theRegEx.isValid()) + return theRegEx + + return theText def getReplaceText(self): """Return the current replace text. @@ -1413,6 +1444,15 @@ class GuiDocEditSearch(QWidget): # Internal Functions ## + def _alertSearchValid(self, isValid): + """Highlight the search box to indicate the search string is or + isn't valid. Take the colour from the replace box. + """ + qPalette = self.replaceBox.palette() + qPalette.setColor(QPalette.Base, self.rxCol[isValid]) + self.searchBox.setPalette(qPalette) + return + def _replaceVisible(self, isVisible): """Set the visibility of all the replace widgets. """ From abc14db7d112dc6d5781446d54ba38bcdd3631e6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Tue, 16 Jun 2020 23:07:18 +0200 Subject: [PATCH 32/72] Search options can now be set in a popup menu --- nw/gui/doceditor.py | 46 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 8ad72649..9deb071b 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -37,7 +37,7 @@ from time import time from PyQt5.QtCore import Qt, QSize, QThread, QTimer, pyqtSlot, QRegExp from PyQt5.QtGui import ( QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette, - QTextDocument, QCursor + QTextDocument, QCursor, QIcon ) from PyQt5.QtWidgets import ( qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QWidget, QLabel, @@ -1297,6 +1297,32 @@ class GuiDocEditSearch(QWidget): self.replaceBox.setFont(boxFont) self.replaceBox.returnPressed.connect(self._doSearch) + self.searchOpt = QMenu(self) + + self.toggleCase = QAction("Case Sensitive", self) + self.toggleCase.setFont(boxFont) + self.toggleCase.setCheckable(True) + self.toggleCase.toggled.connect(self._doToggleCase) + self.searchOpt.addAction(self.toggleCase) + + self.toggleWord = QAction("Whole Words Only", self) + self.toggleWord.setFont(boxFont) + self.toggleWord.setCheckable(True) + self.toggleWord.toggled.connect(self._doToggleWord) + self.searchOpt.addAction(self.toggleWord) + + self.toggleRegEx = QAction("RegEx Mode", self) + self.toggleRegEx.setFont(boxFont) + self.toggleRegEx.setCheckable(True) + self.toggleRegEx.toggled.connect(self._doToggleRegEx) + self.searchOpt.addAction(self.toggleRegEx) + + self.optButton = QAction(self) + self.optButton.setIcon(self.theTheme.getIcon("edit")) + self.optButton.setMenu(self.searchOpt) + + self.searchBox.addAction(self.optButton, QLineEdit.TrailingPosition) + # Buttons # ======= bPx = self.searchBox.sizeHint().height() @@ -1440,6 +1466,24 @@ class GuiDocEditSearch(QWidget): self.docEditor.docAction(nwDocAction.REPL_NEXT) return + def _doToggleCase(self, theState): + """Enable/disable case sensitive mode. + """ + self.isCaseSense = theState + return + + def _doToggleWord(self, theState): + """Enable/disable whole word search mode. + """ + self.isWholeWord = theState + return + + def _doToggleRegEx(self, theState): + """Enable/disable regular expression search mode. + """ + self.isRegEx = theState + return + ## # Internal Functions ## From ef0d1fc89cbf42e761aa3d8520de31215b3fe458 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 16:57:20 +0200 Subject: [PATCH 33/72] Added icons for the search tool --- .../icons/fallback/search_cancel-dark.svg | 31 +++++++ nw/assets/icons/fallback/search_cancel.svg | 31 +++++++ nw/assets/icons/fallback/search_case-dark.svg | 49 ++++++++++++ nw/assets/icons/fallback/search_case.svg | 48 +++++++++++ nw/assets/icons/fallback/search_loop-dark.svg | 31 +++++++ nw/assets/icons/fallback/search_loop.svg | 31 +++++++ .../icons/fallback/search_preserve-dark.svg | 40 ++++++++++ nw/assets/icons/fallback/search_preserve.svg | 40 ++++++++++ .../icons/fallback/search_project-dark.svg | 54 +++++++++++++ nw/assets/icons/fallback/search_project.svg | 54 +++++++++++++ .../icons/fallback/search_regex-dark.svg | 49 ++++++++++++ nw/assets/icons/fallback/search_regex.svg | 48 +++++++++++ nw/assets/icons/fallback/search_word-dark.svg | 45 +++++++++++ nw/assets/icons/fallback/search_word.svg | 45 +++++++++++ nw/gui/theme.py | 80 ++++++++++--------- 15 files changed, 639 insertions(+), 37 deletions(-) create mode 100644 nw/assets/icons/fallback/search_cancel-dark.svg create mode 100644 nw/assets/icons/fallback/search_cancel.svg create mode 100644 nw/assets/icons/fallback/search_case-dark.svg create mode 100644 nw/assets/icons/fallback/search_case.svg create mode 100644 nw/assets/icons/fallback/search_loop-dark.svg create mode 100644 nw/assets/icons/fallback/search_loop.svg create mode 100644 nw/assets/icons/fallback/search_preserve-dark.svg create mode 100644 nw/assets/icons/fallback/search_preserve.svg create mode 100644 nw/assets/icons/fallback/search_project-dark.svg create mode 100644 nw/assets/icons/fallback/search_project.svg create mode 100644 nw/assets/icons/fallback/search_regex-dark.svg create mode 100644 nw/assets/icons/fallback/search_regex.svg create mode 100644 nw/assets/icons/fallback/search_word-dark.svg create mode 100644 nw/assets/icons/fallback/search_word.svg diff --git a/nw/assets/icons/fallback/search_cancel-dark.svg b/nw/assets/icons/fallback/search_cancel-dark.svg new file mode 100644 index 00000000..5a0b2182 --- /dev/null +++ b/nw/assets/icons/fallback/search_cancel-dark.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/fallback/search_cancel.svg b/nw/assets/icons/fallback/search_cancel.svg new file mode 100644 index 00000000..48630300 --- /dev/null +++ b/nw/assets/icons/fallback/search_cancel.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/fallback/search_case-dark.svg b/nw/assets/icons/fallback/search_case-dark.svg new file mode 100644 index 00000000..4701dfe1 --- /dev/null +++ b/nw/assets/icons/fallback/search_case-dark.svg @@ -0,0 +1,49 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/nw/assets/icons/fallback/search_case.svg b/nw/assets/icons/fallback/search_case.svg new file mode 100644 index 00000000..ce7e7673 --- /dev/null +++ b/nw/assets/icons/fallback/search_case.svg @@ -0,0 +1,48 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/nw/assets/icons/fallback/search_loop-dark.svg b/nw/assets/icons/fallback/search_loop-dark.svg new file mode 100644 index 00000000..60baf3c2 --- /dev/null +++ b/nw/assets/icons/fallback/search_loop-dark.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/fallback/search_loop.svg b/nw/assets/icons/fallback/search_loop.svg new file mode 100644 index 00000000..4d6d85c5 --- /dev/null +++ b/nw/assets/icons/fallback/search_loop.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/nw/assets/icons/fallback/search_preserve-dark.svg b/nw/assets/icons/fallback/search_preserve-dark.svg new file mode 100644 index 00000000..e59a8616 --- /dev/null +++ b/nw/assets/icons/fallback/search_preserve-dark.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + image/svg+xml + + + + + + diff --git a/nw/assets/icons/fallback/search_preserve.svg b/nw/assets/icons/fallback/search_preserve.svg new file mode 100644 index 00000000..bcfd6082 --- /dev/null +++ b/nw/assets/icons/fallback/search_preserve.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + image/svg+xml + + + + + + diff --git a/nw/assets/icons/fallback/search_project-dark.svg b/nw/assets/icons/fallback/search_project-dark.svg new file mode 100644 index 00000000..552fed1f --- /dev/null +++ b/nw/assets/icons/fallback/search_project-dark.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/nw/assets/icons/fallback/search_project.svg b/nw/assets/icons/fallback/search_project.svg new file mode 100644 index 00000000..ee16c528 --- /dev/null +++ b/nw/assets/icons/fallback/search_project.svg @@ -0,0 +1,54 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/nw/assets/icons/fallback/search_regex-dark.svg b/nw/assets/icons/fallback/search_regex-dark.svg new file mode 100644 index 00000000..1a81cfe8 --- /dev/null +++ b/nw/assets/icons/fallback/search_regex-dark.svg @@ -0,0 +1,49 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/nw/assets/icons/fallback/search_regex.svg b/nw/assets/icons/fallback/search_regex.svg new file mode 100644 index 00000000..2bbe1c22 --- /dev/null +++ b/nw/assets/icons/fallback/search_regex.svg @@ -0,0 +1,48 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/nw/assets/icons/fallback/search_word-dark.svg b/nw/assets/icons/fallback/search_word-dark.svg new file mode 100644 index 00000000..650c7dfc --- /dev/null +++ b/nw/assets/icons/fallback/search_word-dark.svg @@ -0,0 +1,45 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/nw/assets/icons/fallback/search_word.svg b/nw/assets/icons/fallback/search_word.svg new file mode 100644 index 00000000..8f0aed52 --- /dev/null +++ b/nw/assets/icons/fallback/search_word.svg @@ -0,0 +1,45 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/nw/gui/theme.py b/nw/gui/theme.py index 30bcb621..7df0c560 100644 --- a/nw/gui/theme.py +++ b/nw/gui/theme.py @@ -495,52 +495,58 @@ class GuiIcons: ICON_MAP = { # Project and GUI icons - "cls_none" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_novel" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_plot" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_character" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_world" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_timeline" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_object" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_entity" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_custom" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "cls_trash" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), - "proj_document" : (QStyle.SP_FileIcon, "x-office-document"), - "proj_folder" : (QStyle.SP_DirIcon, "folder"), - "proj_orphan" : (QStyle.SP_MessageBoxWarning, "dialog-warning"), - "proj_nwx" : (None, None), - "status_lang" : (None, None), - "status_time" : (None, None), - "status_stats" : (None, None), - "doc_h1" : (QStyle.SP_FileIcon, "x-office-document"), - "doc_h2" : (QStyle.SP_FileIcon, "x-office-document"), - "doc_h3" : (QStyle.SP_FileIcon, "x-office-document"), - "doc_h4" : (QStyle.SP_FileIcon, "x-office-document"), + "cls_none" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_novel" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_plot" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_character" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_world" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_timeline" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_object" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_entity" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_custom" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "cls_trash" : (QStyle.SP_DriveHDIcon, "drive-harddisk"), + "proj_document" : (QStyle.SP_FileIcon, "x-office-document"), + "proj_folder" : (QStyle.SP_DirIcon, "folder"), + "proj_orphan" : (QStyle.SP_MessageBoxWarning, "dialog-warning"), + "proj_nwx" : (None, None), + "status_lang" : (None, None), + "status_time" : (None, None), + "status_stats" : (None, None), + "doc_h1" : (QStyle.SP_FileIcon, "x-office-document"), + "doc_h2" : (QStyle.SP_FileIcon, "x-office-document"), + "doc_h3" : (QStyle.SP_FileIcon, "x-office-document"), + "doc_h4" : (QStyle.SP_FileIcon, "x-office-document"), + "search_case" : (None, None), + "search_regex" : (None, None), + "search_word" : (None, None), + "search_loop" : (None, None), + "search_project" : (None, None), + "search_cancel" : (None, None), + "search_preserve" : (None, None), ## General Button Icons "folder-open" : (QStyle.SP_DirOpenIcon, "folder-open"), "delete" : (QStyle.SP_DialogDiscardButton, "edit-delete"), - "add" : (None, "list-add"), - "remove" : (None, "list-remove"), "close" : (QStyle.SP_DialogCloseButton, "window-close"), "done" : (QStyle.SP_DialogApplyButton, None), - "search" : (None, "edit-find"), - "search-replace" : (None, "edit-find-replace"), "clear" : (QStyle.SP_LineEditClearButton, "clear_left"), "save" : (QStyle.SP_DialogSaveButton, "document-save"), - "edit" : (None, None), - "check" : (None, None), - "cross" : (None, None), - "hash" : (None, None), - "maximise" : (None, None), - "minimise" : (None, None), - "refresh" : (None, None), - "reference" : (None, None), - "sticky-on" : (None, None), - "sticky-off" : (None, None), + "add" : (None, "list-add"), + "remove" : (None, "list-remove"), + "search" : (None, "edit-find"), + "search-replace" : (None, "edit-find-replace"), + "edit" : (None, None), + "check" : (None, None), + "cross" : (None, None), + "hash" : (None, None), + "maximise" : (None, None), + "minimise" : (None, None), + "refresh" : (None, None), + "reference" : (None, None), - ## Other Icons - "warning" : (QStyle.SP_MessageBoxWarning, "dialog-warning"), + ## Switches + "sticky-on" : (None, None), + "sticky-off" : (None, None), } DECO_MAP = { From c3c0bd7abef7bcd95ceaba7c8d3312c41a06ccc9 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 16:57:49 +0200 Subject: [PATCH 34/72] Added search options as a toolbar instead --- nw/gui/doceditor.py | 153 ++++++++++++++++++++++++++++++++------------ 1 file changed, 111 insertions(+), 42 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 9deb071b..baa0b881 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -40,7 +40,7 @@ from PyQt5.QtGui import ( QTextDocument, QCursor, QIcon ) from PyQt5.QtWidgets import ( - qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QWidget, QLabel, + qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QWidget, QLabel, QToolBar, QToolButton, QHBoxLayout, QGridLayout, QLineEdit, QPushButton, QFrame, QVBoxLayout, QSizePolicy ) @@ -344,8 +344,8 @@ class GuiDocEditor(QTextEdit): if self.docSearch.isVisible(): rH = self.docSearch.height() rW = self.docSearch.width() - rL = wW - sW - rW - tB - self.docSearch.move(rL, tB) + rL = wW - sW - rW - 2*tB + self.docSearch.move(rL, 2*tB) else: rH = 0 @@ -1149,7 +1149,8 @@ class GuiDocEditor(QTextEdit): def _findNext(self, isBackward=False): """Searches for the next or previous occurrence of the search - bar text in the document. Wraps around if not found. + bar text in the document. Wraps around if not found and loop is + enabled, or continues to next file if next file is enabled. """ if not self.docSearch.isVisible(): self._beginSearch() @@ -1165,7 +1166,7 @@ class GuiDocEditor(QTextEdit): searchFor = self.docSearch.getSearchText() wasFound = self.find(searchFor, findOpt) - if not wasFound: + if not wasFound and self.docSearch.doLoop: theCursor = self.textCursor() theCursor.movePosition( QTextCursor.End if isBackward else QTextCursor.Start @@ -1177,8 +1178,8 @@ class GuiDocEditor(QTextEdit): def _replaceNext(self): """Searches for the next occurrence of the search bar text in - the document and replaces it with the replace text. Wraps back - to the top if not found. + the document and replaces it with the replace text. Calls search + next automatically when done. """ if not self.docSearch.isVisible(): self._beginSearch() @@ -1258,10 +1259,10 @@ class BackgroundWordCounter(QThread): # Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport # =============================================================================================== # -class GuiDocEditSearch(QWidget): +class GuiDocEditSearch(QFrame): def __init__(self, docEditor): - QWidget.__init__(self, docEditor) + QFrame.__init__(self, docEditor) logger.debug("Initialising GuiDocEditSearch ...") @@ -1275,14 +1276,19 @@ class GuiDocEditSearch(QWidget): self.isCaseSense = False self.isWholeWord = False self.isRegEx = False + self.doLoop = False + self.doNextFile = False + self.doPreserve = False mPx = self.mainConf.pxInt(6) fPx = int(0.9*self.theTheme.fontPixelSize) + tPx = int(0.8*self.theTheme.fontPixelSize) boxFont = self.theTheme.guiFont boxFont.setPointSizeF(0.9*self.theTheme.fontPointSize) - self.setContentsMargins(mPx, mPx, mPx, mPx) + self.setContentsMargins(0, 0, 0, 0) self.setAutoFillBackground(True) + self.setFrameStyle(QFrame.StyledPanel | QFrame.Plain) self.mainBox = QGridLayout(self) self.setLayout(self.mainBox) @@ -1291,42 +1297,86 @@ class GuiDocEditSearch(QWidget): # ========== self.searchBox = QLineEdit() self.searchBox.setFont(boxFont) + self.searchBox.setPlaceholderText("Search") self.searchBox.returnPressed.connect(self._doSearch) self.replaceBox = QLineEdit() self.replaceBox.setFont(boxFont) + self.replaceBox.setPlaceholderText("Replace") self.replaceBox.returnPressed.connect(self._doSearch) - self.searchOpt = QMenu(self) + self.searchOpt = QToolBar(self) + self.searchOpt.setToolButtonStyle(Qt.ToolButtonIconOnly) + self.searchOpt.setIconSize(QSize(tPx, tPx)) + self.searchOpt.setContentsMargins(0, 0, 0, 0) + self.searchOpt.setStyleSheet(r"QToolBar {padding: 0;}") + + self.searchLabel = QLabel("Search") + self.searchLabel.setFont(boxFont) + self.searchLabel.setIndent(self.mainConf.pxInt(6)) self.toggleCase = QAction("Case Sensitive", self) - self.toggleCase.setFont(boxFont) + self.toggleCase.setToolTip("Match case") + self.toggleCase.setIcon(self.theTheme.getIcon("search_case")) self.toggleCase.setCheckable(True) self.toggleCase.toggled.connect(self._doToggleCase) self.searchOpt.addAction(self.toggleCase) self.toggleWord = QAction("Whole Words Only", self) - self.toggleWord.setFont(boxFont) + self.toggleWord.setToolTip("Match whole words") + self.toggleWord.setIcon(self.theTheme.getIcon("search_word")) self.toggleWord.setCheckable(True) self.toggleWord.toggled.connect(self._doToggleWord) self.searchOpt.addAction(self.toggleWord) self.toggleRegEx = QAction("RegEx Mode", self) - self.toggleRegEx.setFont(boxFont) + self.toggleRegEx.setToolTip("Use regular expressions") + self.toggleRegEx.setIcon(self.theTheme.getIcon("search_regex")) self.toggleRegEx.setCheckable(True) self.toggleRegEx.toggled.connect(self._doToggleRegEx) self.searchOpt.addAction(self.toggleRegEx) - self.optButton = QAction(self) - self.optButton.setIcon(self.theTheme.getIcon("edit")) - self.optButton.setMenu(self.searchOpt) + self.toggleLoop = QAction("Loop Search", self) + self.toggleLoop.setToolTip("Loop the search when reaching the end") + self.toggleLoop.setIcon(self.theTheme.getIcon("search_loop")) + self.toggleLoop.setCheckable(True) + self.toggleLoop.toggled.connect(self._doToggleLoop) + self.searchOpt.addAction(self.toggleLoop) - self.searchBox.addAction(self.optButton, QLineEdit.TrailingPosition) + self.toggleProject = QAction("Search Next File", self) + self.toggleProject.setToolTip("Continue searching in the next file") + self.toggleProject.setIcon(self.theTheme.getIcon("search_project")) + self.toggleProject.setCheckable(True) + self.toggleProject.toggled.connect(self._doToggleProject) + self.searchOpt.addAction(self.toggleProject) + + self.searchOpt.addSeparator() + + self.togglePreserve = QAction("Preserve Case", self) + self.togglePreserve.setToolTip("Preserve case on replace") + self.togglePreserve.setIcon(self.theTheme.getIcon("search_preserve")) + self.togglePreserve.setCheckable(True) + self.togglePreserve.toggled.connect(self._doTogglePreserve) + self.searchOpt.addAction(self.togglePreserve) + + self.searchOpt.addSeparator() + + self.cancelSearch = QAction("Close Search", self) + self.cancelSearch.setToolTip("Close the search box [Esc]") + self.cancelSearch.setIcon(self.theTheme.getIcon("search_cancel")) + self.cancelSearch.triggered.connect(self._doClose) + self.searchOpt.addAction(self.cancelSearch) # Buttons # ======= bPx = self.searchBox.sizeHint().height() + self.showReplace = QToolButton(self) + self.showReplace.setArrowType(Qt.RightArrow) + self.showReplace.setCheckable(True) + self.showReplace.setStyleSheet(r"QToolButton {border: none; background: transparent;}") + self.showReplace.toggled.connect(self._doToggleReplace) + self.searchButton = QPushButton(self.theTheme.getIcon("search"),"") self.searchButton.setFixedSize(QSize(bPx, bPx)) self.searchButton.setToolTip("Find in current document") @@ -1337,16 +1387,13 @@ class GuiDocEditSearch(QWidget): self.replaceButton.setToolTip("Find and replace in current document") self.replaceButton.clicked.connect(self._doReplace) - self.closeButton = QPushButton(self.theTheme.getIcon("close"),"") - self.closeButton.setFixedSize(QSize(bPx, bPx)) - self.closeButton.setToolTip("Close search tool") - self.closeButton.clicked.connect(self._doClose) - - self.mainBox.addWidget(self.searchBox, 0, 0) - self.mainBox.addWidget(self.searchButton, 0, 1) - self.mainBox.addWidget(self.closeButton, 0, 2) - self.mainBox.addWidget(self.replaceBox, 1, 0) - self.mainBox.addWidget(self.replaceButton, 1, 1) + self.mainBox.addWidget(self.searchLabel, 0, 0, 1, 2, Qt.AlignLeft) + self.mainBox.addWidget(self.searchOpt, 0, 2, 1, 2, Qt.AlignRight) + self.mainBox.addWidget(self.showReplace, 1, 0, 1, 1) + self.mainBox.addWidget(self.searchBox, 1, 1, 1, 2) + self.mainBox.addWidget(self.searchButton, 1, 3, 1, 1) + self.mainBox.addWidget(self.replaceBox, 2, 1, 1, 2) + self.mainBox.addWidget(self.replaceButton, 2, 3, 1, 1) self.mainBox.setColumnStretch(0, 1) self.mainBox.setColumnStretch(1, 0) @@ -1354,14 +1401,14 @@ class GuiDocEditSearch(QWidget): self.mainBox.setColumnStretch(3, 0) self.mainBox.setColumnStretch(4, 0) self.mainBox.setSpacing(self.mainConf.pxInt(2)) - self.mainBox.setContentsMargins(0, 0, 0, 0) + self.mainBox.setContentsMargins(mPx, mPx, mPx, mPx) - boxWidth = 18*self.theTheme.textNWidth + boxWidth = self.mainConf.pxInt(200) self.searchBox.setFixedWidth(boxWidth) self.replaceBox.setFixedWidth(boxWidth) self.adjustSize() - self._replaceVisible(False) + self._doToggleReplace(False) # Construct Box Colours qPalette = self.searchBox.palette() @@ -1388,7 +1435,7 @@ class GuiDocEditSearch(QWidget): def closeSearch(self): """Close the search box. """ - self._replaceVisible(False) + self.showReplace.setChecked(False) self.setVisible(False) self.docEditor.updateDocMargins() self.docEditor.setFocus() @@ -1414,7 +1461,7 @@ class GuiDocEditSearch(QWidget): def setReplaceText(self, theText): """Set the replace text. """ - self._replaceVisible(True) + self.showReplace.setChecked(True) self.replaceBox.setFocus() self.replaceBox.setText(theText) return True @@ -1466,6 +1513,19 @@ class GuiDocEditSearch(QWidget): self.docEditor.docAction(nwDocAction.REPL_NEXT) return + def _doToggleReplace(self, theState): + """Toggle the show/hide of the + """ + if theState: + self.showReplace.setArrowType(Qt.DownArrow) + else: + self.showReplace.setArrowType(Qt.RightArrow) + self.replaceBox.setVisible(theState) + self.replaceButton.setVisible(theState) + self.repVisible = theState + self.adjustSize() + return + def _doToggleCase(self, theState): """Enable/disable case sensitive mode. """ @@ -1484,6 +1544,24 @@ class GuiDocEditSearch(QWidget): self.isRegEx = theState return + def _doToggleLoop(self, theState): + """Enable/disable looping the search. + """ + self.doLoop = theState + return + + def _doToggleProject(self, theState): + """Enable/disable continuing search in next project file. + """ + self.doNextFile = theState + return + + def _doTogglePreserve(self, theState): + """Enable/disable preserving case when replacing. + """ + self.doPreserve = theState + return + ## # Internal Functions ## @@ -1497,15 +1575,6 @@ class GuiDocEditSearch(QWidget): self.searchBox.setPalette(qPalette) return - def _replaceVisible(self, isVisible): - """Set the visibility of all the replace widgets. - """ - self.replaceBox.setVisible(isVisible) - self.replaceButton.setVisible(isVisible) - self.repVisible = isVisible - self.adjustSize() - return True - # END Class GuiDocEditSearch # =============================================================================================== # From 0ddb7ad1dec39f5468981b0449477e932dcfb1a1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 17:15:30 +0200 Subject: [PATCH 35/72] Preserve case repalce now works --- nw/common.py | 21 +++++++++++++++++++++ nw/gui/doceditor.py | 16 ++++++++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/nw/common.py b/nw/common.py index bd535a71..0019c38a 100644 --- a/nw/common.py +++ b/nw/common.py @@ -172,3 +172,24 @@ def splitVersionNumber(vString): vInt = vMajor*10000 + vMinor*100 + vPatch return [vMajor, vMinor, vPatch, vInt] + +def transferCase(theSource, theTarget): + """Transfers the case of the source word to the target word. This + will consider all upper or lower, and first char capitalisation. + """ + theResult = theTarget + + if not isinstance(theSource, str) or not isinstance(theTarget, str): + return theResult + if len(theTarget) < 1 or len(theSource) < 1: + return theResult + + if theSource[0] == theSource[0].upper(): + theResult = theTarget[0].upper() + theTarget[1:] + + if theSource == theSource.upper(): + theResult = theTarget.upper() + elif theSource == theSource.lower(): + theResult = theTarget.lower() + + return theResult diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index baa0b881..950fef73 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -48,6 +48,7 @@ from nw.core import NWDoc from nw.gui.dochighlight import GuiDocHighlighter from nw.core import NWSpellSimple, countWords from nw.constants import nwUnicode, nwDocAction +from nw.common import transferCase logger = logging.getLogger(__name__) @@ -1185,16 +1186,23 @@ class GuiDocEditor(QTextEdit): self._beginSearch() return + if not theCursor.hasSelection(): + return + theCursor = self.textCursor() searchFor = self.docSearch.getSearchText() replWith = self.docSearch.getReplaceText() selText = theCursor.selectedText() - if not self.docSearch.isCaseSense: - searchFor = searchFor.lower() - selText = selText.lower() + if self.docSearch.doPreserve: + replWith = transferCase(selText, replWith) - if theCursor.hasSelection() and selText == searchFor: + if not self.docSearch.isCaseSense: + isMatch = searchFor.lower() == selText.lower() + else: + isMatch = searchFor == selText + + if isMatch: theCursor.beginEditBlock() theCursor.removeSelectedText() theCursor.insertText(replWith) From ad21c6e3a4a63d9a0420f3ee6618c069192f6d56 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 19:16:32 +0200 Subject: [PATCH 36/72] Continue search in next file is now working --- nw/gui/build.py | 2 +- nw/gui/doceditor.py | 17 ++++++++++------- nw/gui/projtree.py | 39 ++++++++++++++++++++++++++++++++------- nw/guimain.py | 25 +++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 15 deletions(-) diff --git a/nw/gui/build.py b/nw/gui/build.py index 70ea0703..51a9dd68 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -459,7 +459,7 @@ class GuiBuildNovel(QDialog): makeHtml.setStyles(not noStyling) # Make sure the tree order is correct - self.theParent.treeView.saveTreeOrder() + self.theParent.treeView.flushTreeOrder() self.buildProgress.setMaximum(len(self.theProject.projTree)) self.buildProgress.setValue(0) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 950fef73..382fc123 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -1167,13 +1167,16 @@ class GuiDocEditor(QTextEdit): searchFor = self.docSearch.getSearchText() wasFound = self.find(searchFor, findOpt) - if not wasFound and self.docSearch.doLoop: - theCursor = self.textCursor() - theCursor.movePosition( - QTextCursor.End if isBackward else QTextCursor.Start - ) - self.setTextCursor(theCursor) - self.find(searchFor, findOpt) + if not wasFound: + if self.docSearch.doLoop: + theCursor = self.textCursor() + theCursor.movePosition( + QTextCursor.End if isBackward else QTextCursor.Start + ) + self.setTextCursor(theCursor) + self.find(searchFor, findOpt) + elif self.docSearch.doNextFile: + self.theParent.openNextDocument(self.theHandle) return diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 02e850ad..236b9f90 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -60,8 +60,9 @@ class GuiProjectTree(QTreeWidget): self.theProject = theParent.theProject # Tree Settings - self.theMap = None - self.orphRoot = None + self.theMap = None + self.orphRoot = None + self.treeChanged = False self.ctxMenu = GuiProjectTreeMenu(self) self.clearTree() @@ -256,7 +257,7 @@ class GuiProjectTree(QTreeWidget): pItem.insertChild(nIndex, cItem) self.clearSelection() cItem.setSelected(True) - self.theProject.setProjectChanged(True) + self._setTreeChanged(True) else: return False return True @@ -276,6 +277,16 @@ class GuiProjectTree(QTreeWidget): self.theProject.setTreeOrder(theList) return True + def flushTreeOrder(self): + """Calls saveTreeOrder if there are unsaved changes, otherwise + does nothing. + """ + if self.treeChanged: + logger.verbose("Flushing project tree to project class") + self.saveTreeOrder() + self._setTreeChanged(False) + return + def getTreeFromHandle(self, tHandle): """Recursively return all the children items starting from a given item handle. @@ -332,6 +343,9 @@ class GuiProjectTree(QTreeWidget): continue self.deleteItem(tHandle, True) + if nTrash > 0: + self._setTreeChanged(True) + return True def deleteItem(self, tHandle=None, alreadyAsked=False, askForTrash=False): @@ -413,7 +427,7 @@ class GuiProjectTree(QTreeWidget): trItemT.addChild(trItemC) nwItemS.setParent(self.theProject.projTree.trashRoot()) - self.theProject.setProjectChanged(True) + self._setTreeChanged(True) self.theParent.theIndex.deleteHandle(tHandle) elif nwItemS.itemType == nwItemType.FOLDER: @@ -436,7 +450,7 @@ class GuiProjectTree(QTreeWidget): if trItemS.childCount() == 0: self.takeTopLevelItem(tIndex) self.theParent.mainMenu.setAvailableRoot() - self.theProject.setProjectChanged(True) + self._setTreeChanged(True) else: self.makeAlert("Cannot delete root folder. It is not empty.", nwAlert.ERROR) return False @@ -732,6 +746,8 @@ class GuiProjectTree(QTreeWidget): elif nwItem.itemType == nwItemType.TRASH: newItem.setIcon(self.C_NAME, self.theTheme.getIcon(nwLabels.CLASS_ICON[tClass])) + self._setTreeChanged(True) + return newItem def _addTrashRoot(self): @@ -747,6 +763,7 @@ class GuiProjectTree(QTreeWidget): self.theProject.projTree[trashHandle] ) trItem.setExpanded(True) + self._setTreeChanged(True) return trItem def _addOrphanedRoot(self): @@ -793,7 +810,7 @@ class GuiProjectTree(QTreeWidget): nwItemS.setParent(pHandle) self.propagateCount(tHandle, wC) self.setTreeItemValues(tHandle) - self.theProject.setProjectChanged(True) + self._setTreeChanged(True) logger.debug("The parent of item %s has been changed to %s" % (tHandle,pHandle)) @@ -815,7 +832,15 @@ class GuiProjectTree(QTreeWidget): pHandle = trItemP.data(self.C_NAME, Qt.UserRole) nwItemS.setParent(pHandle) self.setTreeItemValues(tHandle) - self.theProject.setProjectChanged(True) + self._setTreeChanged(True) + return + + def _setTreeChanged(self, theState): + """Set the tree change flag, and propagate to the project. + """ + self.treeChanged = theState + if theState: + self.theProject.setProjectChanged(True) return # END Class GuiProjectTree diff --git a/nw/guimain.py b/nw/guimain.py index 5bfad545..1116f9aa 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -476,6 +476,31 @@ class GuiMain(QMainWindow): return False return True + def openNextDocument(self, tHandle): + """Opens the next document in the project tree, following the + document with the given handle. Stops when reaching the end. + """ + if self.hasProject: + self.treeView.flushTreeOrder() + nHandle = None + goNext = False + for tItem in self.theProject.projTree: + if tItem is None: + continue + if tItem.itemType != nwItemType.FILE: + continue + if tItem.itemHandle == tHandle: + goNext = True + elif goNext: + nHandle = tItem.itemHandle + break + + if nHandle is not None: + self.openDocument(nHandle, tLine=0) + return True + + return False + def saveDocument(self): """Save the current documents. """ From 32052b1bc315316e91f7f17573c08d0a080476c4 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 19:29:25 +0200 Subject: [PATCH 37/72] Searching through files can now loop back to first file again --- nw/gui/doceditor.py | 8 +++++--- nw/guimain.py | 16 +++++++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 382fc123..7873991f 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -1168,15 +1168,17 @@ class GuiDocEditor(QTextEdit): searchFor = self.docSearch.getSearchText() wasFound = self.find(searchFor, findOpt) if not wasFound: - if self.docSearch.doLoop: + if self.docSearch.doNextFile and not isBackward: + self.theParent.openNextDocument( + self.theHandle, wrapAround=self.docSearch.doLoop + ) + elif self.docSearch.doLoop: theCursor = self.textCursor() theCursor.movePosition( QTextCursor.End if isBackward else QTextCursor.Start ) self.setTextCursor(theCursor) self.find(searchFor, findOpt) - elif self.docSearch.doNextFile: - self.theParent.openNextDocument(self.theHandle) return diff --git a/nw/guimain.py b/nw/guimain.py index 1116f9aa..70a6ba0b 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -476,28 +476,34 @@ class GuiMain(QMainWindow): return False return True - def openNextDocument(self, tHandle): + def openNextDocument(self, tHandle, wrapAround=False): """Opens the next document in the project tree, following the document with the given handle. Stops when reaching the end. """ if self.hasProject: self.treeView.flushTreeOrder() - nHandle = None - goNext = False + nHandle = None # The next handle after tHandle + fHandle = None # The first file handle we encounter + foundIt = False # We've found tHandle, pick the next we see for tItem in self.theProject.projTree: if tItem is None: continue if tItem.itemType != nwItemType.FILE: continue + if fHandle is None: + fHandle = tItem.itemHandle if tItem.itemHandle == tHandle: - goNext = True - elif goNext: + foundIt = True + elif foundIt: nHandle = tItem.itemHandle break if nHandle is not None: self.openDocument(nHandle, tLine=0) return True + elif wrapAround: + self.openDocument(fHandle, tLine=0) + return False return False From 6712a7dfcd57a2ad0fce5638a4db121d871c215a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 19:47:25 +0200 Subject: [PATCH 38/72] The state of the search buttons is now saved between sessions --- nw/config.py | 37 ++++++++++++++++++++++++++++++++++--- nw/gui/doceditor.py | 28 +++++++++++++++++++++------- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/nw/config.py b/nw/config.py index 4f690be3..09b3d363 100644 --- a/nw/config.py +++ b/nw/config.py @@ -133,6 +133,13 @@ class Config: self.spellTool = None self.spellLanguage = None + self.searchCase = False + self.searchWord = False + self.searchRegEx = False + self.searchLoop = False + self.searchNextFile = False + self.searchMatchCap = False + ## Backup self.backupPath = "" self.backupOnClose = False @@ -482,6 +489,24 @@ class Config: self.viewSynopsis = self._parseLine( cnfParse, cnfSec, "viewsynopsis", self.CNF_BOOL, self.viewSynopsis ) + self.searchCase = self._parseLine( + cnfParse, cnfSec, "searchcase", self.CNF_BOOL, self.searchCase + ) + self.searchWord = self._parseLine( + cnfParse, cnfSec, "searchword", self.CNF_BOOL, self.searchWord + ) + self.searchRegEx = self._parseLine( + cnfParse, cnfSec, "searchregex", self.CNF_BOOL, self.searchRegEx + ) + self.searchLoop = self._parseLine( + cnfParse, cnfSec, "searchloop", self.CNF_BOOL, self.searchLoop + ) + self.searchNextFile = self._parseLine( + cnfParse, cnfSec, "searchnextfile", self.CNF_BOOL, self.searchNextFile + ) + self.searchMatchCap = self._parseLine( + cnfParse, cnfSec, "searchmatchcap", self.CNF_BOOL, self.searchMatchCap + ) ## Path cnfSec = "Path" @@ -571,9 +596,15 @@ class Config: ## State cnfSec = "State" cnfParse.add_section(cnfSec) - cnfParse.set(cnfSec,"showrefpanel",str(self.showRefPanel)) - cnfParse.set(cnfSec,"viewcomments",str(self.viewComments)) - cnfParse.set(cnfSec,"viewsynopsis",str(self.viewSynopsis)) + cnfParse.set(cnfSec,"showrefpanel", str(self.showRefPanel)) + cnfParse.set(cnfSec,"viewcomments", str(self.viewComments)) + cnfParse.set(cnfSec,"viewsynopsis", str(self.viewSynopsis)) + cnfParse.set(cnfSec,"searchcase", str(self.searchCase)) + cnfParse.set(cnfSec,"searchword", str(self.searchWord)) + cnfParse.set(cnfSec,"searchregex", str(self.searchRegEx)) + cnfParse.set(cnfSec,"searchloop", str(self.searchLoop)) + cnfParse.set(cnfSec,"searchnextfile", str(self.searchNextFile)) + cnfParse.set(cnfSec,"searchmatchcap", str(self.searchMatchCap)) ## Path cnfSec = "Path" diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 7873991f..4c465109 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -1286,12 +1286,12 @@ class GuiDocEditSearch(QFrame): self.theTheme = docEditor.theTheme self.repVisible = False - self.isCaseSense = False - self.isWholeWord = False - self.isRegEx = False - self.doLoop = False - self.doNextFile = False - self.doPreserve = False + self.isCaseSense = self.mainConf.searchCase + self.isWholeWord = self.mainConf.searchWord + self.isRegEx = self.mainConf.searchRegEx + self.doLoop = self.mainConf.searchLoop + self.doNextFile = self.mainConf.searchNextFile + self.doPreserve = self.mainConf.searchMatchCap mPx = self.mainConf.pxInt(6) fPx = int(0.9*self.theTheme.fontPixelSize) @@ -1332,6 +1332,7 @@ class GuiDocEditSearch(QFrame): self.toggleCase.setToolTip("Match case") self.toggleCase.setIcon(self.theTheme.getIcon("search_case")) self.toggleCase.setCheckable(True) + self.toggleCase.setChecked(self.isCaseSense) self.toggleCase.toggled.connect(self._doToggleCase) self.searchOpt.addAction(self.toggleCase) @@ -1339,6 +1340,7 @@ class GuiDocEditSearch(QFrame): self.toggleWord.setToolTip("Match whole words") self.toggleWord.setIcon(self.theTheme.getIcon("search_word")) self.toggleWord.setCheckable(True) + self.toggleWord.setChecked(self.isWholeWord) self.toggleWord.toggled.connect(self._doToggleWord) self.searchOpt.addAction(self.toggleWord) @@ -1346,6 +1348,7 @@ class GuiDocEditSearch(QFrame): self.toggleRegEx.setToolTip("Use regular expressions") self.toggleRegEx.setIcon(self.theTheme.getIcon("search_regex")) self.toggleRegEx.setCheckable(True) + self.toggleRegEx.setChecked(self.isRegEx) self.toggleRegEx.toggled.connect(self._doToggleRegEx) self.searchOpt.addAction(self.toggleRegEx) @@ -1353,6 +1356,7 @@ class GuiDocEditSearch(QFrame): self.toggleLoop.setToolTip("Loop the search when reaching the end") self.toggleLoop.setIcon(self.theTheme.getIcon("search_loop")) self.toggleLoop.setCheckable(True) + self.toggleLoop.setChecked(self.doLoop) self.toggleLoop.toggled.connect(self._doToggleLoop) self.searchOpt.addAction(self.toggleLoop) @@ -1360,6 +1364,7 @@ class GuiDocEditSearch(QFrame): self.toggleProject.setToolTip("Continue searching in the next file") self.toggleProject.setIcon(self.theTheme.getIcon("search_project")) self.toggleProject.setCheckable(True) + self.toggleProject.setChecked(self.doNextFile) self.toggleProject.toggled.connect(self._doToggleProject) self.searchOpt.addAction(self.toggleProject) @@ -1369,6 +1374,7 @@ class GuiDocEditSearch(QFrame): self.togglePreserve.setToolTip("Preserve case on replace") self.togglePreserve.setIcon(self.theTheme.getIcon("search_preserve")) self.togglePreserve.setCheckable(True) + self.togglePreserve.setChecked(self.doPreserve) self.togglePreserve.toggled.connect(self._doTogglePreserve) self.searchOpt.addAction(self.togglePreserve) @@ -1428,7 +1434,7 @@ class GuiDocEditSearch(QFrame): baseCol = qPalette.base().color() rCol = baseCol.redF() + 0.1 gCol = baseCol.greenF() - 0.1 - bCol = baseCol.blueF() - 0.1 + bCol = baseCol.blueF() - 0.1 mCol = max(rCol, gCol, bCol, 1.0) errCol = QColor() @@ -1448,10 +1454,18 @@ class GuiDocEditSearch(QFrame): def closeSearch(self): """Close the search box. """ + self.mainConf.searchCase = self.isCaseSense + self.mainConf.searchWord = self.isWholeWord + self.mainConf.searchRegEx = self.isRegEx + self.mainConf.searchLoop = self.doLoop + self.mainConf.searchNextFile = self.doNextFile + self.mainConf.searchMatchCap = self.doPreserve + self.showReplace.setChecked(False) self.setVisible(False) self.docEditor.updateDocMargins() self.docEditor.setFocus() + return ## From 79d6e3594c17cf30456f0b8378854e3afc51c79f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 19:47:44 +0200 Subject: [PATCH 39/72] Fixed tests --- tests/reference/novelwriter.conf | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/reference/novelwriter.conf b/tests/reference/novelwriter.conf index ba0f2db7..12e6ec11 100644 --- a/tests/reference/novelwriter.conf +++ b/tests/reference/novelwriter.conf @@ -1,5 +1,5 @@ [Main] -timestamp = 2020-06-13 22:59:36 +timestamp = 2020-06-17 19:45:53 theme = default syntax = default_light icons = typicons_colour_light @@ -55,6 +55,12 @@ askbeforebackup = True showrefpanel = True viewcomments = True viewsynopsis = True +searchcase = False +searchword = False +searchregex = False +searchloop = False +searchnextfile = False +searchmatchcap = False [Path] lastpath = From 22341c5e216ef689aa282aba0be0094eb5f1ae55 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 19:56:19 +0200 Subject: [PATCH 40/72] Fixed test and cleaned up imports --- nw/gui/doceditor.py | 7 ++++--- tests/test_gui.py | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 4c465109..cc761202 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -37,11 +37,12 @@ from time import time from PyQt5.QtCore import Qt, QSize, QThread, QTimer, pyqtSlot, QRegExp from PyQt5.QtGui import ( QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette, - QTextDocument, QCursor, QIcon + QTextDocument, QCursor ) from PyQt5.QtWidgets import ( - qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QWidget, QLabel, QToolBar, - QToolButton, QHBoxLayout, QGridLayout, QLineEdit, QPushButton, QFrame, QVBoxLayout, QSizePolicy + qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QWidget, QLabel, + QToolBar, QToolButton, QHBoxLayout, QGridLayout, QLineEdit, QPushButton, + QFrame ) from nw.core import NWDoc diff --git a/tests/test_gui.py b/tests/test_gui.py index cd375cd8..c4c03ea6 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -219,7 +219,6 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp): qtbot.wait(stepDelay) # Save the document - assert nwGUI.docEditor.docChanged assert nwGUI.saveDocument() assert not nwGUI.docEditor.docChanged qtbot.wait(stepDelay) From f2c1ce1ccad7330e305f7bbb746113b7b0b373d3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 20:05:30 +0200 Subject: [PATCH 41/72] Second attempt at fixing test where the word counter suddenly isn't working --- tests/test_gui.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_gui.py b/tests/test_gui.py index c4c03ea6..eeb6d490 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -215,10 +215,11 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp): qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.wait(stepDelay) - nwGUI.docEditor.wCounter.run() - qtbot.wait(stepDelay) + nwGUI.docEditor.wCounter.start() + qtbot.wait(1000) # Save the document + assert nwGUI.docEditor.docChanged assert nwGUI.saveDocument() assert not nwGUI.docEditor.docChanged qtbot.wait(stepDelay) From deec57c9c698fa813526e2b30603b844650ea8c3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 20:23:20 +0200 Subject: [PATCH 42/72] Another attempt at forcing the test word counter --- .travis.yml | 2 +- tests/test_gui.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 89ca3727..873ded23 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ install: - pip install --upgrade pip - pip install -r requirements.txt # - pip install pytest-faulthandler - - pip install PyVirtualDisplay==0.2.5 + - pip install PyVirtualDisplay - pip install pytest-xvfb - pip install pytest-cov - pip install pytest-qt diff --git a/tests/test_gui.py b/tests/test_gui.py index eeb6d490..b83b67fd 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -215,8 +215,9 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp): qtbot.keyClick(nwGUI.docEditor, Qt.Key_Return, delay=keyDelay) qtbot.wait(stepDelay) - nwGUI.docEditor.wCounter.start() - qtbot.wait(1000) + nwGUI.docEditor.wCounter.run() + nwGUI.docEditor._updateCounts() + qtbot.wait(stepDelay) # Save the document assert nwGUI.docEditor.docChanged From 7994491c5dcea1af004d437d0824c5bcc2435925 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 20:45:34 +0200 Subject: [PATCH 43/72] Force tests to run with qt<5.15 --- .travis.yml | 5 ++--- tests/test_gui.py | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 873ded23..7bcec702 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,15 +8,14 @@ addons: apt: packages: - libenchant-dev - - python3-pyqt5 - - python3-pyqt5.qtsvg python: - "3.6" - "3.7" - "3.8" install: - pip install --upgrade pip - - pip install -r requirements.txt + - pip install lxml + - pip install pyqt5<5.15 # - pip install pytest-faulthandler - pip install PyVirtualDisplay - pip install pytest-xvfb diff --git a/tests/test_gui.py b/tests/test_gui.py index b83b67fd..cd375cd8 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -216,7 +216,6 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp): qtbot.wait(stepDelay) nwGUI.docEditor.wCounter.run() - nwGUI.docEditor._updateCounts() qtbot.wait(stepDelay) # Save the document From b2747baf6d6ac65aa2c5d283f964ec7ca1b363e4 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 20:50:22 +0200 Subject: [PATCH 44/72] Pin exact version then ... --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7bcec702..5200e76f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ python: install: - pip install --upgrade pip - pip install lxml - - pip install pyqt5<5.15 + - pip install pyqt5==5.14.2 # - pip install pytest-faulthandler - pip install PyVirtualDisplay - pip install pytest-xvfb From 5832d3db711a2df784d15e09c2fc702d456bb26c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 21:28:22 +0200 Subject: [PATCH 45/72] This seemed to fix the test on the local machine --- .travis.yml | 5 +++-- nw/gui/doceditor.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5200e76f..873ded23 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,14 +8,15 @@ addons: apt: packages: - libenchant-dev + - python3-pyqt5 + - python3-pyqt5.qtsvg python: - "3.6" - "3.7" - "3.8" install: - pip install --upgrade pip - - pip install lxml - - pip install pyqt5==5.14.2 + - pip install -r requirements.txt # - pip install pytest-faulthandler - pip install PyVirtualDisplay - pip install pytest-xvfb diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index cc761202..cc72cf9a 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -593,7 +593,7 @@ class GuiDocEditor(QTextEdit): as it is triggered on every keypress when typing. """ self.hasSelection = self.textCursor().hasSelection() - if not self.hasFocus(): + if self.docSearch.searchBox.hasFocus(): # Block the event when the focus is on the search bar. return From c264e6100deabb8dae4064a8924662399f605d09 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 21:46:23 +0200 Subject: [PATCH 46/72] Replaced one of the icons on the search bar --- .../icons/fallback/search_project-dark.svg | 43 +++++-------------- nw/assets/icons/fallback/search_project.svg | 37 +++------------- 2 files changed, 17 insertions(+), 63 deletions(-) diff --git a/nw/assets/icons/fallback/search_project-dark.svg b/nw/assets/icons/fallback/search_project-dark.svg index 552fed1f..1e57b60e 100644 --- a/nw/assets/icons/fallback/search_project-dark.svg +++ b/nw/assets/icons/fallback/search_project-dark.svg @@ -5,13 +5,13 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" - version="1.2" - width="24" - height="24" + id="svg8301" viewBox="0 0 24 24" - id="svg5828"> + height="24" + width="24" + version="1.2"> + id="metadata8307"> @@ -23,32 +23,9 @@ - - - - - - + id="defs8305" /> + diff --git a/nw/assets/icons/fallback/search_project.svg b/nw/assets/icons/fallback/search_project.svg index ee16c528..12afc290 100644 --- a/nw/assets/icons/fallback/search_project.svg +++ b/nw/assets/icons/fallback/search_project.svg @@ -5,13 +5,13 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" - id="svg5828" + id="svg8301" viewBox="0 0 24 24" height="24" width="24" version="1.2"> + id="metadata8307"> @@ -23,32 +23,9 @@ - - - - - - + id="defs8305" /> + From c2b55530be60ce3ff175d6ed51b5c98b73cb4779 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 21:46:59 +0200 Subject: [PATCH 47/72] Renamed a variable, and fixed the document margins of search box change of size --- nw/gui/doceditor.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index cc72cf9a..5e0fb025 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -1200,7 +1200,7 @@ class GuiDocEditor(QTextEdit): replWith = self.docSearch.getReplaceText() selText = theCursor.selectedText() - if self.docSearch.doPreserve: + if self.docSearch.doMatchCap: replWith = transferCase(selText, replWith) if not self.docSearch.isCaseSense: @@ -1292,7 +1292,7 @@ class GuiDocEditSearch(QFrame): self.isRegEx = self.mainConf.searchRegEx self.doLoop = self.mainConf.searchLoop self.doNextFile = self.mainConf.searchNextFile - self.doPreserve = self.mainConf.searchMatchCap + self.doMatchCap = self.mainConf.searchMatchCap mPx = self.mainConf.pxInt(6) fPx = int(0.9*self.theTheme.fontPixelSize) @@ -1371,13 +1371,13 @@ class GuiDocEditSearch(QFrame): self.searchOpt.addSeparator() - self.togglePreserve = QAction("Preserve Case", self) - self.togglePreserve.setToolTip("Preserve case on replace") - self.togglePreserve.setIcon(self.theTheme.getIcon("search_preserve")) - self.togglePreserve.setCheckable(True) - self.togglePreserve.setChecked(self.doPreserve) - self.togglePreserve.toggled.connect(self._doTogglePreserve) - self.searchOpt.addAction(self.togglePreserve) + self.toggleMatchCap = QAction("Preserve Case", self) + self.toggleMatchCap.setToolTip("Preserve case on replace") + self.toggleMatchCap.setIcon(self.theTheme.getIcon("search_preserve")) + self.toggleMatchCap.setCheckable(True) + self.toggleMatchCap.setChecked(self.doMatchCap) + self.toggleMatchCap.toggled.connect(self._doToggleMatchCap) + self.searchOpt.addAction(self.toggleMatchCap) self.searchOpt.addSeparator() @@ -1426,10 +1426,10 @@ class GuiDocEditSearch(QFrame): boxWidth = self.mainConf.pxInt(200) self.searchBox.setFixedWidth(boxWidth) self.replaceBox.setFixedWidth(boxWidth) + self.replaceBox.setVisible(False) + self.replaceButton.setVisible(False) self.adjustSize() - self._doToggleReplace(False) - # Construct Box Colours qPalette = self.searchBox.palette() baseCol = qPalette.base().color() @@ -1460,7 +1460,7 @@ class GuiDocEditSearch(QFrame): self.mainConf.searchRegEx = self.isRegEx self.mainConf.searchLoop = self.doLoop self.mainConf.searchNextFile = self.doNextFile - self.mainConf.searchMatchCap = self.doPreserve + self.mainConf.searchMatchCap = self.doMatchCap self.showReplace.setChecked(False) self.setVisible(False) @@ -1552,6 +1552,7 @@ class GuiDocEditSearch(QFrame): self.replaceButton.setVisible(theState) self.repVisible = theState self.adjustSize() + self.docEditor.updateDocMargins() return def _doToggleCase(self, theState): @@ -1584,10 +1585,10 @@ class GuiDocEditSearch(QFrame): self.doNextFile = theState return - def _doTogglePreserve(self, theState): - """Enable/disable preserving case when replacing. + def _doToggleMatchCap(self, theState): + """Enable/disable preserving capitalisation when replacing. """ - self.doPreserve = theState + self.doMatchCap = theState return ## From 399b13dd3dd3021ea878fdd5cdaba04275a3c9ca Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 22:27:06 +0200 Subject: [PATCH 48/72] Make both panes use viewportmargin --- nw/gui/doceditor.py | 1 - nw/gui/docviewer.py | 17 +++++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 5e0fb025..efe863a7 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -351,7 +351,6 @@ class GuiDocEditor(QTextEdit): else: rH = 0 - # print(tM, tH, rH, max(tM, tH, rH)) self.setViewportMargins(tM, max(cM, tH, rH), tM, cM) return diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 8c2ea236..b280aa01 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -60,6 +60,7 @@ class GuiDocViewer(QTextBrowser): self.qDocument = self.document() self.setMinimumWidth(self.mainConf.pxInt(300)) + self.setAutoFillBackground(True) self.setOpenExternalLinks(False) self.initViewer() @@ -104,11 +105,12 @@ class GuiDocViewer(QTextBrowser): self.setFont(theFont) docPalette = self.palette() + docPalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack)) docPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack)) docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) self.setPalette(docPalette) - self.qDocument.setDocumentMargin(self.mainConf.getTextMargin()) + self.qDocument.setDocumentMargin(0) theOpt = QTextOption() if self.mainConf.doJustify: theOpt.setAlignment(Qt.AlignJustify) @@ -242,19 +244,10 @@ class GuiDocViewer(QTextBrowser): tH = self.docHeader.height() fH = self.docFooter.height() fY = self.height() - fH - tB - tT = cM - tH - bT = cM - fH + self.docHeader.setGeometry(tB, tB, tW, tH) self.docFooter.setGeometry(tB, fY, tW, fH) - self.setViewportMargins(0, tH, 0, fH) - - docFormat = self.qDocument.rootFrame().frameFormat() - docFormat.setTopMargin(max(0, tT)) - docFormat.setBottomMargin(max(0, bT)) - - self.qDocument.blockSignals(True) - self.qDocument.rootFrame().setFrameFormat(docFormat) - self.qDocument.blockSignals(False) + self.setViewportMargins(cM, max(cM, tH), cM, max(cM, fH)) return From 1978d50e7e9ed7f99ddc2469329797f3e6043348 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 22:39:01 +0200 Subject: [PATCH 49/72] Some minor cleanup in doceditor --- nw/gui/doceditor.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index efe863a7..ab76adfb 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -78,6 +78,9 @@ class GuiDocEditor(QTextEdit): self.bigDoc = False self.nonWord = "\"'" + # Document State + self.hasSelection = False + # Typography self.typDQOpen = self.mainConf.fmtDoubleQuotes[0] self.typDQClose = self.mainConf.fmtDoubleQuotes[1] @@ -86,7 +89,6 @@ class GuiDocEditor(QTextEdit): # Core Elements self.qDocument = self.document() - self.qDocument.setDocumentMargin(self.mainConf.getTextMargin()) self.qDocument.contentsChange.connect(self._docChange) # Document Title @@ -100,8 +102,7 @@ class GuiDocEditor(QTextEdit): self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self._openContextMenu) - # Editor State - self.hasSelection = False + # Editor Settings self.setMinimumWidth(self.mainConf.pxInt(300)) self.setAutoFillBackground(True) self.setAcceptRichText(False) From bbd6e708681fecd3fcb549e0252d91e23f753d40 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 17 Jun 2020 22:39:52 +0200 Subject: [PATCH 50/72] Forgot to save the file ... --- nw/gui/doceditor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index ab76adfb..eadce6ff 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -594,7 +594,8 @@ class GuiDocEditor(QTextEdit): """ self.hasSelection = self.textCursor().hasSelection() if self.docSearch.searchBox.hasFocus(): - # Block the event when the focus is on the search bar. + # Block the event when the focus is on the search bar as it + # seems to be capturing the return key. return if keyEvent.modifiers() == Qt.ShiftModifier: From a64d867ed42750948ff58f643be4ca846f13e631 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 19:30:02 +0200 Subject: [PATCH 51/72] Added a quote symbol selection dialog --- nw/constants/constants.py | 45 +++++++++-------- nw/gui/custom.py | 104 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 124 insertions(+), 25 deletions(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 9bd47110..b05f8e6d 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -169,27 +169,30 @@ class nwQuotes(): Source: https://en.wikipedia.org/wiki/Quotation_mark """ - SYMBOLS = [ - "\u0022", # Quotation mark - "\u0027", # Apostrophe - "\u00ab", # Left-pointing double angle quotation mark - "\u00bb", # Right-pointing double angle quotation mark - "\u2018", # Left single quotation mark - "\u2019", # Right single quotation mark - "\u201a", # Single low-9 quotation mark - "\u201b", # Single high-reversed-9 quotation mark - "\u201c", # Left double quotation mark - "\u201d", # Right double quotation mark - "\u201e", # Double low-9 quotation mark - "\u201f", # Double high-reversed-9 quotation mark - "\u2039", # Single left-pointing angle quotation mark - "\u203a", # Single right-pointing angle quotation mark - "\u2e42", # Double low-reversed-9 quotation mark - "\u300c", # Left corner bracket - "\u300d", # Right corner bracket - "\u300e", # Left white corner bracket - "\u300f", # Right white corner bracket - ] + SYMBOLS = { + "\u0027" : "Straight single quotation mark", + "\u0022" : "Straight double quotation mark", + + "\u2018" : "Left single quotation mark", + "\u2019" : "Right single quotation mark", + "\u201a" : "Single low-9 quotation mark", + "\u201b" : "Single high-reversed-9 quotation mark", + "\u201c" : "Left double quotation mark", + "\u201d" : "Right double quotation mark", + "\u201e" : "Double low-9 quotation mark", + "\u201f" : "Double high-reversed-9 quotation mark", + "\u2e42" : "Double low-reversed-9 quotation mark", + + "\u2039" : "Single left-pointing angle quotation mark", + "\u203a" : "Single right-pointing angle quotation mark", + "\u00ab" : "Left-pointing double angle quotation mark", + "\u00bb" : "Right-pointing double angle quotation mark", + + "\u300c" : "Left corner bracket", + "\u300d" : "Right corner bracket", + "\u300e" : "Left white corner bracket", + "\u300f" : "Right white corner bracket", + } # END Class nwQuotes diff --git a/nw/gui/custom.py b/nw/gui/custom.py index 5c3dfbd6..051fa1f3 100644 --- a/nw/gui/custom.py +++ b/nw/gui/custom.py @@ -30,17 +30,17 @@ import logging import nw -from PyQt5.QtGui import QColor, QPalette, QPainter +from PyQt5.QtGui import QColor, QPalette, QPainter, QFontMetrics from PyQt5.QtCore import ( Qt, QRect, QPoint, QSize, QRectF, QPropertyAnimation, pyqtProperty ) from PyQt5.QtWidgets import ( QGridLayout, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QSizePolicy, - QAbstractButton, QDialog, QTabWidget, QTabBar, QStyle, - QStylePainter, QStyleOptionTab + QAbstractButton, QDialog, QTabWidget, QTabBar, QStyle, QDialogButtonBox, + QStylePainter, QStyleOptionTab, QListWidget, QListWidgetItem, QFrame ) -from nw.constants import nwUnicode +from nw.constants import nwUnicode, nwQuotes logger = logging.getLogger(__name__) @@ -436,3 +436,99 @@ class VerticalTabBar(QTabBar): return # END Class VerticalTabBar + +# =============================================================================================== # +# Quotes Dialog +# =============================================================================================== # + +class QuotesDialog(QDialog): + + def __init__(self, theParent=None, currentQuote="\""): + QDialog.__init__(self, parent=theParent) + + self.mainConf = nw.CONFIG + + self.outerBox = QVBoxLayout() + self.innerBox = QHBoxLayout() + self.labelBox = QVBoxLayout() + + self.selectedQuote = currentQuote + + qMetrics = QFontMetrics(self.font()) + pxW = 7*qMetrics.boundingRectChar("M").width() + pxH = 7*qMetrics.boundingRectChar("M").height() + pxH = 7*qMetrics.boundingRectChar("M").height() + + lblFont = self.font() + lblFont.setPointSizeF(4*lblFont.pointSizeF()) + + # Preview Label + self.previewLabel = QLabel(currentQuote) + self.previewLabel.setFont(lblFont) + self.previewLabel.setFixedSize(QSize(pxW, pxH)) + self.previewLabel.setAlignment(Qt.AlignCenter) + self.previewLabel.setFrameStyle(QFrame.Box | QFrame.Plain) + + # Quote Symbols + self.listBox = QListWidget() + self.listBox.itemSelectionChanged.connect(self._selectedSymbol) + + minSize = 100 + for sKey, sLabel in nwQuotes.SYMBOLS.items(): + theText = "[ %s ] %s" % (sKey, sLabel) + minSize = max(minSize, qMetrics.boundingRect(theText).width()) + qtItem = QListWidgetItem(theText) + qtItem.setData(Qt.UserRole, sKey) + self.listBox.addItem(qtItem) + if sKey == currentQuote: + self.listBox.setCurrentItem(qtItem) + + self.listBox.setMinimumWidth(minSize + self.mainConf.pxInt(40)) + self.listBox.setMinimumHeight(self.mainConf.pxInt(150)) + + # Buttons + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.buttonBox.accepted.connect(self._doAccept) + self.buttonBox.rejected.connect(self._doReject) + + # Assemble + self.labelBox.addWidget(self.previewLabel, 0, Qt.AlignTop) + self.labelBox.addStretch(1) + + self.innerBox.addLayout(self.labelBox) + self.innerBox.addWidget(self.listBox) + + self.outerBox.addLayout(self.innerBox) + self.outerBox.addWidget(self.buttonBox) + + self.setLayout(self.outerBox) + + return + + ## + # Slots + ## + + def _selectedSymbol(self): + """Update the preview label and the selected quote style. + """ + selItems = self.listBox.selectedItems() + if selItems: + theSymbol = selItems[0].data(Qt.UserRole) + self.previewLabel.setText(theSymbol) + self.selectedQuote = theSymbol + return + + def _doAccept(self): + """Ok button clicked. + """ + self.accept() + return + + def _doReject(self): + """Cancel button clicked. + """ + self.reject() + return + +# END Class QuotesDialog From 670f1c0b35bb56a3547bcbec55b792116db4740f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 19:30:34 +0200 Subject: [PATCH 52/72] Added the quote selection dialog to Preferences --- nw/gui/preferences.py | 98 ++++++++++++++++++++++++++---------------- nw/gui/projsettings.py | 2 - 2 files changed, 62 insertions(+), 38 deletions(-) diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index 5c0ef235..ff9f0469 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -37,7 +37,7 @@ from PyQt5.QtWidgets import ( QDialogButtonBox, QFileDialog, QFontDialog ) -from nw.gui.custom import QSwitch, QConfigLayout, PagedDialog +from nw.gui.custom import QSwitch, QConfigLayout, PagedDialog, QuotesDialog from nw.core import NWSpellCheck, NWSpellSimple, NWSpellEnchant from nw.constants import nwAlert, nwQuotes @@ -816,51 +816,72 @@ class GuiConfigEditAutoReplaceTab(QWidget): self.mainForm.addGroupLabel("Quotation Style") qWidth = self.mainConf.pxInt(40) + bWidth = int(2.5*self.theTheme.getTextWidth("...")) ## Single Quote Style self.quoteSingleStyleO = QLineEdit() self.quoteSingleStyleO.setMaxLength(1) + self.quoteSingleStyleO.setReadOnly(True) self.quoteSingleStyleO.setFixedWidth(qWidth) self.quoteSingleStyleO.setAlignment(Qt.AlignCenter) self.quoteSingleStyleO.setText(self.mainConf.fmtSingleQuotes[0]) + self.btnSingleStyleO = QPushButton("...") + self.btnSingleStyleO.setMaximumWidth(bWidth) + self.btnSingleStyleO.clicked.connect(self._getSingleOpen) self.mainForm.addRow( "Single quote open style", self.quoteSingleStyleO, - "Auto-replaces apostrophe before words." + "Auto-replaces apostrophe before words.", + theButton=self.btnSingleStyleO ) self.quoteSingleStyleC = QLineEdit() self.quoteSingleStyleC.setMaxLength(1) + self.quoteSingleStyleC.setReadOnly(True) self.quoteSingleStyleC.setFixedWidth(qWidth) self.quoteSingleStyleC.setAlignment(Qt.AlignCenter) self.quoteSingleStyleC.setText(self.mainConf.fmtSingleQuotes[1]) + self.btnSingleStyleC = QPushButton("...") + self.btnSingleStyleC.setMaximumWidth(bWidth) + self.btnSingleStyleC.clicked.connect(self._getSingleClose) self.mainForm.addRow( "Single quote close style", self.quoteSingleStyleC, - "Auto-replaces apostrophe after words." + "Auto-replaces apostrophe after words.", + theButton=self.btnSingleStyleC ) ## Double Quote Style self.quoteDoubleStyleO = QLineEdit() self.quoteDoubleStyleO.setMaxLength(1) + self.quoteDoubleStyleO.setReadOnly(True) self.quoteDoubleStyleO.setFixedWidth(qWidth) self.quoteDoubleStyleO.setAlignment(Qt.AlignCenter) self.quoteDoubleStyleO.setText(self.mainConf.fmtDoubleQuotes[0]) + self.btnDoubleStyleO = QPushButton("...") + self.btnDoubleStyleO.setMaximumWidth(bWidth) + self.btnDoubleStyleO.clicked.connect(self._getDoubleOpen) self.mainForm.addRow( "Double quote open style", self.quoteDoubleStyleO, - "Auto-replaces straight quotes before words." + "Auto-replaces straight quotes before words.", + theButton=self.btnDoubleStyleO ) self.quoteDoubleStyleC = QLineEdit() self.quoteDoubleStyleC.setMaxLength(1) + self.quoteDoubleStyleC.setReadOnly(True) self.quoteDoubleStyleC.setFixedWidth(qWidth) self.quoteDoubleStyleC.setAlignment(Qt.AlignCenter) self.quoteDoubleStyleC.setText(self.mainConf.fmtDoubleQuotes[1]) + self.btnDoubleStyleC = QPushButton("...") + self.btnDoubleStyleC.setMaximumWidth(bWidth) + self.btnDoubleStyleC.clicked.connect(self._getDoubleClose) self.mainForm.addRow( "Double quote close style", self.quoteDoubleStyleC, - "Auto-replaces straight quotes after words." + "Auto-replaces straight quotes after words.", + theButton=self.btnDoubleStyleC ) return @@ -889,37 +910,10 @@ class GuiConfigEditAutoReplaceTab(QWidget): fmtDoubleQuotesO = self.quoteDoubleStyleO.text() fmtDoubleQuotesC = self.quoteDoubleStyleC.text() - if self._checkQuoteSymbol(fmtSingleQuotesO): - self.mainConf.fmtSingleQuotes[0] = fmtSingleQuotesO - else: - self.theParent.makeAlert( - "Invalid quote symbol: %s" % fmtSingleQuotesO, nwAlert.ERROR - ) - validEntries = False - - if self._checkQuoteSymbol(fmtSingleQuotesC): - self.mainConf.fmtSingleQuotes[1] = fmtSingleQuotesC - else: - self.theParent.makeAlert( - "Invalid quote symbol: %s" % fmtSingleQuotesC, nwAlert.ERROR - ) - validEntries = False - - if self._checkQuoteSymbol(fmtDoubleQuotesO): - self.mainConf.fmtDoubleQuotes[0] = fmtDoubleQuotesO - else: - self.theParent.makeAlert( - "Invalid quote symbol: %s" % fmtDoubleQuotesO, nwAlert.ERROR - ) - validEntries = False - - if self._checkQuoteSymbol(fmtDoubleQuotesC): - self.mainConf.fmtDoubleQuotes[1] = fmtDoubleQuotesC - else: - self.theParent.makeAlert( - "Invalid quote symbol: %s" % fmtDoubleQuotesC, nwAlert.ERROR - ) - validEntries = False + self.mainConf.fmtSingleQuotes[0] = fmtSingleQuotesO + self.mainConf.fmtSingleQuotes[1] = fmtSingleQuotesC + self.mainConf.fmtDoubleQuotes[0] = fmtDoubleQuotesO + self.mainConf.fmtDoubleQuotes[1] = fmtDoubleQuotesC self.mainConf.confChanged = True @@ -939,6 +933,38 @@ class GuiConfigEditAutoReplaceTab(QWidget): self.autoReplaceDots.setEnabled(theState) return + def _getSingleOpen(self): + """Dialog for single quote open. + """ + qtBox = QuotesDialog(self, currentQuote=self.quoteSingleStyleO.text()) + if qtBox.exec_() == QDialog.Accepted: + self.quoteSingleStyleO.setText(qtBox.selectedQuote) + return + + def _getSingleClose(self): + """Dialog for single quote close. + """ + qtBox = QuotesDialog(self, currentQuote=self.quoteSingleStyleC.text()) + if qtBox.exec_() == QDialog.Accepted: + self.quoteSingleStyleC.setText(qtBox.selectedQuote) + return + + def _getDoubleOpen(self): + """Dialog for double quote open. + """ + qtBox = QuotesDialog(self, currentQuote=self.quoteDoubleStyleO.text()) + if qtBox.exec_() == QDialog.Accepted: + self.quoteDoubleStyleO.setText(qtBox.selectedQuote) + return + + def _getDoubleClose(self): + """Dialog for double quote close. + """ + qtBox = QuotesDialog(self, currentQuote=self.quoteDoubleStyleC.text()) + if qtBox.exec_() == QDialog.Accepted: + self.quoteDoubleStyleC.setText(qtBox.selectedQuote) + return + ## # Internal Functions ## diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index 0afaefbf..a9c19038 100644 --- a/nw/gui/projsettings.py +++ b/nw/gui/projsettings.py @@ -73,8 +73,6 @@ class GuiProjectSettings(PagedDialog): self.buttonBox.rejected.connect(self._doClose) self.addControls(self.buttonBox) - self.show() - logger.debug("GuiProjectSettings initialisation complete") return From 28641a7b13a796f6012d89e6be17884dec677c44 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 21:20:15 +0200 Subject: [PATCH 53/72] Fixed test and cleaned up imports --- nw/gui/preferences.py | 2 +- nw/gui/projsettings.py | 7 +++---- tests/test_gui.py | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py index ff9f0469..56905307 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -39,7 +39,7 @@ from PyQt5.QtWidgets import ( from nw.gui.custom import QSwitch, QConfigLayout, PagedDialog, QuotesDialog from nw.core import NWSpellCheck, NWSpellSimple, NWSpellEnchant -from nw.constants import nwAlert, nwQuotes +from nw.constants import nwQuotes logger = logging.getLogger(__name__) diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index a9c19038..8f5dacb3 100644 --- a/nw/gui/projsettings.py +++ b/nw/gui/projsettings.py @@ -31,10 +31,9 @@ import nw from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush from PyQt5.QtWidgets import ( - QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QLineEdit, QPlainTextEdit, - QLabel, QWidget, QTabWidget, QDialogButtonBox, QListWidget, QPushButton, - QListWidgetItem, QColorDialog, QAbstractItemView, QTreeWidget, QCheckBox, - QTreeWidgetItem + QHBoxLayout, QVBoxLayout, QGridLayout, QLineEdit, QPlainTextEdit, QLabel, + QWidget, QDialogButtonBox, QListWidget, QPushButton, QListWidgetItem, + QColorDialog, QAbstractItemView, QTreeWidget, QTreeWidgetItem ) from nw.constants import nwAlert diff --git a/tests/test_gui.py b/tests/test_gui.py index cd375cd8..1a18ec99 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -273,6 +273,7 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp): nwGUI.mainConf.backupPath = nwTempGUI projEdit = GuiProjectSettings(nwGUI, nwGUI.theProject) + projEdit.show() qtbot.addWidget(projEdit) projEdit.tabMain.editName.setText("") From 4dd27ba58c5bf3c6c5406c36a37ff90b1bf7eea8 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 21:37:34 +0200 Subject: [PATCH 54/72] Updated changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93f9a24f..f0bbdb6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ * The Open Project dialog will now ask before removing an entry from the recent projects list. PR #309. * The text emphasis functions, either selected from the menu or via keyboard shortcuts, will now try to respond to the command in a more meaningful way. That is, the text editor will try to toggle the bold or italics features independently of eachother on the selected text. A feature to apply both at the same time has also been added. PR #310. +* The document editor search tool has been completely rewritten. It now appears as a search box at the top of the document, and has a number of toggle switches added to it. You can modify the search tool to be case sensitive, select only whole words, use regular expression search strings, loop when reaching the end, and continue the search in next file. For the replace feature, you can also select to have the feature try to preserve the case of the replaced word. Issues #84 and #305, PR #314. +* A dialog has been added for selecting quotation mark style. These are now used in the Preferences dialog instead of a plain text box. PR #317. **Other Changes** From 70ce2b1226e570f7dbae22d8ae48a9bc180d2ff2 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 21:58:38 +0200 Subject: [PATCH 55/72] Update codecov settings --- .codecov.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.codecov.yml b/.codecov.yml index 8a775495..45ebc6c7 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -3,12 +3,14 @@ codecov: coverage: precision: 2 - round: down + round: up range: "70...100" status: - project: yes - patch: yes + project: + default: + threshold: 1% + patch: no changes: no parsers: From 8ff54ceaa48680014800ba577e8c59b55e7e3bac Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 22:36:36 +0200 Subject: [PATCH 56/72] Added some more checks in the gui tests for writing documents --- tests/reference/gui/3_nwProject.nwx | 16 ++++++++-------- tests/test_gui.py | 16 ++++++++++++++-- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index 7ae01352..038ee8f2 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -11,9 +11,9 @@ True False True - None + 31489056e0916 None - 0 + 59 %title% @@ -41,14 +41,14 @@ ROOT NOVEL New - False + True New Chapter FOLDER NOVEL New - False + True Just a Page @@ -57,9 +57,9 @@ Note False PAGE - 0 - 0 - 0 + 331 + 59 + 2 0 diff --git a/tests/test_gui.py b/tests/test_gui.py index 1a18ec99..fb98c1eb 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -127,6 +127,11 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp): nwGUI.treeView.newTreeItem(nwItemType.FILE, None) assert nwGUI.openSelectedItem() + # Add Some Text + nwGUI.docEditor.replaceText("Hello World!") + assert nwGUI.docEditor.getText() == "Hello World!" + nwGUI.docEditor.replaceText("") + # Type something into the document nwGUI.setFocus(2) for c in "# Main Location": @@ -359,6 +364,7 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp): # Create new, save, open project nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject(nwTempGUI, True) + assert nwGUI.openDocument("31489056e0916") itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916") qtbot.addWidget(itemEdit) @@ -375,7 +381,6 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp): itemEdit.editExport.setChecked(False) assert not itemEdit.editExport.isChecked() - itemEdit._doSave() itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916") @@ -383,9 +388,16 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp): assert itemEdit.editName.text() == "Just a Page" assert itemEdit.editStatus.currentData() == "Note" assert itemEdit.editLayout.currentData() == nwItemLayout.PAGE - itemEdit._doClose() + # Check that the header is updated + nwGUI.docEditor.updateDocTitle("31489056e0916") + assert nwGUI.docEditor.docHeader.theTitle.text() == "Novel › New Chapter › Just a Page" + assert not nwGUI.docEditor.setCursorLine("where?") + assert nwGUI.docEditor.setCursorLine(2) + qtbot.wait(stepDelay) + assert nwGUI.docEditor.getCursorPosition() == 9 + qtbot.wait(stepDelay) assert nwGUI.saveProject() qtbot.wait(stepDelay) From 1135ca7f870a456c1904af3b75edb860436e8cca Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 23:00:47 +0200 Subject: [PATCH 57/72] Added document action test --- tests/test_gui.py | 133 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/tests/test_gui.py b/tests/test_gui.py index fb98c1eb..3ad703a5 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -623,3 +623,136 @@ def testSplitTool(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp): # qtbot.stopForInteraction() nwGUI.closeMain() + +@pytest.mark.gui +def testDocAction(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp): + + nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp]) + qtbot.addWidget(nwGUI) + nwGUI.show() + qtbot.waitForWindowShown(nwGUI) + qtbot.wait(stepDelay) + + nwGUI.theProject.projTree.setSeed(42) + assert nwGUI.openProject(nwLipsum) + qtbot.wait(stepDelay) + + # CUT = 3 + # COPY = 4 + # PASTE = 5 + # SEL_ALL = 12 + # SEL_PARA = 13 + # FIND = 14 + # REPLACE = 15 + # GO_NEXT = 16 + # GO_PREV = 17 + # REPL_NEXT = 18 + + # Split By Chapter + assert nwGUI.openDocument("4c4f28287af27") + assert nwGUI.docEditor.setCursorPosition(30) + + cleanText = nwGUI.docEditor.getText()[27:74] + + # Bold + assert nwGUI.passDocumentAction(nwDocAction.BOLD) + assert nwGUI.docEditor.getText()[27:78] == "**Pellentesque** nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BOLD) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Italic + assert nwGUI.passDocumentAction(nwDocAction.ITALIC) + assert nwGUI.docEditor.getText()[27:76] == "*Pellentesque* nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.ITALIC) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Bold-Italic + assert nwGUI.passDocumentAction(nwDocAction.BOLDITALIC) + assert nwGUI.docEditor.getText()[27:80] == "***Pellentesque*** nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BOLDITALIC) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Strikethrough + assert nwGUI.passDocumentAction(nwDocAction.STRIKE) + assert nwGUI.docEditor.getText()[27:78] == "~~Pellentesque~~ nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.STRIKE) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Should get us back to plain + assert nwGUI.passDocumentAction(nwDocAction.BOLD) + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.ITALIC) + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.ITALIC) + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BOLD) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Equivalent of the above + assert nwGUI.passDocumentAction(nwDocAction.BOLDITALIC) + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.ITALIC) + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BOLD) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Double Quotes + assert nwGUI.passDocumentAction(nwDocAction.D_QUOTE) + assert nwGUI.docEditor.getText()[27:76] == "“Pellentesque” nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.UNDO) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Single Quotes + assert nwGUI.passDocumentAction(nwDocAction.S_QUOTE) + assert nwGUI.docEditor.getText()[27:76] == "‘Pellentesque’ nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.UNDO) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Block Formats + assert nwGUI.docEditor.setCursorPosition(30) + assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H1) + assert nwGUI.docEditor.getText()[27:76] == "# Pellentesque nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H2) + assert nwGUI.docEditor.getText()[27:77] == "## Pellentesque nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H3) + assert nwGUI.docEditor.getText()[27:78] == "### Pellentesque nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H4) + assert nwGUI.docEditor.getText()[27:79] == "#### Pellentesque nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BLOCK_TXT) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BLOCK_COM) + assert nwGUI.docEditor.getText()[27:76] == "% Pellentesque nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.BLOCK_TXT) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # Undo/Redo + assert nwGUI.passDocumentAction(nwDocAction.UNDO) + assert nwGUI.docEditor.getText()[27:76] == "% Pellentesque nec erat ut nulla posuere commodo." + qtbot.wait(stepDelay) + assert nwGUI.passDocumentAction(nwDocAction.REDO) + assert nwGUI.docEditor.getText()[27:74] == cleanText + qtbot.wait(stepDelay) + + # qtbot.stopForInteraction() + nwGUI.closeMain() From 3bfe21468276eefeb65ffa0e27f24fb1113a0c96 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 18 Jun 2020 23:45:23 +0200 Subject: [PATCH 58/72] Added thin space and thin non-breaking space to unicode class --- nw/constants/constants.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index b05f8e6d..90e49dfe 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -231,6 +231,8 @@ class nwUnicode: ## Other U_NBSP = "\u00a0" # Non-breaking space + U_THNSP = "\u2009" # Thin space + U_THNBSP = "\u202f" # Thin non-breaking space U_CHECK = "\u2714" # Heavy check mark U_MULT = "\u2715" # Multiplication x @@ -275,6 +277,8 @@ class nwUnicode: ## Other H_NBSP = " " + H_THNSP = " " + H_THNBSP = " " H_CHECK = "✔" H_MULT = "✕" From 61856cec5b60f2813c3e32015ef3ebeebc8e80a5 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 17:34:41 +0200 Subject: [PATCH 59/72] Added new constants for document insert feature --- nw/constants/__init__.py | 8 ++++++-- nw/constants/constants.py | 27 +++++++++++++++++++++++---- nw/constants/enum.py | 13 +++++++++++++ 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/nw/constants/__init__.py b/nw/constants/__init__.py index 57761e42..0e90354a 100644 --- a/nw/constants/__init__.py +++ b/nw/constants/__init__.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- from nw.constants.iso import isoLanguage, isoCountry from nw.constants.constants import ( - nwConst, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode + nwConst, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode, + nwInsertSymbols ) from nw.constants.enum import ( - nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline + nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline, + nwDocInsert ) __all__ = [ @@ -17,10 +19,12 @@ __all__ = [ "nwLabels", "nwQuotes", "nwUnicode", + "nwInsertSymbols", "nwAlert", "nwDocAction", "nwItemClass", "nwItemLayout", "nwItemType", "nwOutline", + "nwDocInsert", ] diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 90e49dfe..1ff5da4f 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -25,7 +25,7 @@ along with this program. If not, see . """ -from nw.constants.enum import nwItemClass, nwItemLayout, nwOutline +from nw.constants.enum import nwItemClass, nwItemLayout, nwOutline, nwDocInsert class nwConst(): @@ -197,7 +197,7 @@ class nwQuotes(): # END Class nwQuotes class nwUnicode: - """Suppoted unicode character constants and translation maps for HTML. + """Supported unicode character constants and translation maps for HTML. """ # Unicode Constants @@ -229,10 +229,12 @@ class nwUnicode: U_EMDASH = "\u2014" # Long dash U_HELLIP = "\u2026" # Ellipsis - ## Other + ## Spaces U_NBSP = "\u00a0" # Non-breaking space U_THNSP = "\u2009" # Thin space U_THNBSP = "\u202f" # Thin non-breaking space + + ## Symbols U_CHECK = "\u2714" # Heavy check mark U_MULT = "\u2715" # Multiplication x @@ -275,10 +277,12 @@ class nwUnicode: H_EMDASH = "—" H_HELLIP = "…" - ## Other + ## Spaces H_NBSP = " " H_THNSP = " " H_THNBSP = " " + + ## Symbols H_CHECK = "✔" H_MULT = "✕" @@ -293,3 +297,18 @@ class nwUnicode: H_LTRIS = "◂" # END Class nwUnicode + +class nwInsertSymbols(): + + SYMBOLS = { + nwDocInsert.NO_INSERT : "", + nwDocInsert.HARD_BREAK : " \n", + nwDocInsert.NB_SPACE : nwUnicode.U_NBSP, + nwDocInsert.THIN_SPACE : nwUnicode.U_THNSP, + nwDocInsert.THIN_NB_SPACE : nwUnicode.U_THNBSP, + nwDocInsert.SHORT_DASH : nwUnicode.U_ENDASH, + nwDocInsert.LONG_DASH : nwUnicode.U_EMDASH, + nwDocInsert.ELLIPSIS : nwUnicode.U_HELLIP, + } + +# END Enum nwDocInsert diff --git a/nw/constants/enum.py b/nw/constants/enum.py index 1f90a928..84f8d4f5 100644 --- a/nw/constants/enum.py +++ b/nw/constants/enum.py @@ -96,6 +96,19 @@ class nwDocAction(Enum): # END Enum nwDocAction +class nwDocInsert(Enum): + + NO_INSERT = 0 + HARD_BREAK = 1 + NB_SPACE = 2 + THIN_SPACE = 3 + THIN_NB_SPACE = 4 + SHORT_DASH = 5 + LONG_DASH = 6 + ELLIPSIS = 7 + +# END Enum nwDocInsert + class nwAlert(Enum): INFO = 0 From 0fc0851def1dadd505a9577673ea407c05d164b3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 17:35:14 +0200 Subject: [PATCH 60/72] Added insert options and key bindings for seven different symbol and space inserts --- nw/core/tohtml.py | 2 + nw/gui/doceditor.py | 48 +++++------- nw/gui/dochighlight.py | 2 +- nw/gui/mainmenu.py | 161 ++++++++++++++++++++++++++++------------- nw/guimain.py | 27 ++++++- 5 files changed, 156 insertions(+), 84 deletions(-) diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 6123ce21..1fd6f00d 100644 --- a/nw/core/tohtml.py +++ b/nw/core/tohtml.py @@ -54,6 +54,8 @@ class ToHtml(Tokenizer): nwUnicode.U_EMDASH : nwUnicode.H_EMDASH, nwUnicode.U_HELLIP : nwUnicode.H_HELLIP, nwUnicode.U_NBSP : nwUnicode.H_NBSP, + nwUnicode.U_THNSP : nwUnicode.H_THNSP, + nwUnicode.U_THNBSP : nwUnicode.H_THNBSP, } self.revDict = {} self.reReplace = [] diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index eadce6ff..35ac09d8 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -48,7 +48,7 @@ from PyQt5.QtWidgets import ( from nw.core import NWDoc from nw.gui.dochighlight import GuiDocHighlighter from nw.core import NWSpellSimple, countWords -from nw.constants import nwUnicode, nwDocAction +from nw.constants import nwUnicode, nwDocAction, nwDocInsert, nwInsertSymbols from nw.common import transferCase logger = logging.getLogger(__name__) @@ -570,6 +570,21 @@ class GuiDocEditor(QTextEdit): )) return + def insertText(self, theInsert): + """Insert a specific type of text at the cursor position. + """ + if isinstance(theInsert, str): + theText = theInsert + elif theInsert in nwInsertSymbols.SYMBOLS: + theText = nwInsertSymbols.SYMBOLS[theInsert] + else: + return False + theCursor = self.textCursor() + theCursor.beginEditBlock() + theCursor.insertText(theText) + theCursor.endEditBlock() + return True + def closeSearch(self): """Close the search box. """ @@ -598,18 +613,7 @@ class GuiDocEditor(QTextEdit): # seems to be capturing the return key. return - if keyEvent.modifiers() == Qt.ShiftModifier: - theKey = keyEvent.key() - if theKey == Qt.Key_Return: - self._insertHardBreak() - elif theKey == Qt.Key_Enter: - self._insertHardBreak() - elif theKey == Qt.Key_Space: - self._insertNonBreakingSpace() - else: - QTextEdit.keyPressEvent(self, keyEvent) - else: - QTextEdit.keyPressEvent(self, keyEvent) + QTextEdit.keyPressEvent(self, keyEvent) return @@ -797,24 +801,6 @@ class GuiDocEditor(QTextEdit): return True - def _insertHardBreak(self): - """Inserts a hard line break at the cursor position. - """ - theCursor = self.textCursor() - theCursor.beginEditBlock() - theCursor.insertText(" \n") - theCursor.endEditBlock() - return - - def _insertNonBreakingSpace(self): - """Inserts a non-breaking space at the cursor position. - """ - theCursor = self.textCursor() - theCursor.beginEditBlock() - theCursor.insertText(nwUnicode.U_NBSP) - theCursor.endEditBlock() - return - def _openSpellContext(self): """Opens the spell check context menu at the current point of the cursor. diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index a76c9646..4acc0e7a 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -132,7 +132,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): # Non-Breaking Spaces self.hRules.append(( - "[%s]+" % nwUnicode.U_NBSP, { + "[%s%s]+" % (nwUnicode.U_NBSP, nwUnicode.U_THNBSP), { 0 : self.hStyles["nobreak"], } )) diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 720eefae..7f7c9164 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -28,12 +28,12 @@ import logging import nw -from PyQt5.QtCore import QUrl +from PyQt5.QtCore import Qt, QUrl from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QMenuBar, QAction, QMessageBox from nw.gui.about import GuiAbout -from nw.constants import nwItemType, nwItemClass, nwDocAction +from nw.constants import nwItemType, nwItemClass, nwDocAction, nwDocInsert logger = logging.getLogger(__name__) @@ -50,8 +50,9 @@ class GuiMainMenu(QMenuBar): self._buildProjectMenu() self._buildDocumentMenu() self._buildEditMenu() - self._buildViewMenu() + self._buildInsertMenu() self._buildFormatMenu() + self._buildViewMenu() self._buildToolsMenu() self._buildHelpMenu() @@ -59,6 +60,7 @@ class GuiMainMenu(QMenuBar): self._docAction = self.theParent.passDocumentAction self._moveTreeItem = self.theParent.treeView.moveTreeItem self._newTreeItem = self.theParent.treeView.newTreeItem + self._docInsert = self.theParent.docEditor.insertText logger.debug("GuiMainMenu initialisation complete") @@ -341,53 +343,6 @@ class GuiMainMenu(QMenuBar): return - def _buildViewMenu(self): - - # View - self.viewMenu = self.addMenu("&View") - - # View > TreeView - self.aFocusTree = QAction("Focus Project Tree", self) - self.aFocusTree.setStatusTip("Move focus to project tree") - self.aFocusTree.setShortcut("Alt+1") - self.aFocusTree.triggered.connect(lambda : self.theParent.setFocus(1)) - self.viewMenu.addAction(self.aFocusTree) - - # View > Document Pane 1 - self.aFocusEditor = QAction("Focus Document Editor", self) - self.aFocusEditor.setStatusTip("Move focus to left document pane") - self.aFocusEditor.setShortcut("Alt+2") - self.aFocusEditor.triggered.connect(lambda : self.theParent.setFocus(2)) - self.viewMenu.addAction(self.aFocusEditor) - - # View > Document Pane 2 - self.aFocusView = QAction("Focus Document Viewer", self) - self.aFocusView.setStatusTip("Move focus to right document pane") - self.aFocusView.setShortcut("Alt+3") - self.aFocusView.triggered.connect(lambda : self.theParent.setFocus(3)) - self.viewMenu.addAction(self.aFocusView) - - # View > Separator - self.viewMenu.addSeparator() - - # View > Toggle Distraction Free Mode - self.aZenMode = QAction("Zen Mode", self) - self.aZenMode.setStatusTip("Toggles distraction free mode, only showing text editor") - self.aZenMode.setShortcut("F8") - self.aZenMode.setCheckable(True) - self.aZenMode.setChecked(self.theParent.isZenMode) - self.aZenMode.toggled.connect(self.theParent.toggleZenMode) - self.viewMenu.addAction(self.aZenMode) - - # View > Toggle Full Screen - self.aFullScreen = QAction("Full Screen Mode", self) - self.aFullScreen.setStatusTip("Maximises the main window") - self.aFullScreen.setShortcut("F11") - self.aFullScreen.triggered.connect(self.theParent.toggleFullScreenMode) - self.viewMenu.addAction(self.aFullScreen) - - return - def _buildEditMenu(self): # Edit @@ -497,6 +452,65 @@ class GuiMainMenu(QMenuBar): return + def _buildInsertMenu(self): + + # Insert + self.insertMenu = self.addMenu("&Insert") + + # Insert > Short Dash + self.aInsENDash = QAction("Short Dash", self) + self.aInsENDash.setStatusTip("Insert short dash") + self.aInsENDash.setShortcut("Ctrl+K, -") + self.aInsENDash.triggered.connect(lambda: self._docInsert(nwDocInsert.SHORT_DASH)) + self.insertMenu.addAction(self.aInsENDash) + + # Insert > Long Dash + self.aInsEMDash = QAction("Long Dash", self) + self.aInsEMDash.setStatusTip("Insert long dash") + self.aInsEMDash.setShortcut("Ctrl+K, _") + self.aInsEMDash.triggered.connect(lambda: self._docInsert(nwDocInsert.LONG_DASH)) + self.insertMenu.addAction(self.aInsEMDash) + + # Insert > Ellipsis + self.aInsEllipsis = QAction("Ellipsis", self) + self.aInsEllipsis.setStatusTip("Insert ellipsis") + self.aInsEllipsis.setShortcut("Ctrl+K, .") + self.aInsEllipsis.triggered.connect(lambda: self._docInsert(nwDocInsert.ELLIPSIS)) + self.insertMenu.addAction(self.aInsEllipsis) + + # Insert > Separator + self.insertMenu.addSeparator() + + # Insert > Hard Line Break + self.aInsHardBreak = QAction("Hard Line Break", self) + self.aInsHardBreak.setStatusTip("Insert a hard line break") + self.aInsHardBreak.setShortcut("Ctrl+K, Return") + self.aInsHardBreak.triggered.connect(lambda: self._docInsert(nwDocInsert.HARD_BREAK)) + self.insertMenu.addAction(self.aInsHardBreak) + + # Insert > Non-Breaking Space + self.aInsNBSpace = QAction("Non-Breaking Space", self) + self.aInsNBSpace.setStatusTip("Insert a non-breaking space") + self.aInsNBSpace.setShortcut("Ctrl+K, Space") + self.aInsNBSpace.triggered.connect(lambda: self._docInsert(nwDocInsert.NB_SPACE)) + self.insertMenu.addAction(self.aInsNBSpace) + + # Insert > Thin Space + self.aInsThinSpace = QAction("Thin Space", self) + self.aInsThinSpace.setStatusTip("Insert a thin space") + self.aInsThinSpace.setShortcut("Ctrl+K, Shift+Space") + self.aInsThinSpace.triggered.connect(lambda: self._docInsert(nwDocInsert.THIN_SPACE)) + self.insertMenu.addAction(self.aInsThinSpace) + + # Insert > Thin Non-Breaking Space + self.aInsThinNBSpace = QAction("Thin Non-Breaking Space", self) + self.aInsThinNBSpace.setStatusTip("Insert a thin non-breaking space") + self.aInsThinNBSpace.setShortcut("Ctrl+K, Ctrl+Space") + self.aInsThinNBSpace.triggered.connect(lambda: self._docInsert(nwDocInsert.THIN_NB_SPACE)) + self.insertMenu.addAction(self.aInsThinNBSpace) + + return + def _buildFormatMenu(self): # Format @@ -594,6 +608,53 @@ class GuiMainMenu(QMenuBar): return + def _buildViewMenu(self): + + # View + self.viewMenu = self.addMenu("&View") + + # View > TreeView + self.aFocusTree = QAction("Focus Project Tree", self) + self.aFocusTree.setStatusTip("Move focus to project tree") + self.aFocusTree.setShortcut("Alt+1") + self.aFocusTree.triggered.connect(lambda : self.theParent.setFocus(1)) + self.viewMenu.addAction(self.aFocusTree) + + # View > Document Pane 1 + self.aFocusEditor = QAction("Focus Document Editor", self) + self.aFocusEditor.setStatusTip("Move focus to left document pane") + self.aFocusEditor.setShortcut("Alt+2") + self.aFocusEditor.triggered.connect(lambda : self.theParent.setFocus(2)) + self.viewMenu.addAction(self.aFocusEditor) + + # View > Document Pane 2 + self.aFocusView = QAction("Focus Document Viewer", self) + self.aFocusView.setStatusTip("Move focus to right document pane") + self.aFocusView.setShortcut("Alt+3") + self.aFocusView.triggered.connect(lambda : self.theParent.setFocus(3)) + self.viewMenu.addAction(self.aFocusView) + + # View > Separator + self.viewMenu.addSeparator() + + # View > Toggle Distraction Free Mode + self.aZenMode = QAction("Zen Mode", self) + self.aZenMode.setStatusTip("Toggles distraction free mode, only showing text editor") + self.aZenMode.setShortcut("F8") + self.aZenMode.setCheckable(True) + self.aZenMode.setChecked(self.theParent.isZenMode) + self.aZenMode.toggled.connect(self.theParent.toggleZenMode) + self.viewMenu.addAction(self.aZenMode) + + # View > Toggle Full Screen + self.aFullScreen = QAction("Full Screen Mode", self) + self.aFullScreen.setStatusTip("Maximises the main window") + self.aFullScreen.setShortcut("F11") + self.aFullScreen.triggered.connect(self.theParent.toggleFullScreenMode) + self.viewMenu.addAction(self.aFullScreen) + + return + def _buildToolsMenu(self): # Tools diff --git a/nw/guimain.py b/nw/guimain.py index 70a6ba0b..a82d5a99 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -966,12 +966,15 @@ class GuiMain(QMainWindow): """Connect to the main window all menu actions that need to be available also when the main menu is hidden. """ + # Project self.addAction(self.mainMenu.aSaveProject) self.addAction(self.mainMenu.aExitNW) + + # Document self.addAction(self.mainMenu.aSaveDoc) self.addAction(self.mainMenu.aFileDetails) - self.addAction(self.mainMenu.aZenMode) - self.addAction(self.mainMenu.aFullScreen) + + # Edit self.addAction(self.mainMenu.aEditUndo) self.addAction(self.mainMenu.aEditRedo) self.addAction(self.mainMenu.aEditCut) @@ -979,6 +982,17 @@ class GuiMain(QMainWindow): self.addAction(self.mainMenu.aEditPaste) self.addAction(self.mainMenu.aSelectAll) self.addAction(self.mainMenu.aSelectPar) + + # Insert + self.addAction(self.mainMenu.aInsENDash) + self.addAction(self.mainMenu.aInsEMDash) + self.addAction(self.mainMenu.aInsEllipsis) + self.addAction(self.mainMenu.aInsHardBreak) + self.addAction(self.mainMenu.aInsNBSpace) + self.addAction(self.mainMenu.aInsThinSpace) + self.addAction(self.mainMenu.aInsThinNBSpace) + + # Format self.addAction(self.mainMenu.aFmtItalic) self.addAction(self.mainMenu.aFmtBold) self.addAction(self.mainMenu.aFmtBoldIt) @@ -990,10 +1004,19 @@ class GuiMain(QMainWindow): self.addAction(self.mainMenu.aFmtHead4) self.addAction(self.mainMenu.aFmtComment) self.addAction(self.mainMenu.aFmtNoFormat) + + # View + self.addAction(self.mainMenu.aZenMode) + self.addAction(self.mainMenu.aFullScreen) + + # Tools self.addAction(self.mainMenu.aSpellCheck) self.addAction(self.mainMenu.aReRunSpell) self.addAction(self.mainMenu.aPreferences) + + # Help self.addAction(self.mainMenu.aHelp) + return True def _setWindowTitle(self, projName=None): From fdf79157471abb74781185dbb13d46cb263492c5 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 17:35:35 +0200 Subject: [PATCH 61/72] Updated sample text with extra descriptions --- sample/content/636b6aa9b697b.nwd | 4 +++- sample/nwProject.nwx | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 4916b753..0ae22a00 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -13,7 +13,9 @@ In addition, the editor supports automatic formatting of “quotes”, both doub If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, let’s auto-replace this A with , and this C with . While is just . Press Ctrl+R to see what this looks like in the view pane. -The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. +The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported – and can be automatically inserted when typing two hyphens.⎄ + +Thin spaces and thin non-breaking spaces are also supported from the Insert menu, and can be used to separate numbers from their units, like: 25 kg. #### Some Section Here diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index aeba369e..23daf66e 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,21 +1,21 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 332 - 56 - 10692 + 404 + 70 + 14966 False True True 636b6aa9b697b - 636b6aa9b697b - 927 + bb2c23b3c42cc + 967 B E @@ -114,10 +114,10 @@ 1st Draft True SCENE - 1240 - 223 - 7 - 403 + 1484 + 263 + 8 + 1087 Another Scene From ee23f937f20af10f5b3e016667529010ffa08723 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 19:51:23 +0200 Subject: [PATCH 62/72] Added issues tracker menu entry, and made a few minor changes --- nw/__init__.py | 1 + nw/gui/mainmenu.py | 34 ++++++++++++++++++++++++++------ sample/content/636b6aa9b697b.nwd | 2 +- sample/nwProject.nwx | 12 +++++------ 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/nw/__init__.py b/nw/__init__.py index 6ab06265..92fd0ac2 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -47,6 +47,7 @@ __maintainer__ = "Veronica Berglyd Olsen" __email__ = "code@vkbo.net" __status__ = "Pre-Release" __url__ = "https://github.com/vkbo/novelWriter" +__issuesurl__ = "https://github.com/vkbo/novelWriter/issues" __domain__ = "novelwriter.io" __docurl__ = "https://novelwriter.readthedocs.io" __credits__ = [ diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 7f7c9164..d831da81 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -28,7 +28,7 @@ import logging import nw -from PyQt5.QtCore import Qt, QUrl +from PyQt5.QtCore import QUrl from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QMenuBar, QAction, QMessageBox @@ -99,6 +99,8 @@ class GuiMainMenu(QMenuBar): ## def _menuExit(self): + """Exit novelWriter. + """ self.theParent.closeMain() return @@ -125,19 +127,33 @@ class GuiMainMenu(QMenuBar): return True def _showAboutQt(self): + """Show Qt's own About dialog. + """ msgBox = QMessageBox() msgBox.aboutQt(self.theParent,"About Qt") return True def _openHelp(self): + """Open the documentation URL in the system's default browser. + """ QDesktopServices.openUrl(QUrl(nw.__docurl__)) return True + def _openIssue(self): + """Open the issue tracker URL in the system's default browser. + """ + QDesktopServices.openUrl(QUrl(nw.__issuesurl__)) + return True + def _showDocumentLocation(self): + """Open the dialog showing the location of the editor document. + """ self.theParent.docEditor.revealLocation() return True def _doBackup(self): + """Call the backup function for the project. + """ self.theProject.zipIt(True) return True @@ -230,14 +246,14 @@ class GuiMainMenu(QMenuBar): self.projMenu.addSeparator() # Project > Edit - self.aEditItem = QAction("&Edit Project Item", self) + self.aEditItem = QAction("Edit Project Item", self) self.aEditItem.setStatusTip("Change item settings") self.aEditItem.setShortcuts(["Ctrl+E", "F2"]) self.aEditItem.triggered.connect(self.theParent.editItem) self.projMenu.addAction(self.aEditItem) # Project > Delete - self.aDeleteItem = QAction("&Delete Project Item", self) + self.aDeleteItem = QAction("Delete Project Item", self) self.aDeleteItem.setStatusTip("Delete selected item") self.aDeleteItem.setShortcut("Ctrl+Del") self.aDeleteItem.triggered.connect(lambda : self.theParent.treeView.deleteItem(None)) @@ -267,21 +283,21 @@ class GuiMainMenu(QMenuBar): self.docuMenu = self.addMenu("&Document") # Document > New - self.aNewDoc = QAction("&New Document", self) + self.aNewDoc = QAction("New Document", self) self.aNewDoc.setStatusTip("Create new document") self.aNewDoc.setShortcut("Ctrl+N") self.aNewDoc.triggered.connect(lambda : self._newTreeItem(nwItemType.FILE, None)) self.docuMenu.addAction(self.aNewDoc) # Document > Open - self.aOpenDoc = QAction("&Open Document", self) + self.aOpenDoc = QAction("Open Document", self) self.aOpenDoc.setStatusTip("Open selected document") self.aOpenDoc.setShortcut("Ctrl+O") self.aOpenDoc.triggered.connect(self.theParent.openSelectedItem) self.docuMenu.addAction(self.aOpenDoc) # Document > Save - self.aSaveDoc = QAction("&Save Document", self) + self.aSaveDoc = QAction("Save Document", self) self.aSaveDoc.setStatusTip("Save current document") self.aSaveDoc.setShortcut("Ctrl+S") self.aSaveDoc.triggered.connect(self.theParent.saveDocument) @@ -764,6 +780,12 @@ class GuiMainMenu(QMenuBar): self.aHelp.triggered.connect(self._openHelp) self.helpMenu.addAction(self.aHelp) + # Document > Report Issue + self.aIssue = QAction("Report an Issue", self) + self.aIssue.setStatusTip("View online documentation") + self.aIssue.triggered.connect(self._openIssue) + self.helpMenu.addAction(self.aIssue) + return # END Class GuiMainMenu diff --git a/sample/content/636b6aa9b697b.nwd b/sample/content/636b6aa9b697b.nwd index 0ae22a00..394a96ce 100644 --- a/sample/content/636b6aa9b697b.nwd +++ b/sample/content/636b6aa9b697b.nwd @@ -13,7 +13,7 @@ In addition, the editor supports automatic formatting of “quotes”, both doub If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, let’s auto-replace this A with , and this C with . While is just . Press Ctrl+R to see what this looks like in the view pane. -The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported – and can be automatically inserted when typing two hyphens.⎄ +The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported – and can be automatically inserted when typing two hyphens. Thin spaces and thin non-breaking spaces are also supported from the Insert menu, and can be used to separate numbers from their units, like: 25 kg. diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 23daf66e..4ce89213 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 404 - 70 - 14966 + 407 + 71 + 15118 False @@ -114,10 +114,10 @@ 1st Draft True SCENE - 1484 + 1483 263 8 - 1087 + 1086 Another Scene From b0380adc5f1876eaa39573fc1776abf9f18f00d1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 21:55:43 +0200 Subject: [PATCH 63/72] Added line and paragraph separatir unicodes back into nwUnicode class --- nw/constants/constants.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 1ff5da4f..e4e6af98 100644 --- a/nw/constants/constants.py +++ b/nw/constants/constants.py @@ -229,10 +229,12 @@ class nwUnicode: U_EMDASH = "\u2014" # Long dash U_HELLIP = "\u2026" # Ellipsis - ## Spaces + ## Spaces and Lines U_NBSP = "\u00a0" # Non-breaking space U_THNSP = "\u2009" # Thin space U_THNBSP = "\u202f" # Thin non-breaking space + U_LSEP = "\u2028" # Line separator + U_PSEP = "\u2029" # Paragraph separator ## Symbols U_CHECK = "\u2714" # Heavy check mark From 03f1142e40da2fbf05b614cd160adb0ea87c17e1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 21:56:37 +0200 Subject: [PATCH 64/72] Added replace quotes function, and cleaned up some buggy behaviour with undo and how changes are tracked --- nw/constants/enum.py | 2 + nw/gui/doceditor.py | 114 +++++++++++++++++++++++++++++++++++-------- nw/gui/mainmenu.py | 17 ++++++- 3 files changed, 111 insertions(+), 22 deletions(-) diff --git a/nw/constants/enum.py b/nw/constants/enum.py index 84f8d4f5..7177223c 100644 --- a/nw/constants/enum.py +++ b/nw/constants/enum.py @@ -93,6 +93,8 @@ class nwDocAction(Enum): BLOCK_H4 = 22 BLOCK_COM = 23 BLOCK_TXT = 24 + REPL_SNG = 25 + REPL_DBL = 26 # END Enum nwDocAction diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 35ac09d8..db989d45 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -45,11 +45,12 @@ from PyQt5.QtWidgets import ( QFrame ) -from nw.core import NWDoc +from nw.core import NWDoc, NWSpellSimple, countWords from nw.gui.dochighlight import GuiDocHighlighter -from nw.core import NWSpellSimple, countWords -from nw.constants import nwUnicode, nwDocAction, nwDocInsert, nwInsertSymbols from nw.common import transferCase +from nw.constants import ( + nwAlert, nwUnicode, nwDocAction, nwDocInsert, nwInsertSymbols +) logger = logging.getLogger(__name__) @@ -76,6 +77,7 @@ class GuiDocEditor(QTextEdit): self.paraCount = 0 self.lastEdit = 0 self.bigDoc = False + self.doReplace = False self.nonWord = "\"'" # Document State @@ -157,6 +159,7 @@ class GuiDocEditor(QTextEdit): self.paraCount = 0 self.lastEdit = 0 self.bigDoc = False + self.doReplace = False self.hasSelection = False @@ -261,7 +264,9 @@ class GuiDocEditor(QTextEdit): self.hLight.spellCheck = False bfTime = time() + self._allowAutoReplace(False) self.setPlainText(theDoc) + self._allowAutoReplace(True) afTime = time() logger.debug("Document highlighted in %.3f milliseconds" % (1000*(afTime-bfTime))) @@ -388,8 +393,8 @@ class GuiDocEditor(QTextEdit): """ if self.mainConf.verQtValue >= 50900: theText = self.qDocument.toRawText() - theText = theText.replace("\u2028", "\n") # Line separators - theText = theText.replace("\u2029", "\n") # Paragraph separators + theText = theText.replace(nwUnicode.U_LSEP, "\n") # Line separators + theText = theText.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators else: theText = self.toPlainText() return theText @@ -496,6 +501,7 @@ class GuiDocEditor(QTextEdit): if not self.theParent.hasProject: logger.error("No project open") return False + self._allowAutoReplace(False) if theAction == nwDocAction.UNDO: self.undo() elif theAction == nwDocAction.REDO: @@ -544,9 +550,15 @@ class GuiDocEditor(QTextEdit): self._formatBlock(nwDocAction.BLOCK_COM) elif theAction == nwDocAction.BLOCK_TXT: self._formatBlock(nwDocAction.BLOCK_TXT) + elif theAction == nwDocAction.REPL_SNG: + self._replaceQuotes("'", self.typSQOpen, self.typSQClose) + elif theAction == nwDocAction.REPL_DBL: + self._replaceQuotes("\"", self.typDQOpen, self.typDQClose) else: logger.debug("Unknown or unsupported document action %s" % str(theAction)) + self._allowAutoReplace(True) return False + self._allowAutoReplace(True) return True def isEmpty(self): @@ -597,24 +609,23 @@ class GuiDocEditor(QTextEdit): def keyPressEvent(self, keyEvent): """Intercept key press events. - We need to intercept key presses briefly to record the state of - selection. This is in order to know whether we had a selection - prior to triggering the _docChange slot, as we do not want to - trigger autoreplace on selections. Autoreplace on selections - messes with undo/redo history. - We also need to intercept the Shift key modifier for certain key - combinations that modifies standard keys like enter and space. - However, we don't want to spend a lot of time in this function - as it is triggered on every keypress when typing. + We need to intercept a few key sequences: + * The return key redirects here even if the search box has + focus. Since we need the return key to continue search, we + block any further interaction here while it's in focus. + * The undo sequence bypasses the doAction pathway from the + menu, so we redirect it back from here. + * The default redo sequence is Ctrl+Shift+Z, which we don't + use, so we block it. """ - self.hasSelection = self.textCursor().hasSelection() if self.docSearch.searchBox.hasFocus(): - # Block the event when the focus is on the search bar as it - # seems to be capturing the return key. return - - QTextEdit.keyPressEvent(self, keyEvent) - + elif keyEvent == QKeySequence.Redo: + return + elif keyEvent == QKeySequence.Undo: + self.docAction(nwDocAction.UNDO) + else: + QTextEdit.keyPressEvent(self, keyEvent) return def mouseReleaseEvent(self, mEvent): @@ -650,7 +661,7 @@ class GuiDocEditor(QTextEdit): self.setDocumentChanged(True) if not self.wcTimer.isActive(): self.wcTimer.start() - if self.mainConf.doReplace and not self.hasSelection: + if self.doReplace and charsAdded == 1: self._docAutoReplace(self.qDocument.findBlock(thePos)) return @@ -862,6 +873,58 @@ class GuiDocEditor(QTextEdit): return + def _replaceQuotes(self, sQuote, oQuote, cQuote): + """Replace all straight quotes in the selected text. + """ + theCursor = self.textCursor() + if theCursor.hasSelection(): + posS = theCursor.selectionStart() + posE = theCursor.selectionEnd() + closeCheck = ( + " ", "\n", nwUnicode.U_LSEP, nwUnicode.U_PSEP + ) + + self._allowAutoReplace(False) + for posC in range(posS, posE+1): + theCursor.setPosition(posC) + theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2) + selText = theCursor.selectedText() + + nS = len(selText) + if nS == 2: + pC = selText[0] + cC = selText[1] + elif nS == 1: + pC = " " + cC = selText[0] + else: + continue + + if cC != sQuote: + continue + + theCursor.clearSelection() + theCursor.setPosition(posC) + if pC in closeCheck: + theCursor.beginEditBlock() + theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1) + theCursor.insertText(oQuote) + theCursor.endEditBlock() + else: + theCursor.beginEditBlock() + theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1) + theCursor.insertText(cQuote) + theCursor.endEditBlock() + + self._allowAutoReplace(True) + + else: + self.theParent.makeAlert( + "Please selection some text before calling replace quotes.", nwAlert.ERROR + ) + + return + def _checkDocSize(self, theSize): """Check if document size crosses the big document limit set in config. If so, we will set the big document flag to True. @@ -1225,6 +1288,15 @@ class GuiDocEditor(QTextEdit): return + def _allowAutoReplace(self, theState): + """used to enable/disable the auto-replace feature temporarily. + """ + if theState: + self.doReplace = self.mainConf.doReplace + else: + self.doReplace = False + return + # END Class GuiDocEditor # =============================================================================================== # diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index d831da81..1f78212d 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -577,7 +577,7 @@ class GuiMainMenu(QMenuBar): self.aFmtSQuote.triggered.connect(lambda: self._docAction(nwDocAction.S_QUOTE)) self.fmtMenu.addAction(self.aFmtSQuote) - # Edit > Separator + # Format > Separator self.fmtMenu.addSeparator() # Format > Header 1 @@ -622,6 +622,21 @@ class GuiMainMenu(QMenuBar): self.aFmtNoFormat.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_TXT)) self.fmtMenu.addAction(self.aFmtNoFormat) + # Format > Separator + self.fmtMenu.addSeparator() + + # Format > Replace Single Quotes + self.aFmtReplSng = QAction("Replace Single Quotes", self) + self.aFmtReplSng.setStatusTip("Replace all straight single quotes in selected text") + self.aFmtReplSng.triggered.connect(lambda: self._docAction(nwDocAction.REPL_SNG)) + self.fmtMenu.addAction(self.aFmtReplSng) + + # Format > Replace Double Quotes + self.aFmtReplDbl = QAction("Replace Double Quotes", self) + self.aFmtReplDbl.setStatusTip("Replace all straight double quotes in selected text") + self.aFmtReplDbl.triggered.connect(lambda: self._docAction(nwDocAction.REPL_DBL)) + self.fmtMenu.addAction(self.aFmtReplDbl) + return def _buildViewMenu(self): From d6dd776056a4d0ea9faaa59b6325cd35f416da38 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 22:17:57 +0200 Subject: [PATCH 65/72] Delete unused variable --- nw/gui/doceditor.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index db989d45..3af27d07 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -80,9 +80,6 @@ class GuiDocEditor(QTextEdit): self.doReplace = False self.nonWord = "\"'" - # Document State - self.hasSelection = False - # Typography self.typDQOpen = self.mainConf.fmtDoubleQuotes[0] self.typDQClose = self.mainConf.fmtDoubleQuotes[1] @@ -161,8 +158,6 @@ class GuiDocEditor(QTextEdit): self.bigDoc = False self.doReplace = False - self.hasSelection = False - self.setDocumentChanged(False) self.docHeader.setTitleFromHandle(self.theHandle) From b19c106b45003cd3cbff7bf2ea9cf950f3f7dae7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 22:46:22 +0200 Subject: [PATCH 66/72] CHange the order of preference for the view document action --- nw/guimain.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/nw/guimain.py b/nw/guimain.py index a82d5a99..cd479c9e 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -518,20 +518,30 @@ class GuiMain(QMainWindow): """Load a document for viewing in the view panel. """ if tHandle is None: - tHandle = self.treeView.getSelectedHandle() - if tHandle is None: - logger.debug("No document selected, trying editor document") - tHandle = self.docEditor.theHandle - if tHandle is None: - logger.debug("No document selected, trying last viewed") - tHandle = self.theProject.lastViewed - if tHandle is None: - logger.debug("No document selected, giving up") - return False + logger.debug("Viewing document, but no handle provided") + + if self.docEditor.hasFocus(): + logger.verbose("Trying editor document") + tHandle = self.docEditor.theHandle + + if tHandle is not None: + self.saveDocument() + else: + logger.verbose("Trying selected document") + tHandle = self.treeView.getSelectedHandle() + + if tHandle is None: + logger.verbose("Trying last viewed document") + tHandle = self.theProject.lastViewed + + if tHandle is None: + logger.verbose("No document to view, giving up") + return False # Make sure main tab is in Editor view self.tabWidget.setCurrentWidget(self.splitDocs) + logger.debug("Viewing document with handle %s" % tHandle) if self.docViewer.loadText(tHandle): if not self.splitView.isVisible(): bPos = self.splitMain.sizes() From 747ff63a9247db4bb4c2c6d497f104f8da83de0c Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 23:01:22 +0200 Subject: [PATCH 67/72] Also save first if pressing the relaod button from the doc viewer header bar --- nw/gui/docviewer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index b280aa01..93887fdf 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -529,6 +529,8 @@ class GuiDocViewHeader(QWidget): def _refreshDocument(self): """Reload the content of the document. """ + if self.docViewer.theHandle == self.theParent.docEditor.theHandle: + self.theParent.saveDocument() self.docViewer.reloadText() return From 5a373f1cda304ec05028466712e7801ad0911d92 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 19 Jun 2020 23:18:17 +0200 Subject: [PATCH 68/72] Save the window size and replace column width in project settings --- nw/core/options.py | 5 +++++ nw/gui/build.py | 4 ++-- nw/gui/projsettings.py | 41 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/nw/core/options.py b/nw/core/options.py index 25bf1fb4..0ff7c919 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -78,6 +78,11 @@ class OptionState(): "columnWidth", "columnHidden", }, + "GuiProjectSettings": { + "winWidth", + "winHeight", + "replaceColW", + } } return diff --git a/nw/gui/build.py b/nw/gui/build.py index 51a9dd68..e949a846 100644 --- a/nw/gui/build.py +++ b/nw/gui/build.py @@ -871,8 +871,8 @@ class GuiBuildNovel(QDialog): "section" : self.fmtSection.text().strip(), }) - winWidth = self.mainConf.pxInt(self.width()) - winHeight = self.mainConf.pxInt(self.height()) + winWidth = self.mainConf.rpxInt(self.width()) + winHeight = self.mainConf.rpxInt(self.height()) justifyText = self.justifyText.isChecked() noStyling = self.noStyling.isChecked() textFont = self.textFont.text() diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index 8f5dacb3..6f8f32ba 100644 --- a/nw/gui/projsettings.py +++ b/nw/gui/projsettings.py @@ -51,9 +51,17 @@ class GuiProjectSettings(PagedDialog): self.mainConf = nw.CONFIG self.theParent = theParent self.theProject = theProject + self.optState = theProject.optState self.theProject.countStatus() self.setWindowTitle("Project Settings") + self.setMinimumWidth(self.mainConf.pxInt(570)) + self.setMinimumHeight(self.mainConf.pxInt(355)) + + self.resize( + self.mainConf.pxInt(self.optState.getInt("GuiProjectSettings", "winWidth", 570)), + self.mainConf.pxInt(self.optState.getInt("GuiProjectSettings", "winHeight", 355)) + ) self.tabMain = GuiProjectEditMain(self.theParent, self.theProject) self.tabMeta = GuiProjectEditMeta(self.theParent, self.theProject) @@ -76,7 +84,13 @@ class GuiProjectSettings(PagedDialog): return + ## + # Slots + ## + def _doSave(self): + """Save settings and close dialog. + """ logger.verbose("GuiProjectSettings save button clicked") projName = self.tabMain.editName.text() @@ -100,13 +114,23 @@ class GuiProjectSettings(PagedDialog): newList = self.tabReplace.getNewList() self.theProject.setAutoReplace(newList) - self.close() + self._doClose() return def _doClose(self): - logger.verbose("GuiProjectSettings close button clicked") + """Close the dialog. + """ + winWidth = self.mainConf.rpxInt(self.width()) + winHeight = self.mainConf.rpxInt(self.height()) + replaceColW = self.mainConf.rpxInt(self.tabReplace.listBox.columnWidth(0)) + + self.optState.setValue("GuiProjectSettings", "winWidth", winWidth) + self.optState.setValue("GuiProjectSettings", "winHeight", winHeight) + self.optState.setValue("GuiProjectSettings", "replaceColW", replaceColW) + self.close() + return # END Class GuiProjectSettings @@ -452,16 +476,23 @@ class GuiProjectEditReplace(QWidget): def __init__(self, theParent, theProject): QWidget.__init__(self, theParent) + self.mainConf = nw.CONFIG self.theParent = theParent self.theTheme = theParent.theTheme self.theProject = theProject + self.optState = theProject.optState self.arChanged = False - self.outerBox = QVBoxLayout() - self.bottomBox = QHBoxLayout() - self.listBox = QTreeWidget() + self.outerBox = QVBoxLayout() + self.bottomBox = QHBoxLayout() + + wCol0 = self.mainConf.pxInt( + self.optState.getInt("GuiProjectSettings", "replaceColW", 100) + ) + self.listBox = QTreeWidget() self.listBox.setHeaderLabels(["Keyword","Replace With"]) self.listBox.itemSelectionChanged.connect(self._selectedItem) + self.listBox.setColumnWidth(0, wCol0) self.listBox.setIndentation(0) for aKey, aVal in self.theProject.autoReplace.items(): From 1d6f346955fd73b4b56c5a7bd497449e519c5c73 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 20 Jun 2020 18:17:48 +0200 Subject: [PATCH 69/72] Don't scroll project tree every time a document is opened --- nw/gui/doceditor.py | 2 +- nw/gui/docviewer.py | 2 +- nw/gui/projtree.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 3af27d07..b932b42c 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -1815,7 +1815,7 @@ class GuiDocEditHeader(QWidget): """Capture a click on the title and ensure that the item is selected in the project tree. """ - self.theParent.treeView.setSelectedHandle(self.theHandle) + self.theParent.treeView.setSelectedHandle(self.theHandle, doScroll=True) return # END Class GuiDocEditHeader diff --git a/nw/gui/docviewer.py b/nw/gui/docviewer.py index 93887fdf..17a7c079 100644 --- a/nw/gui/docviewer.py +++ b/nw/gui/docviewer.py @@ -542,7 +542,7 @@ class GuiDocViewHeader(QWidget): """Capture a click on the title and ensure that the item is selected in the project tree. """ - self.theParent.treeView.setSelectedHandle(self.theHandle) + self.theParent.treeView.setSelectedHandle(self.theHandle, doScroll=True) return # END Class GuiDocViewHeader diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 236b9f90..7c2bc7a4 100644 --- a/nw/gui/projtree.py +++ b/nw/gui/projtree.py @@ -569,14 +569,14 @@ class GuiProjectTree(QTreeWidget): selHandles.append(selItems[n].data(self.C_NAME, Qt.UserRole)) return selHandles - def setSelectedHandle(self, tHandle): + def setSelectedHandle(self, tHandle, doScroll=False): """Set a specific handle as the selected item. """ if tHandle in self.theMap: self.clearSelection() self.theMap[tHandle].setSelected(True) selItems = self.selectedIndexes() - if selItems: + if selItems and doScroll: self.scrollTo( selItems[0], QAbstractItemView.PositionAtCenter ) From b46177b40b5709e746ee377303cba085001a8ca3 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 20 Jun 2020 19:47:39 +0200 Subject: [PATCH 70/72] Fixed test --- tests/reference/gui/3_nwProject.nwx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/reference/gui/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index 038ee8f2..4d6ef96a 100644 --- a/tests/reference/gui/3_nwProject.nwx +++ b/tests/reference/gui/3_nwProject.nwx @@ -41,14 +41,14 @@ ROOT NOVEL New - True + False New Chapter FOLDER NOVEL New - True + False Just a Page From 24d79ade586f8ce8f42e41570694e5d67a018dd1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 20 Jun 2020 20:10:31 +0200 Subject: [PATCH 71/72] Updated changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0bbdb6a..059fd081 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ * Underline text formatting has been removed. It is not standard HTML5, not Markdown, and was previously implemented using the double underscore notation that in standard Markdown is renderred as bold text. Instead, novelWriter now renders a single `*` or `-` wrapping a piece of text *within* a paragraphs as italicised text, and a double `**` or `__` as bold text. The keyboard shortcuts and automatic features **only** support the `*` notation. A triple set of `***` are treated as both bold and italicised. PR #310. * Strikethrough formatting has been added back into novelWriter using the standard Markdown `~~` wrapping. PR #310. +* Added support for thin spaces and non-breaking thin spaces. PR #319. +* The `Ctrl+Z` key sequence would not go through the wrapper function for document action for the document editor, but act directly on the document. This caused some of the logic preventing conflict between auto-replace and undo to be bypassed. This has now been resolved by blocking the keypress itself. Issue #320, PR #321. +* The dialog window size and column width setting for the auto-replace feature in Project Settings are now preserved between closing and opening the dialog. Issue #322, PR #324. **User Interface** @@ -13,6 +16,10 @@ * The text emphasis functions, either selected from the menu or via keyboard shortcuts, will now try to respond to the command in a more meaningful way. That is, the text editor will try to toggle the bold or italics features independently of eachother on the selected text. A feature to apply both at the same time has also been added. PR #310. * The document editor search tool has been completely rewritten. It now appears as a search box at the top of the document, and has a number of toggle switches added to it. You can modify the search tool to be case sensitive, select only whole words, use regular expression search strings, loop when reaching the end, and continue the search in next file. For the replace feature, you can also select to have the feature try to preserve the case of the replaced word. Issues #84 and #305, PR #314. * A dialog has been added for selecting quotation mark style. These are now used in the Preferences dialog instead of a plain text box. PR #317. +* Added an insert menu for inserting special symbols like dashes, ellipsis, thin and non-breaking spaces, and hard line breaks. PR #319. +* A menu option to replace straight single and double quotes in a selected piece of text has been added. This uses the same logic as the auto-replace feature. Issue #312, PR #321. +* When pressing `Ctrl+R` while the document editor has focus, the edited document will be viewed or refreshed in the document viewer. Previously, the selected document in the project tree had priority. The document is also now saved before loaded in the viewer, censuring that it shows the very latest changes. Issue #143, PR #323. +* The selection in the project tree should not scroll into view when just opening the document. This can be quite annoying if loading several documents in sequence by double-clicking, as the target may move just when you're about to click. PR #325. **Other Changes** From 51ddfb3cee068bf3cbf28bec5f6bb1151f15bedc Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 20 Jun 2020 20:12:12 +0200 Subject: [PATCH 72/72] Fixed a typo --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 059fd081..714ed4ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ **Core Functionality** -* Underline text formatting has been removed. It is not standard HTML5, not Markdown, and was previously implemented using the double underscore notation that in standard Markdown is renderred as bold text. Instead, novelWriter now renders a single `*` or `-` wrapping a piece of text *within* a paragraphs as italicised text, and a double `**` or `__` as bold text. The keyboard shortcuts and automatic features **only** support the `*` notation. A triple set of `***` are treated as both bold and italicised. PR #310. +* Underline text formatting has been removed. It is not standard HTML5, not Markdown, and was previously implemented using the double underscore notation that in standard Markdown is renderred as bold text. Instead, novelWriter now renders a single `*` or `_` wrapping a piece of text *within* a paragraphs as italicised text, and a double `**` or `__` as bold text. The keyboard shortcuts and automatic features **only** support the `*` notation. A triple set of `***` are treated as both bold and italicised. PR #310. * Strikethrough formatting has been added back into novelWriter using the standard Markdown `~~` wrapping. PR #310. * Added support for thin spaces and non-breaking thin spaces. PR #319. * The `Ctrl+Z` key sequence would not go through the wrapper function for document action for the document editor, but act directly on the document. This caused some of the logic preventing conflict between auto-replace and undo to be bypassed. This has now been resolved by blocking the keypress itself. Issue #320, PR #321.