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: 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/CHANGELOG.md b/CHANGELOG.md index 82f8a3cc..714ed4ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # novelWriter ChangeLog +## 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. +* 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** + +* 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. +* 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** + +* 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-06-14] **Bugfixes** 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/conf.py b/docs/source/conf.py index 4d8b3077..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" +version = "0.9" # The full version, including alpha/beta/rc tags -release = "0.8.0" +release = "0.9.0rc1" # -- General configuration --------------------------------------------------- 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." diff --git a/nw/__init__.py b/nw/__init__.py index 2adb8b23..92fd0ac2 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -40,13 +40,14 @@ __package__ = "novelWriter" __author__ = "Veronica Berglyd Olsen" __copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen" __license__ = "GPLv3" -__version__ = "0.8" -__hexversion__ = "0x000800f0" +__version__ = "0.9.0rc1" +__hexversion__ = "0x000900c1" __date__ = "2020-06-14" __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/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..1e57b60e --- /dev/null +++ b/nw/assets/icons/fallback/search_project-dark.svg @@ -0,0 +1,31 @@ + + + + + + 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..12afc290 --- /dev/null +++ b/nw/assets/icons/fallback/search_project.svg @@ -0,0 +1,31 @@ + + + + + + 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/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/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/constants/__init__.py b/nw/constants/__init__.py index 1abd529c..0e90354a 100644 --- a/nw/constants/__init__.py +++ b/nw/constants/__init__.py @@ -1,25 +1,30 @@ # -*- 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, + nwInsertSymbols ) from nw.constants.enum import ( - nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline + nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline, + nwDocInsert ) __all__ = [ "isoLanguage", "isoCountry", "nwConst", + "nwRegEx", "nwFiles", "nwKeyWords", "nwLabels", "nwQuotes", "nwUnicode", + "nwInsertSymbols", "nwAlert", "nwDocAction", "nwItemClass", "nwItemLayout", "nwItemType", "nwOutline", + "nwDocInsert", ] diff --git a/nw/constants/constants.py b/nw/constants/constants.py index 66b026fc..e4e6af98 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(): @@ -34,6 +34,15 @@ class nwConst(): # END Class nwConst +class nwRegEx(): + + FMT_B = r"(? 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/options.py b/nw/core/options.py index abab6a65..0ff7c919 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,17 @@ class OptionState(): "incComments", "incKeywords", "incBodyText", - ]), - "GuiOutline": set([ + }, + "GuiOutline": { "headerOrder", "columnWidth", "columnHidden", - ]), + }, + "GuiProjectSettings": { + "winWidth", + "winHeight", + "replaceColW", + } } return diff --git a/nw/core/project.py b/nw/core/project.py index 0784fc7a..3db93bf2 100644 --- a/nw/core/project.py +++ b/nw/core/project.py @@ -1075,17 +1075,22 @@ 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 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) - orphItem.setClass(nwItemClass.NO_CLASS) - orphItem.setLayout(nwItemLayout.NO_LAYOUT) + orphItem.setClass(oClass) + orphItem.setLayout(oLayout) self.projTree.append(oHandle, None, orphItem) return diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py index 845f355c..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 = [] @@ -117,14 +119,28 @@ class ToHtml(Tokenizer): """Convert the list of text tokens into a HTML document saved to theResult. """ - htmlTags = { - self.FMT_B_B : "", - self.FMT_B_E : "", - self.FMT_I_B : "", - self.FMT_I_E : "", - self.FMT_U_B : "", - self.FMT_U_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 70599d59..09345e09 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__) @@ -44,8 +44,10 @@ 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 + 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,23 +294,19 @@ 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 - # Keep in sync with the DocHighlighter class - rxFormats = [( - QRegularExpression(r"(?", "") + theText = theText.replace("", "") self.setHtml(theText) return 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 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 bd5da37f..b932b42c 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] WordCounter - 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 @@ -32,20 +34,23 @@ 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 ) from PyQt5.QtWidgets import ( qApp, QTextEdit, QAction, QMenu, QShortcut, QMessageBox, QWidget, QLabel, - QToolButton, QHBoxLayout + QToolBar, QToolButton, QHBoxLayout, QGridLayout, QLineEdit, QPushButton, + 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 +from nw.common import transferCase +from nw.constants import ( + nwAlert, nwUnicode, nwDocAction, nwDocInsert, nwInsertSymbols +) logger = logging.getLogger(__name__) @@ -72,6 +77,7 @@ class GuiDocEditor(QTextEdit): self.paraCount = 0 self.lastEdit = 0 self.bigDoc = False + self.doReplace = False self.nonWord = "\"'" # Typography @@ -82,11 +88,11 @@ class GuiDocEditor(QTextEdit): # Core Elements self.qDocument = self.document() - self.qDocument.setDocumentMargin(self.mainConf.getTextMargin()) self.qDocument.contentsChange.connect(self._docChange) # Document Title self.docHeader = GuiDocEditHeader(self) + self.docSearch = GuiDocEditSearch(self) # Syntax self.hLight = GuiDocHighlighter(self.qDocument, self.theParent) @@ -95,9 +101,9 @@ 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) # Custom Shortcuts @@ -126,7 +132,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() @@ -150,8 +156,7 @@ class GuiDocEditor(QTextEdit): self.paraCount = 0 self.lastEdit = 0 self.bigDoc = False - - self.hasSelection = False + self.doReplace = False self.setDocumentChanged(False) self.docHeader.setTitleFromHandle(self.theHandle) @@ -182,12 +187,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() @@ -251,7 +259,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))) @@ -309,6 +319,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,40 +333,26 @@ class GuiDocEditor(QTextEdit): tW = self.mainConf.getZenWidth() else: tW = self.mainConf.getTextWidth() - wW = self.width() - tM = int((wW - sW - tW)/2) + tM = (wW - sW - tW)//2 if tM < cM: tM = cM else: tM = cM tB = self.frameWidth() - tW = self.width() - 2*tB - sW + tW = wW - 2*tB - sW tH = self.docHeader.height() - tT = cM - tH self.docHeader.setGeometry(tB, tB, tW, tH) - self.setViewportMargins(0, tH, 0, 0) - docFormat = self.qDocument.rootFrame().frameFormat() - docFormat.setLeftMargin(tM) - docFormat.setRightMargin(tM) - if tT > 0: - docFormat.setTopMargin(tT) + if self.docSearch.isVisible(): + rH = self.docSearch.height() + rW = self.docSearch.width() + rL = wW - sW - rW - 2*tB + self.docSearch.move(rL, 2*tB) else: - docFormat.setTopMargin(0) + rH = 0 - # 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) + self.setViewportMargins(tM, max(cM, tH, rH), tM, cM) return @@ -391,8 +388,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 @@ -499,6 +496,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: @@ -509,16 +507,18 @@ 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.STRIKE: + self._toggleStrike() 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: @@ -530,7 +530,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: @@ -545,9 +545,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): @@ -571,37 +577,50 @@ 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. + """ + self.docSearch.closeSearch() + return self.docSearch.isVisible() + ## # Document Events and Maintenance ## 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 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) + if self.docSearch.searchBox.hasFocus(): + return + elif keyEvent == QKeySequence.Redo: + return + elif keyEvent == QKeySequence.Undo: + self.docAction(nwDocAction.UNDO) else: QTextEdit.keyPressEvent(self, keyEvent) - return def mouseReleaseEvent(self, mEvent): @@ -637,7 +656,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 @@ -788,24 +807,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. @@ -867,6 +868,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. @@ -881,18 +934,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 +955,134 @@ 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 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(): + posS = theCursor.selectionStart() + posE = theCursor.selectionEnd() + + numB = 0 + for n in range(3): + if self.qDocument.characterAt(posS-n-1) == "*": + numB += 1 + else: + break + + numA = 0 + for n in range(3): + if self.qDocument.characterAt(posE+n) == "*": + numA += 1 + else: + break + + cLevel = min(numB, numA) + 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: + # Already at 1 or 2, increase to 3 + self._wrapSelection("*"*(3 - cLevel)) + + 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. """ @@ -1003,7 +1182,8 @@ class GuiDocEditor(QTextEdit): selText = theCursor.selectedText() else: selText = "" - self.theParent.searchBar.setSearchText(selText) + self.docSearch.setSearchText(selText) + self.updateDocMargins() return def _beginReplace(self): @@ -1011,54 +1191,82 @@ 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. + 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 and loop is + enabled, or continues to next file if next file is enabled. """ - searchFor = self.theParent.searchBar.getSearchText() - wasFound = self.find(searchFor) - if not wasFound: - theCursor = self.textCursor() - theCursor.movePosition(QTextCursor.Start) - self.setTextCursor(theCursor) - return + if not self.docSearch.isVisible(): + self._beginSearch() + 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. - """ - searchFor = self.theParent.searchBar.getSearchText() - wasFound = self.find(searchFor, QTextDocument.FindBackward) + 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, findOpt) if not wasFound: - theCursor = self.textCursor() - theCursor.movePosition(QTextCursor.End) - self.setTextCursor(theCursor) + 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) + return 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() + return + + if not theCursor.hasSelection(): + return + theCursor = self.textCursor() - searchFor = self.theParent.searchBar.getSearchText() - replWith = self.theParent.searchBar.getReplaceText() - if theCursor.hasSelection() and theCursor.selectedText() == searchFor: - xPos = theCursor.selectionStart() + searchFor = self.docSearch.getSearchText() + replWith = self.docSearch.getReplaceText() + selText = theCursor.selectedText() + + if self.docSearch.doMatchCap: + replWith = transferCase(selText, replWith) + + if not self.docSearch.isCaseSense: + isMatch = searchFor.lower() == selText.lower() + else: + isMatch = searchFor == selText + + if isMatch: 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 def _setupSpellChecking(self): @@ -1075,13 +1283,27 @@ 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 -class WordCounter(QThread): +# =============================================================================================== # +# The Off GUI Thread Word Counter +# Runs the word counter in the background for the DocEditor +# =============================================================================================== # - def __init__(self, theParent): - QThread.__init__(self, theParent) - self.theParent = theParent +class BackgroundWordCounter(QThread): + + def __init__(self, docEditor): + QThread.__init__(self, docEditor) + self.docEditor = docEditor self.charCount = 0 self.wordCount = 0 self.paraCount = 0 @@ -1091,16 +1313,357 @@ class WordCounter(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 + +# =============================================================================================== # +# The Embedded Document Search/Replace Feature +# Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport +# =============================================================================================== # + +class GuiDocEditSearch(QFrame): + + def __init__(self, docEditor): + QFrame.__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 + 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.doMatchCap = self.mainConf.searchMatchCap + + 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(0, 0, 0, 0) + self.setAutoFillBackground(True) + self.setFrameStyle(QFrame.StyledPanel | QFrame.Plain) + + self.mainBox = QGridLayout(self) + self.setLayout(self.mainBox) + + # Text Boxes + # ========== + 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 = 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.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) + + self.toggleWord = QAction("Whole Words Only", self) + 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) + + self.toggleRegEx = QAction("RegEx Mode", self) + 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) + + 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.setChecked(self.doLoop) + self.toggleLoop.toggled.connect(self._doToggleLoop) + self.searchOpt.addAction(self.toggleLoop) + + 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.setChecked(self.doNextFile) + self.toggleProject.toggled.connect(self._doToggleProject) + self.searchOpt.addAction(self.toggleProject) + + self.searchOpt.addSeparator() + + 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() + + 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") + 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.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) + self.mainBox.setColumnStretch(2, 0) + self.mainBox.setColumnStretch(3, 0) + self.mainBox.setColumnStretch(4, 0) + self.mainBox.setSpacing(self.mainConf.pxInt(2)) + self.mainBox.setContentsMargins(mPx, mPx, mPx, mPx) + + boxWidth = self.mainConf.pxInt(200) + self.searchBox.setFixedWidth(boxWidth) + self.replaceBox.setFixedWidth(boxWidth) + self.replaceBox.setVisible(False) + self.replaceButton.setVisible(False) + self.adjustSize() + + # 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 -## END Class WordCounter + 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.doMatchCap + + self.showReplace.setChecked(False) + self.setVisible(False) + self.docEditor.updateDocMargins() + self.docEditor.setFocus() + + 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() + if self.isRegEx: + self._alertSearchValid(True) + logger.verbose("Setting search text to '%s'" % theText) + return True + + def setReplaceText(self, theText): + """Set the replace text. + """ + self.showReplace.setChecked(True) + self.replaceBox.setFocus() + self.replaceBox.setText(theText) + return True + + def getSearchText(self): + """Return the current search text either as text or as a regular + expression object. + """ + 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. + """ + return self.replaceBox.text() + + ## + # Slots + ## + + def _doClose(self): + """Hide the search/replace bar. + """ + self.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 + + 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() + self.docEditor.updateDocMargins() + 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 + + 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 _doToggleMatchCap(self, theState): + """Enable/disable preserving capitalisation when replacing. + """ + self.doMatchCap = theState + return + + ## + # 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 + +# END Class GuiDocEditSearch + +# =============================================================================================== # +# The Embedded Document Header +# Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport +# =============================================================================================== # class GuiDocEditHeader(QWidget): @@ -1252,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/dochighlight.py b/nw/gui/dochighlight.py index afd5a77c..4acc0e7a 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -33,7 +33,7 @@ from PyQt5.QtGui import ( QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush ) -from nw.constants import nwUnicode +from nw.constants import nwUnicode, nwRegEx logger = logging.getLogger(__name__) @@ -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) @@ -98,28 +97,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"), + "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), + "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 = [] @@ -131,32 +130,39 @@ class GuiDocHighlighter(QSyntaxHighlighter): } )) - # Non-breaking Space + # Non-Breaking Spaces self.hRules.append(( - "[%s]+" % nwUnicode.U_NBSP, { + "[%s%s]+" % (nwUnicode.U_NBSP, nwUnicode.U_THNBSP), { 0 : self.hStyles["nobreak"], } )) # Markdown self.hRules.append(( - r"(? 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)) @@ -265,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) @@ -341,53 +359,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,18 +468,70 @@ 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 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") @@ -516,12 +539,26 @@ 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) + + # 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() @@ -540,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 @@ -585,6 +622,68 @@ 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): + + # 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): @@ -696,6 +795,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/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/preferences.py b/nw/gui/preferences.py index 5c0ef235..56905307 100644 --- a/nw/gui/preferences.py +++ b/nw/gui/preferences.py @@ -37,9 +37,9 @@ 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 +from nw.constants import nwQuotes logger = logging.getLogger(__name__) @@ -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/projload.py b/nw/gui/projload.py index 06a32f72..738a493c 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) @@ -62,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() @@ -74,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) @@ -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 diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py index 11b6a014..6f8f32ba 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 @@ -52,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) @@ -73,13 +80,17 @@ class GuiProjectSettings(PagedDialog): self.buttonBox.rejected.connect(self._doClose) self.addControls(self.buttonBox) - self.show() - logger.debug("GuiProjectSettings initialisation complete") return + ## + # Slots + ## + def _doSave(self): + """Save settings and close dialog. + """ logger.verbose("GuiProjectSettings save button clicked") projName = self.tabMain.editName.text() @@ -103,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 @@ -282,7 +303,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() @@ -455,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(): diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py index 02e850ad..7c2bc7a4 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 @@ -555,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 ) @@ -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/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..7df0c560 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) @@ -497,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 = { diff --git a/nw/guimain.py b/nw/guimain.py index 576cd990..cd479c9e 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, GuiSearchBar, GuiSessionLogView, GuiTheme + GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, GuiTheme, + GuiProjectSettings, GuiProjectTree, GuiSessionLogView ) 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) @@ -111,22 +110,13 @@ 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.searchBar) - 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) self.splitView.setSizes(self.mainConf.getViewPanePos()) 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) @@ -144,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()) @@ -153,7 +142,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) @@ -168,7 +157,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) @@ -487,6 +476,37 @@ class GuiMain(QMainWindow): return False return True + 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 # 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: + 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 + def saveDocument(self): """Save the current documents. """ @@ -498,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() @@ -601,15 +631,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 ## @@ -948,12 +976,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) @@ -961,9 +992,20 @@ class GuiMain(QMainWindow): self.addAction(self.mainMenu.aEditPaste) self.addAction(self.mainMenu.aSelectAll) self.addAction(self.mainMenu.aSelectPar) - self.addAction(self.mainMenu.aFmtBold) + + # 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.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) @@ -972,10 +1014,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): @@ -1075,8 +1126,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() 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..394a96ce 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 @@ -7,13 +7,15 @@ 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***. 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. 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/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..4a099dfc 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 @@ -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/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 c3284683..4ce89213 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 298 - 42 - 5113 + 407 + 71 + 15118 False @@ -15,7 +15,7 @@ True 636b6aa9b697b bb2c23b3c42cc - 914 + 967 B E @@ -114,10 +114,10 @@ 1st Draft True SCENE - 1199 - 216 - 7 - 28 + 1483 + 263 + 8 + 1086 Another Scene @@ -129,7 +129,7 @@ 476 93 3 - 551 + 428 Interlude @@ -274,9 +274,9 @@ New True SCENE - 0 - 0 - 0 + 30 + 6 + 1 36 diff --git a/setup.py b/setup.py index c9b89fb2..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.8", + version = "0.9rc1", author = "Veronica Berglyd Olsen", author_email = "code@vkbo.net", description = "A markdown-like document editor for writing novels", 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 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/3_nwProject.nwx b/tests/reference/gui/3_nwProject.nwx index 7ae01352..4d6ef96a 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% @@ -57,9 +57,9 @@ Note False PAGE - 0 - 0 - 0 + 331 + 59 + 2 0 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 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 = diff --git a/tests/test_gui.py b/tests/test_gui.py index cd375cd8..3ad703a5 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": @@ -273,6 +278,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("") @@ -358,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) @@ -374,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") @@ -382,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) @@ -610,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()