Merge branch 'master' into editor_footer

This commit is contained in:
Veronica K. B. Olsen
2020-06-27 18:12:45 +02:00
40 changed files with 1661 additions and 629 deletions
+5 -3
View File
@@ -3,12 +3,14 @@ codecov:
coverage: coverage:
precision: 2 precision: 2
round: down round: up
range: "70...100" range: "70...100"
status: status:
project: yes project:
patch: yes default:
threshold: 1%
patch: no
changes: no changes: no
parsers: parsers:
+50 -5
View File
@@ -1,20 +1,65 @@
# novelWriter ChangeLog # novelWriter ChangeLog
## Version 0.9rc1 [2020-xx-xx] ## Version 0.10.0 RC1 [2020-xx-xx]
Note: If the project file is opened by this version of novelWriter, the project file can no longer be read by an earlier version due to the change of how autoReplace settings are stored.
**User Interface**
* The Session Log dialog has been redesigned and now has a few more filter options. The filters now also update the filtered time count properly. The dialog now shows a histogram of words added in a given session, or optionally, on a given date. The filtered log data can also be saved as a JSON or CSV file, the latter suitable for importing to a spread sheet. The new dialog tool required a new session log file format, so the new session log has been given a new file name. The old log file will be left untouched in the project's `meta` folder. For projects created prior to this change, the log will record a word count offset that will be subtracted from the first entry such that the first word diff will always be 0 instead of the total word count of the entire project. Such a large word diff would otherwise saturate the histogram. PR #339.
**Other Changes**
* The way the auto-replace settings are stored in the project XML file has been changed in order to be more consistent with other features, and to avoid a potential pitfall in defining the tag name from a user-entered string. The project class retains its ability to read the old format of the file, and will save in the new format. PR #344.
## Version 0.9.2 [2020-06-26]
**Bugfixes**
* The project tree word counts were getting mixed up when a file was moved to the trash folder, or permanently deleted. This has now been fixed, and moving a file should give a zero net change of project word count. Permanently deleting it will result in a negative net change. Issue #333, PR #335.
**User Interface**
* There is a feature in the project tree class that ensures that the tree item being acted on is visible in the tree. It is called when you for instance click the header of an open document. It was also activated when opening a document from the tree view with either double-click by mouse, or by using the Enter key. This meant that the tree view would often move, which made it hard to mouse click on items after eachother since you ended up chasing a moving target. This feature is now disabled for document open. In addition, the scroll into view feature has been added to the search/replace call to move into the next document when reaching the end of the current document. This was requested in Issue #332. PR #334.
* The Build Novel Project tool will now display the build time of the document in the preview window in order for the user to know if it is potentially out of date. The timestamp is given, as well as a fuzzy time string, indicating the age of the content. Issue #331, PRs #336 and #337.
**Documentation**
* The documentation has been updated to clarify the correct formatting for italic, bold and strikethrough formatting tags. Issue #220, PR #338.
## Version 0.9.1 [2020-06-21]
**Bugfixes**
* Fixed a serious bug in the replace part of the search/replace function that caused novelWriter to crash when the replace text function was called. Issue #327, PR #328.
## Version 0.9 [2020-06-21]
**Core Functionality** **Core Functionality**
* Underline text formatting has been removed. It is not standard HTML5, not Markdown, and was previously implemented using the double underscore notation that in standard Markdown is renderred as bold text. Instead, novelWriter now renders a single `*` or `-` wrapping a piece of text *within* a paragraphs as italicised text, and a double `**` or `__` as bold text. The keyboard shortcuts and automatic features **only** support the `*` notation. A triple set of `***` are treated as both bold and italicised. PR #310. * Underline text formatting has been removed. It is not standard HTML5, nor 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. * 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 (undo) 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 and let the menu action handle the key sequence. 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** **User Interface**
* The Open Project dialog will now ask before removing an entry from the recent projects list. PR #309. * 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 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 menu entry 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 the next file in the project tree. 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 edit 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, ensuring 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** **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. * Added the file's class and layout to the meta data string 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] ## Version 0.8 [2020-06-14]
@@ -32,7 +77,7 @@
* Icons have been added to the Title and Document columns in the Outline. The titles get a new icon indicating the header level, while the documents get the already existing document icon. PR #302. * Icons have been added to the Title and Document columns in the Outline. The titles get a new icon indicating the header level, while the documents get the already existing document icon. PR #302.
* Added a context menu to the project tree for easier access to some of the most use actions on the tree. PR #282. * Added a context menu to the project tree for easier access to some of the most use actions on the tree. PR #282.
* Improved the support for High DPI screens. Margins and box sizes that were hardcoded should now scale. User settings should also scale back and forth when switching between scale factors. Issue #280, PR #285. * Improved the support for High DPI screens. Margins and box sizes that were hardcoded should now scale. User settings should also scale back and forth when switching between scale factors. Issue #280, PR #285.
* The total edit time of a project is no displayed on the Details tab of the Project Settings dialog. PR #290. * The total edit time of a project is now displayed on the Details tab of the Project Settings dialog. PR #290.
* The title bar in the document editor now has a full screen button and a close button, and in the document viewer a reload button and a close button. The full screen button toggles the distraction free mode, and the reload button regenerates the document being viewed to update any changes that may have been made to it. PRs #293, #300, #303 and #306. * The title bar in the document editor now has a full screen button and a close button, and in the document viewer a reload button and a close button. The full screen button toggles the distraction free mode, and the reload button regenerates the document being viewed to update any changes that may have been made to it. PRs #293, #300, #303 and #306.
* The References panel below the document viewer has been redesigned. It now sits in a resizeable panel below the document, and its controls sit in a footer bar in the document itself. The functionality of the feature is otherwise unchanged, but the buttons have received new icons. PRs #304 and #306. * The References panel below the document viewer has been redesigned. It now sits in a resizeable panel below the document, and its controls sit in a footer bar in the document itself. The functionality of the feature is otherwise unchanged, but the buttons have received new icons. PRs #304 and #306.
* The option to render comments and synopsis in the document view panel has been added to Preferences. The toggle option for comments that was previously in the menu has been removed. PR #311. * The option to render comments and synopsis in the document view panel has been added to Preferences. The toggle option for comments that was previously in the menu has been removed. PR #311.
+2 -2
View File
@@ -24,9 +24,9 @@ copyright = "2018-2020, Veronica Berglyd Olsen"
author = "Veronica Berglyd Olsen" author = "Veronica Berglyd Olsen"
# The short X.Y version # The short X.Y version
version = "0.9" version = "0.10.0"
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = "0.9.0rc1" release = "0.10.0rc1"
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
+22 -3
View File
@@ -58,10 +58,16 @@ The editor also has a minimal set of keywords used for setting tags and referenc
"``% Synopsis: text...``", "A synopsis comment. Shows up in the Synopsis column of the Outline View, but is otherwise treated as a comment." "``% 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." "``@keyword: value``", "A keyword argument followed by a value, or a comma separated list of values."
.. note::
The bold/italic/strikethrough formatting tags do not allow spaces between the words and the tag itself.
That is, ``**text**`` is valid, ``**text **`` is not.
The editor and viewer also supports markdown standard hard line breaks, and preserves non-breaking spaces. The editor and viewer also supports markdown standard hard line breaks, and preserves non-breaking spaces.
A hard line break is achieved by leaving two or more spaces at the end of the line. A hard line break is achieved by leaving two or more spaces at the end of the line.
Alternatively, the user can press :kbd:`Shift-Enter` to insert this. Alternatively, the user can press :kbd:`Ctrl-K, Return` to insert this.
A non-breaking space is inserted with :kbd:`Shift-Space`. A non-breaking space is inserted with :kbd:`Ctrl-K, Space`.
Thin spaces are also supported, and can be inserted with :kbd:`Ctrl-K, Shift-Space`, and the non-breaking version of it with :kbd:`Ctrl-K, Ctrl-Space`.
Both hard line breaks and non-breaking spaces are highlighted by the syntax highlighter as an alternate coloured background, depending on the selected theme. Both hard line breaks and non-breaking spaces are highlighted by the syntax highlighter as an alternate coloured background, depending on the selected theme.
@@ -133,7 +139,6 @@ These are as following:
":kbd:`Ctrl-Q`", "Exit novelWriter." ":kbd:`Ctrl-Q`", "Exit novelWriter."
":kbd:`Ctrl-R`", "If in tree view, open a document for viewing. If editor pane has focus, open current document for viewing." ":kbd:`Ctrl-R`", "If in tree view, open a document for viewing. If editor pane has focus, open current document for viewing."
":kbd:`Ctrl-S`", "Save the current document in the editor." ":kbd:`Ctrl-S`", "Save the current document in the editor."
":kbd:`Ctrl-U`", "Format selected text, or word under cursor, as underline."
":kbd:`Ctrl-V`", "Paste text from clipboard to cursor position." ":kbd:`Ctrl-V`", "Paste text from clipboard to cursor position."
":kbd:`Ctrl-W`", "Close the current document in the editor." ":kbd:`Ctrl-W`", "Close the current document in the editor."
":kbd:`Ctrl-X`", "Cut selected text to clipboard." ":kbd:`Ctrl-X`", "Cut selected text to clipboard."
@@ -174,3 +179,17 @@ These are as following:
.. note:: .. note::
On macOS, replace :kbd:`Ctrl` with :kbd:`Cmd`. On macOS, replace :kbd:`Ctrl` with :kbd:`Cmd`.
A set of insert features are also available through shortcuts, but they require a double combination of shortcuts.
.. csv-table:: Keyboard Shortcuts
:header: "Shortcut", "Description"
:widths: 30, 50
":kbd:`Ctrl-K, -`", "Insert a short dash (en dash)."
":kbd:`Ctrl-K, _`", "Insert a long dash (em dash)."
":kbd:`Ctrl-K, .`", "Insert ellipsis."
":kbd:`Ctrl-K, Return`", "Insert a hard line break."
":kbd:`Ctrl-K, Space`", "Insert a non-breaking space."
":kbd:`Ctrl-K, Shift-Space`", "Insert a thin space."
":kbd:`Ctrl-K, Ctrl-Space`", "Insert a thin non-breaking space."
-186
View File
@@ -1,186 +0,0 @@
# -*- coding: utf-8 -*-
"""
novelWriter Text Analysis Class
===================================
Class for analysing bits of text.
File History:
Created: 2018-09-22 [0.0.1]
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 <https://www.gnu.org/licenses/>.
"""
import logging
import nw
from time import time
logger = logging.getLogger(__name__)
class TextAnalysis():
def __init__(self, theText, langCode):
self.theText = theText
self.langCode = langCode
return
def getStats(self):
tStart = time()
wordCount = self._countWords()
tEnd = time()-tStart
logger.verbose("Words: %7d in %8.3f ms" % (wordCount,tEnd*1e3))
tStart = time()
sentCount = self._countSentences()
tEnd = time()-tStart
logger.verbose("Sentences: %7d in %8.3f ms" % (sentCount,tEnd*1e3))
tStart = time()
paraCount = self._countParagraphs()
tEnd = time()-tStart
logger.verbose("Paragraphs: %7d in %8.3f ms" % (paraCount,tEnd*1e3))
return wordCount, sentCount, paraCount
def getReadabilityScore(self):
"""Calculate Flesch--Kincaid Readability Score.
"""
tStart = time()
wordCount = self._countWords()
sentCount = self._countSentences()
if self.langCode[:3] == "en_":
ratSyllWord = self._countSyllablesEN()
else:
ratSyllWord = -1.0
rScore = 206.835 - 1.015*(wordCount/sentCount) - 84.6*(ratSyllWord)
gLevel = -15.59 + 0.390*(wordCount/sentCount) + 11.8*(ratSyllWord)
tEnd = time()-tStart
logger.verbose("Readability: %7.3f in %8.3f ms" % (rScore,tEnd*1e3))
logger.verbose("Grade Level: %7.3f in %8.3f ms" % (gLevel,tEnd*1e3))
logger.verbose("Assessment: %s" % self.getReadabilityText(rScore))
return rScore, gLevel
def getReadabilityText(self, rScore):
if rScore >= 90.0:
return "Very Easy"
elif rScore >= 80.0:
return "Easy"
elif rScore >= 70.0:
return "Fairly Easy"
elif rScore >= 60.0:
return "Average"
elif rScore >= 50.0:
return "Fairly Difficult"
elif rScore >= 30.0:
return "Difficult"
else:
return "Very Difficult"
#
# Internal Functions
#
def _countWords(self):
"""Counts the number of words in a text by simply splitting on
all white spaces.
"""
return len(self.theText.strip().split())
def _countSentences(self):
"""Counts the number of non-repeated sentence endings seen in
the text. Note: This will count filenames and urls as multiple
sentences.
"""
nSent = 0
sawEnd = False
for ch in self.theText.strip():
if ch in ".!?":
if not sawEnd:
sawEnd = True
nSent += 1
else:
sawEnd = False
return nSent
def _countParagraphs(self, pThreshold=2):
"""Counts the number of paragraphs by counting repeated line
breaks.
"""
nPara = 1
sawEnd = 0
for ch in self.theText.strip():
if ch == "\r": # Ignore Windows line end chars
continue
if ch == "\n": # Count endlines
sawEnd += 1
else: # If non-endline is encountered, check condition for paragraph
if sawEnd >= pThreshold:
nPara += 1
sawEnd = 0
return nPara
def _countSyllablesEN(self):
"""Attempt to count the syllables in a piece of English language
text. This function tends to slightly over-estimate the number
of syllables as it doesn't handle the complexity of silent
vowels in endings very well. It will count them all.
"""
cleanText = ""
for ch in self.theText:
if ch in "abcdefghijklmnopqrstuvwxyz'":
cleanText += ch
else:
cleanText += " "
asVow = "aeiouy'"
dExept = ("ei","ie","ua","ia","eo")
theWords = cleanText.lower().split()
allSylls = 0
for inWord in theWords:
nChar = len(inWord)
nSyll = 0
wasVow = False
wasY = False
if nChar == 0:
continue
if inWord[0] in asVow:
nSyll += 1
wasVow = True
wasY = inWord[0] == "y"
for c in range(1,nChar):
isVow = False
if inWord[c] in asVow:
nSyll += 1
isVow = True
if isVow and wasVow:
nSyll -= 1
if isVow and wasY:
nSyll -= 1
if inWord[c:c+2] in dExept:
nSyll += 1
wasVow = isVow
wasY = inWord[c] == "y"
if inWord.endswith(("e")):
nSyll -= 1
if inWord.endswith(("le","ea","io")):
nSyll += 1
if nSyll < 1:
nSyll = 1
allSylls += nSyll
return allSylls/len(theWords)
# END Class TextAnalysis
+4 -3
View File
@@ -40,13 +40,14 @@ __package__ = "novelWriter"
__author__ = "Veronica Berglyd Olsen" __author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 20182020, Veronica Berglyd Olsen" __copyright__ = "Copyright 20182020, Veronica Berglyd Olsen"
__license__ = "GPLv3" __license__ = "GPLv3"
__version__ = "0.9.0rc1" __version__ = "0.10.0rc1"
__hexversion__ = "0x000900c1" __hexversion__ = "0x001000c1"
__date__ = "2020-06-14" __date__ = "2020-06-26"
__maintainer__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net" __email__ = "code@vkbo.net"
__status__ = "Pre-Release" __status__ = "Pre-Release"
__url__ = "https://github.com/vkbo/novelWriter" __url__ = "https://github.com/vkbo/novelWriter"
__issuesurl__ = "https://github.com/vkbo/novelWriter/issues"
__domain__ = "novelwriter.io" __domain__ = "novelwriter.io"
__docurl__ = "https://novelwriter.readthedocs.io" __docurl__ = "https://novelwriter.readthedocs.io"
__credits__ = [ __credits__ = [
+34
View File
@@ -193,3 +193,37 @@ def transferCase(theSource, theTarget):
theResult = theTarget.lower() theResult = theTarget.lower()
return theResult return theResult
def fuzzyTime(secDiff):
"""Converts a time difference in seconds into a fuzzy time string.
"""
if secDiff < 0:
return "in the future"
elif secDiff < 30:
return "just now"
elif secDiff < 90:
return "a minute ago"
elif secDiff < 3300: # 55 minutes
return "%d minutes ago" % int(round(secDiff/60))
elif secDiff < 5400: # 90 minutes
return "an hour ago"
elif secDiff < 84600: # 23.5 hours
return "%d hours ago" % int(round(secDiff/3600))
elif secDiff < 129600: # 1.5 days
return "a day ago"
elif secDiff < 561600: # 6.5 days
return "%d days ago" % int(round(secDiff/86400))
elif secDiff < 907200: # 10.5 days
return "a week ago"
elif secDiff < 2419200: # 28 days
return "%d weeks ago" % int(round(secDiff/604800))
elif secDiff < 3888000: # 45 days
return "a month ago"
elif secDiff < 31104000: # 360 days
return "%d months ago" % int(round(secDiff/2592000))
elif secDiff < 47304000: # 1.5 years
return "a year ago"
else:
return "%d years ago" % int(round(secDiff/31557600))
return "beyond time and space"
+6 -2
View File
@@ -1,10 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from nw.constants.iso import isoLanguage, isoCountry from nw.constants.iso import isoLanguage, isoCountry
from nw.constants.constants import ( from nw.constants.constants import (
nwConst, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode nwConst, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode,
nwInsertSymbols
) )
from nw.constants.enum import ( from nw.constants.enum import (
nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline,
nwDocInsert
) )
__all__ = [ __all__ = [
@@ -17,10 +19,12 @@ __all__ = [
"nwLabels", "nwLabels",
"nwQuotes", "nwQuotes",
"nwUnicode", "nwUnicode",
"nwInsertSymbols",
"nwAlert", "nwAlert",
"nwDocAction", "nwDocAction",
"nwItemClass", "nwItemClass",
"nwItemLayout", "nwItemLayout",
"nwItemType", "nwItemType",
"nwOutline", "nwOutline",
"nwDocInsert",
] ]
+55 -26
View File
@@ -25,12 +25,13 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from nw.constants.enum import nwItemClass, nwItemLayout, nwOutline from nw.constants.enum import nwItemClass, nwItemLayout, nwOutline, nwDocInsert
class nwConst(): class nwConst():
tStampFmt = "%Y-%m-%d %H:%M:%S" # Default format tStampFmt = "%Y-%m-%d %H:%M:%S" # Default format
fStampFmt = "%Y-%m-%d %H.%M.%S" # FileName safe format fStampFmt = "%Y-%m-%d %H.%M.%S" # FileName safe format
dStampFmt = "%Y-%m-%d" # Date only format
# END Class nwConst # END Class nwConst
@@ -50,7 +51,7 @@ class nwFiles():
PROJ_LOCK = "nwProject.lock" PROJ_LOCK = "nwProject.lock"
TOC_TXT = "ToC.txt" TOC_TXT = "ToC.txt"
TOC_JSON = "ToC.json" TOC_JSON = "ToC.json"
SESS_INFO = "sessionInfo.log" SESS_STATS = "sessionStats.log"
INDEX_FILE = "tagsIndex.json" INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json" OPTS_FILE = "guiOptions.json"
RECENT_FILE = "recentProjects.json" RECENT_FILE = "recentProjects.json"
@@ -169,32 +170,35 @@ class nwQuotes():
Source: https://en.wikipedia.org/wiki/Quotation_mark Source: https://en.wikipedia.org/wiki/Quotation_mark
""" """
SYMBOLS = [ SYMBOLS = {
"\u0022", # Quotation mark "\u0027" : "Straight single quotation mark",
"\u0027", # Apostrophe "\u0022" : "Straight double quotation mark",
"\u00ab", # Left-pointing double angle quotation mark
"\u00bb", # Right-pointing double angle quotation mark "\u2018" : "Left single quotation mark",
"\u2018", # Left single quotation mark "\u2019" : "Right single quotation mark",
"\u2019", # Right single quotation mark "\u201a" : "Single low-9 quotation mark",
"\u201a", # Single low-9 quotation mark "\u201b" : "Single high-reversed-9 quotation mark",
"\u201b", # Single high-reversed-9 quotation mark "\u201c" : "Left double quotation mark",
"\u201c", # Left double quotation mark "\u201d" : "Right double quotation mark",
"\u201d", # Right double quotation mark "\u201e" : "Double low-9 quotation mark",
"\u201e", # Double low-9 quotation mark "\u201f" : "Double high-reversed-9 quotation mark",
"\u201f", # Double high-reversed-9 quotation mark "\u2e42" : "Double low-reversed-9 quotation mark",
"\u2039", # Single left-pointing angle quotation mark
"\u203a", # Single right-pointing angle quotation mark "\u2039" : "Single left-pointing angle quotation mark",
"\u2e42", # Double low-reversed-9 quotation mark "\u203a" : "Single right-pointing angle quotation mark",
"\u300c", # Left corner bracket "\u00ab" : "Left-pointing double angle quotation mark",
"\u300d", # Right corner bracket "\u00bb" : "Right-pointing double angle quotation mark",
"\u300e", # Left white corner bracket
"\u300f", # Right white corner bracket "\u300c" : "Left corner bracket",
] "\u300d" : "Right corner bracket",
"\u300e" : "Left white corner bracket",
"\u300f" : "Right white corner bracket",
}
# END Class nwQuotes # END Class nwQuotes
class nwUnicode: class nwUnicode:
"""Suppoted unicode character constants and translation maps for HTML. """Supported unicode character constants and translation maps for HTML.
""" """
# Unicode Constants # Unicode Constants
@@ -226,8 +230,14 @@ class nwUnicode:
U_EMDASH = "\u2014" # Long dash U_EMDASH = "\u2014" # Long dash
U_HELLIP = "\u2026" # Ellipsis U_HELLIP = "\u2026" # Ellipsis
## Other ## Spaces and Lines
U_NBSP = "\u00a0" # Non-breaking space U_NBSP = "\u00a0" # Non-breaking space
U_THNSP = "\u2009" # Thin space
U_THNBSP = "\u202f" # Thin non-breaking space
U_LSEP = "\u2028" # Line separator
U_PSEP = "\u2029" # Paragraph separator
## Symbols
U_CHECK = "\u2714" # Heavy check mark U_CHECK = "\u2714" # Heavy check mark
U_MULT = "\u2715" # Multiplication x U_MULT = "\u2715" # Multiplication x
@@ -270,8 +280,12 @@ class nwUnicode:
H_EMDASH = "&mdash;" H_EMDASH = "&mdash;"
H_HELLIP = "&hellip;" H_HELLIP = "&hellip;"
## Other ## Spaces
H_NBSP = "&nbsp;" H_NBSP = "&nbsp;"
H_THNSP = "&thinsp;"
H_THNBSP = "&#8239;"
## Symbols
H_CHECK = "&#10004;" H_CHECK = "&#10004;"
H_MULT = "&#10005;" H_MULT = "&#10005;"
@@ -286,3 +300,18 @@ class nwUnicode:
H_LTRIS = "&#9666;" H_LTRIS = "&#9666;"
# END Class nwUnicode # END Class nwUnicode
class nwInsertSymbols():
SYMBOLS = {
nwDocInsert.NO_INSERT : "",
nwDocInsert.HARD_BREAK : " \n",
nwDocInsert.NB_SPACE : nwUnicode.U_NBSP,
nwDocInsert.THIN_SPACE : nwUnicode.U_THNSP,
nwDocInsert.THIN_NB_SPACE : nwUnicode.U_THNBSP,
nwDocInsert.SHORT_DASH : nwUnicode.U_ENDASH,
nwDocInsert.LONG_DASH : nwUnicode.U_EMDASH,
nwDocInsert.ELLIPSIS : nwUnicode.U_HELLIP,
}
# END Enum nwDocInsert
+15
View File
@@ -93,9 +93,24 @@ class nwDocAction(Enum):
BLOCK_H4 = 22 BLOCK_H4 = 22
BLOCK_COM = 23 BLOCK_COM = 23
BLOCK_TXT = 24 BLOCK_TXT = 24
REPL_SNG = 25
REPL_DBL = 26
# END Enum nwDocAction # END Enum nwDocAction
class nwDocInsert(Enum):
NO_INSERT = 0
HARD_BREAK = 1
NB_SPACE = 2
THIN_SPACE = 3
THIN_NB_SPACE = 4
SHORT_DASH = 5
LONG_DASH = 6
ELLIPSIS = 7
# END Enum nwDocInsert
class nwAlert(Enum): class nwAlert(Enum):
INFO = 0 INFO = 0
+11 -1
View File
@@ -45,14 +45,19 @@ class OptionState():
self.theState = {} self.theState = {}
self.validMap = { self.validMap = {
"GuiSession": { "GuiSessionLog": {
"winWidth",
"winHeight",
"widthCol0", "widthCol0",
"widthCol1", "widthCol1",
"widthCol2", "widthCol2",
"sortCol", "sortCol",
"sortOrder", "sortOrder",
"incNovel",
"incNotes",
"hideZeros", "hideZeros",
"hideNegative", "hideNegative",
"groupByDay",
}, },
"GuiDocSplit": { "GuiDocSplit": {
"spLevel", "spLevel",
@@ -78,6 +83,11 @@ class OptionState():
"columnWidth", "columnWidth",
"columnHidden", "columnHidden",
}, },
"GuiProjectSettings": {
"winWidth",
"winHeight",
"replaceColW",
}
} }
return return
+69 -24
View File
@@ -93,6 +93,8 @@ class NWProject():
self.lastViewed = None # The handle of the last file to be viewed self.lastViewed = None # The handle of the last file to be viewed
self.lastWCount = 0 # The project word count from last session self.lastWCount = 0 # The project word count from last session
self.currWCount = 0 # The project word count in current session self.currWCount = 0 # The project word count in current session
self.novelWCount = 0 # Total number of words in novel files
self.notesWCount = 0 # Total number of words in note files
self.doBackup = True # Run project backup on exit self.doBackup = True # Run project backup on exit
# Set Defaults # Set Defaults
@@ -231,6 +233,8 @@ class NWProject():
self.lastViewed = None self.lastViewed = None
self.lastWCount = 0 self.lastWCount = 0
self.currWCount = 0 self.currWCount = 0
self.novelWCount = 0
self.notesWCount = 0
return return
@@ -352,6 +356,15 @@ class NWProject():
# Check Project Storage Version # Check Project Storage Version
# ============================= # =============================
# Changes:
# 1.0 : Original file format.
# 1.1 : Changes the way documents are structure in the project
# folder from data_X, where X is the first hex value of
# the handle, to a single content folder.
# 1.2 : Changes the way autoReplace entries are stored. The 1.1
# parser will lose the autoReplace settings if allowed to
# read the file. Introduced in version 0.10.
if fileVersion == "1.0": if fileVersion == "1.0":
msgBox = QMessageBox() msgBox = QMessageBox()
msgRes = msgBox.question(self.theParent, "Old Project Version", ( msgRes = msgBox.question(self.theParent, "Old Project Version", (
@@ -365,12 +378,14 @@ class NWProject():
if msgRes != QMessageBox.Yes: if msgRes != QMessageBox.Yes:
return False return False
elif fileVersion != "1.1": elif fileVersion != "1.1" and fileVersion != "1.2":
self.makeAlert(( self.makeAlert((
"Unknown or unsupported %s project format. " "Unknown or unsupported {nw:s} project file format. "
"The project cannot be opened by this version of %s." "The project cannot be opened by this version of {nw:s}. "
) % ( "The file was saved with {nw:s} version {vers:s}."
nw.__package__, nw.__package__ ).format(
nw = nw.__package__,
vers = appVersion,
), nwAlert.ERROR) ), nwAlert.ERROR)
return False return False
@@ -435,13 +450,25 @@ class NWProject():
self.lastViewed = checkString(xItem.text, None, True) self.lastViewed = checkString(xItem.text, None, True)
elif xItem.tag == "lastWordCount": elif xItem.tag == "lastWordCount":
self.lastWCount = checkInt(xItem.text, 0, False) self.lastWCount = checkInt(xItem.text, 0, False)
elif xItem.tag == "novelWordCount":
self.novelWCount = checkInt(xItem.text, 0, False)
elif xItem.tag == "notesWordCount":
self.notesWCount = checkInt(xItem.text, 0, False)
elif xItem.tag == "status": elif xItem.tag == "status":
self.statusItems.unpackEntries(xItem) self.statusItems.unpackEntries(xItem)
elif xItem.tag == "importance": elif xItem.tag == "importance":
self.importItems.unpackEntries(xItem) self.importItems.unpackEntries(xItem)
elif xItem.tag == "autoReplace": elif xItem.tag == "autoReplace":
for xEntry in xItem: for xEntry in xItem:
self.autoReplace[xEntry.tag] = checkString(xEntry.text, None, False) if xEntry.tag == "entry":
if "key" in xEntry.attrib:
self.autoReplace[xEntry.attrib["key"]] = checkString(
xEntry.text, None, False
)
else: # Old format
self.autoReplace[xEntry.tag] = checkString(
xEntry.text, None, False
)
elif xItem.tag == "titleFormat": elif xItem.tag == "titleFormat":
titleFormat = self.titleFormat.copy() titleFormat = self.titleFormat.copy()
for xEntry in xItem: for xEntry in xItem:
@@ -493,14 +520,18 @@ class NWProject():
# Root element and project details # Root element and project details
logger.debug("Writing project meta") logger.debug("Writing project meta")
nwXML = etree.Element("novelWriterXML",attrib={ nwXML = etree.Element("novelWriterXML", attrib={
"appVersion" : str(nw.__version__), "appVersion" : str(nw.__version__),
"hexVersion" : str(nw.__hexversion__), "hexVersion" : str(nw.__hexversion__),
"fileVersion" : "1.1", "fileVersion" : "1.2",
"timeStamp" : formatTimeStamp(saveTime), "timeStamp" : formatTimeStamp(saveTime),
}) })
editTime = int(self.editTime + saveTime - self.projOpened) editTime = int(self.editTime + saveTime - self.projOpened)
wcNovel, wcNotes = self.projTree.sumWords()
self.novelWCount = wcNovel
self.notesWCount = wcNotes
self.setProjectWordCount(wcNovel + wcNotes)
# Save Project Meta # Save Project Meta
xProject = etree.SubElement(nwXML, "project") xProject = etree.SubElement(nwXML, "project")
@@ -519,11 +550,9 @@ class NWProject():
self._packProjectValue(xSettings, "lastEdited", self.lastEdited) self._packProjectValue(xSettings, "lastEdited", self.lastEdited)
self._packProjectValue(xSettings, "lastViewed", self.lastViewed) self._packProjectValue(xSettings, "lastViewed", self.lastViewed)
self._packProjectValue(xSettings, "lastWordCount", self.currWCount) self._packProjectValue(xSettings, "lastWordCount", self.currWCount)
self._packProjectValue(xSettings, "novelWordCount", wcNovel)
xAutoRep = etree.SubElement(xSettings, "autoReplace") self._packProjectValue(xSettings, "notesWordCount", wcNotes)
for aKey, aValue in self.autoReplace.items(): self._packProjectKeyValue(xSettings, "autoReplace", self.autoReplace)
if len(aKey) > 0:
self._packProjectValue(xAutoRep, aKey, aValue)
xTitleFmt = etree.SubElement(xSettings, "titleFormat") xTitleFmt = etree.SubElement(xSettings, "titleFormat")
for aKey, aValue in self.titleFormat.items(): for aKey, aValue in self.titleFormat.items():
@@ -1027,10 +1056,20 @@ class NWProject():
if not isinstance(aValue, str): if not isinstance(aValue, str):
aValue = str(aValue) aValue = str(aValue)
if aValue == "" and not allowNone: continue if aValue == "" and not allowNone: continue
xItem = etree.SubElement(xParent,theName) xItem = etree.SubElement(xParent, theName)
xItem.text = aValue xItem.text = aValue
return return
def _packProjectKeyValue(self, xParent, theName, theDict):
"""Pack the entries in the auto-replace dictionary.
"""
xAutoRep = etree.SubElement(xParent, theName)
for aKey, aValue in theDict.items():
if len(aKey) > 0:
xEntry = etree.SubElement(xAutoRep, "entry", attrib={"key": aKey})
xEntry.text = aValue
return
def _scanProjectFolder(self): def _scanProjectFolder(self):
"""Scan the project folder and check that the files in it are """Scan the project folder and check that the files in it are
also in the project XML file. If they aren't, import them as also in the project XML file. If they aren't, import them as
@@ -1101,18 +1140,24 @@ class NWProject():
if not self.ensureFolderStructure(): if not self.ensureFolderStructure():
return False return False
sessionFile = path.join(self.projMeta, nwFiles.SESS_INFO) sessionFile = path.join(self.projMeta, nwFiles.SESS_STATS)
isFile = path.isfile(sessionFile)
with open(sessionFile, mode="a+", encoding="utf8") as outFile: with open(sessionFile, mode="a+", encoding="utf8") as outFile:
print(( if not isFile:
"Start: {opened:s} " # It's a new file, so add a header
"End: {closed:s} " if self.lastWCount > 0:
"Words: {words:8d}" outFile.write("# Offset %d\n" % self.lastWCount)
).format( outFile.write("# %-17s %-19s %8s %8s\n" % (
opened = formatTimeStamp(self.projOpened), "Start Time", "End Time", "Novel", "Notes"
closed = formatTimeStamp(time()), ))
words = self.getSessionWordCount(),
), file=outFile) outFile.write("%-19s %-19s %8d %8d\n" % (
formatTimeStamp(self.projOpened),
formatTimeStamp(time()),
self.novelWCount,
self.notesWCount,
))
return True return True
+2
View File
@@ -54,6 +54,8 @@ class ToHtml(Tokenizer):
nwUnicode.U_EMDASH : nwUnicode.H_EMDASH, nwUnicode.U_EMDASH : nwUnicode.H_EMDASH,
nwUnicode.U_HELLIP : nwUnicode.H_HELLIP, nwUnicode.U_HELLIP : nwUnicode.H_HELLIP,
nwUnicode.U_NBSP : nwUnicode.H_NBSP, nwUnicode.U_NBSP : nwUnicode.H_NBSP,
nwUnicode.U_THNSP : nwUnicode.H_THNSP,
nwUnicode.U_THNBSP : nwUnicode.H_THNBSP,
} }
self.revDict = {} self.revDict = {}
self.reReplace = [] self.reReplace = []
+18 -1
View File
@@ -36,7 +36,7 @@ from time import time
from nw.core.item import NWItem from nw.core.item import NWItem
from nw.common import checkString from nw.common import checkString
from nw.constants import nwFiles, nwItemType, nwItemClass from nw.constants import nwFiles, nwItemType, nwItemClass, nwItemLayout
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -182,6 +182,23 @@ class NWTree():
return return
def sumWords(self):
"""Loops over all entries and adds up the word counts.
"""
noteWords = 0
novelWords = 0
for tHandle in self._treeOrder:
tItem = self.__getitem__(tHandle)
if tItem is None:
continue
if tItem.itemLayout == nwItemLayout.NO_LAYOUT:
pass
elif tItem.itemLayout == nwItemLayout.NOTE:
noteWords += tItem.wordCount
else:
novelWords += tItem.wordCount
return novelWords, noteWords
## ##
# Tree Structure Methods # Tree Structure Methods
## ##
+2 -2
View File
@@ -15,7 +15,7 @@ from nw.gui.preferences import GuiPreferences
from nw.gui.projload import GuiProjectLoad from nw.gui.projload import GuiProjectLoad
from nw.gui.projsettings import GuiProjectSettings from nw.gui.projsettings import GuiProjectSettings
from nw.gui.projtree import GuiProjectTree from nw.gui.projtree import GuiProjectTree
from nw.gui.sessionlog import GuiSessionLogView from nw.gui.sessionlog import GuiSessionLog
from nw.gui.statusbar import GuiMainStatus from nw.gui.statusbar import GuiMainStatus
from nw.gui.theme import GuiIcons, GuiTheme from nw.gui.theme import GuiIcons, GuiTheme
@@ -36,7 +36,7 @@ __all__ = [
"GuiProjectLoad", "GuiProjectLoad",
"GuiProjectSettings", "GuiProjectSettings",
"GuiProjectTree", "GuiProjectTree",
"GuiSessionLogView", "GuiSessionLog",
"GuiMainStatus", "GuiMainStatus",
"GuiIcons", "GuiIcons",
"GuiTheme", "GuiTheme",
+95 -12
View File
@@ -31,8 +31,9 @@ import nw
from os import path from os import path
from time import time from time import time
from datetime import datetime
from PyQt5.QtCore import Qt, QByteArray from PyQt5.QtCore import Qt, QByteArray, QTimer
from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog from PyQt5.QtPrintSupport import QPrinter, QPrintPreviewDialog
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QPalette, QColor, QTextDocumentWriter, QFont QPalette, QColor, QTextDocumentWriter, QFont
@@ -43,6 +44,7 @@ from PyQt5.QtWidgets import (
QFileDialog, QFontDialog, QSpinBox QFileDialog, QFontDialog, QSpinBox
) )
from nw.common import fuzzyTime
from nw.gui.custom import QSwitch from nw.gui.custom import QSwitch
from nw.core import ToHtml from nw.core import ToHtml
from nw.constants import ( from nw.constants import (
@@ -76,6 +78,7 @@ class GuiBuildNovel(QDialog):
self.htmlText = [] # List of html document self.htmlText = [] # List of html document
self.htmlStyle = [] # List of html styles self.htmlStyle = [] # List of html styles
self.nwdText = [] # List of markdown documents self.nwdText = [] # List of markdown documents
self.buildTime = 0 # The timestamp of the last build
self.setWindowTitle("Build Novel Project") self.setWindowTitle("Build Novel Project")
self.setMinimumWidth(self.mainConf.pxInt(900)) self.setMinimumWidth(self.mainConf.pxInt(900))
@@ -411,11 +414,12 @@ class GuiBuildNovel(QDialog):
self.docView.clearStyleSheet() self.docView.clearStyleSheet()
else: else:
self.docView.setStyleSheet(self.htmlStyle) self.docView.setStyleSheet(self.htmlStyle)
self.docView.setContent(self.htmlText) self.docView.setContent(self.htmlText, self.buildTime)
else: else:
self.htmlText = [] self.htmlText = []
self.htmlStyle = [] self.htmlStyle = []
self.nwdText = [] self.nwdText = []
self.buildTime = 0
return return
@@ -509,6 +513,7 @@ class GuiBuildNovel(QDialog):
tEnd = time() tEnd = time()
logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart))) logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart)))
self.htmlStyle = makeHtml.getStyleSheet() self.htmlStyle = makeHtml.getStyleSheet()
self.buildTime = tEnd
# Load the preview document with the html data # Load the preview document with the html data
self.docView.setTextFont(textFont, textSize) self.docView.setTextFont(textFont, textSize)
@@ -517,7 +522,7 @@ class GuiBuildNovel(QDialog):
self.docView.clearStyleSheet() self.docView.clearStyleSheet()
else: else:
self.docView.setStyleSheet(self.htmlStyle) self.docView.setStyleSheet(self.htmlStyle)
self.docView.setContent(self.htmlText) self.docView.setContent(self.htmlText, self.buildTime)
self._saveCache() self._saveCache()
@@ -563,9 +568,6 @@ class GuiBuildNovel(QDialog):
def _saveDocument(self, theFormat): def _saveDocument(self, theFormat):
"""Save the document to various formats. """Save the document to various formats.
""" """
# FMT_PDF
byteFmt = QByteArray() byteFmt = QByteArray()
fileExt = "" fileExt = ""
textFmt = "" textFmt = ""
@@ -694,6 +696,7 @@ class GuiBuildNovel(QDialog):
"workingTitle" : self.theProject.projName, "workingTitle" : self.theProject.projName,
"novelTitle" : self.theProject.bookTitle, "novelTitle" : self.theProject.bookTitle,
"authors" : self.theProject.bookAuthors, "authors" : self.theProject.bookAuthors,
"buildTime" : self.buildTime,
} }
} }
@@ -808,6 +811,8 @@ class GuiBuildNovel(QDialog):
if "nwdText" in theData.keys(): if "nwdText" in theData.keys():
self.nwdText = theData["nwdText"] self.nwdText = theData["nwdText"]
dataCount += 1 dataCount += 1
if "buildTime" in theData.keys():
self.buildTime = theData["buildTime"]
return dataCount == 3 return dataCount == 3
@@ -828,6 +833,7 @@ class GuiBuildNovel(QDialog):
"htmlText" : self.htmlText, "htmlText" : self.htmlText,
"htmlStyle" : self.htmlStyle, "htmlStyle" : self.htmlStyle,
"nwdText" : self.nwdText, "nwdText" : self.nwdText,
"buildTime" : self.buildTime,
}, indent=nIndent)) }, indent=nIndent))
except Exception as e: except Exception as e:
logger.error("Failed to save build cache") logger.error("Failed to save build cache")
@@ -871,8 +877,8 @@ class GuiBuildNovel(QDialog):
"section" : self.fmtSection.text().strip(), "section" : self.fmtSection.text().strip(),
}) })
winWidth = self.mainConf.pxInt(self.width()) winWidth = self.mainConf.rpxInt(self.width())
winHeight = self.mainConf.pxInt(self.height()) winHeight = self.mainConf.rpxInt(self.height())
justifyText = self.justifyText.isChecked() justifyText = self.justifyText.isChecked()
noStyling = self.noStyling.isChecked() noStyling = self.noStyling.isChecked()
textFont = self.textFont.text() textFont = self.textFont.text()
@@ -924,12 +930,14 @@ class GuiBuildNovelDocView(QTextBrowser):
self.mainConf = nw.CONFIG self.mainConf = nw.CONFIG
self.theProject = theProject self.theProject = theProject
self.theParent = theParent self.theParent = theParent
self.theTheme = theParent.theTheme
self.buildTime = 0
self.setMinimumWidth(40*self.theParent.theTheme.textNWidth) self.setMinimumWidth(40*self.theParent.theTheme.textNWidth)
self.setOpenExternalLinks(False) self.setOpenExternalLinks(False)
self.qDocument = self.document() self.qDocument = self.document()
self.qDocument.setDocumentMargin(self.mainConf.textMargin) self.qDocument.setDocumentMargin(self.mainConf.getTextMargin())
self.setPlaceholderText( self.setPlaceholderText(
"This area will show the content of the document to be " "This area will show the content of the document to be "
"exported or printed. Press the \"Build Novel Project\" " "exported or printed. Press the \"Build Novel Project\" "
@@ -946,12 +954,35 @@ class GuiBuildNovelDocView(QTextBrowser):
docPalette = self.palette() docPalette = self.palette()
docPalette.setColor(QPalette.Base, QColor(255, 255, 255)) docPalette.setColor(QPalette.Base, QColor(255, 255, 255))
docPalette.setColor(QPalette.Text, QColor( 0, 0, 0)) docPalette.setColor(QPalette.Text, QColor(0, 0, 0))
self.setPalette(docPalette) self.setPalette(docPalette)
lblPalette = self.palette()
lblPalette.setColor(QPalette.Background, lblPalette.toolTipBase().color())
lblPalette.setColor(QPalette.Foreground, lblPalette.toolTipText().color())
lblFont = self.font()
lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize)
fPx = int(1.1*self.theTheme.fontPixelSize)
mPx = self.mainConf.pxInt(4)
self.theTitle = QLabel("<b>Build Time:</b> Unknown", self)
self.theTitle.setIndent(0)
self.theTitle.setAutoFillBackground(True)
self.theTitle.setAlignment(Qt.AlignCenter)
self.theTitle.setFixedHeight(fPx)
self.theTitle.setPalette(lblPalette)
self.theTitle.setFont(lblFont)
self._updateDocMargins()
self.setStyleSheet() self.setStyleSheet()
self.show() # Age Timer
self.ageTimer = QTimer()
self.ageTimer.setInterval(10000)
self.ageTimer.timeout.connect(self._updateBuildAge)
self.ageTimer.start()
logger.debug("GuiBuildNovelDocView initialisation complete") logger.debug("GuiBuildNovelDocView initialisation complete")
@@ -977,15 +1008,20 @@ class GuiBuildNovelDocView(QTextBrowser):
self.setFont(theFont) self.setFont(theFont)
return return
def setContent(self, theText): def setContent(self, theText, timeStamp):
"""Set the content, either from text or list of text. """Set the content, either from text or list of text.
""" """
if isinstance(theText, list): if isinstance(theText, list):
theText = "".join(theText) theText = "".join(theText)
self.buildTime = timeStamp
theText = theText.replace("&emsp;", "&nbsp;"*4) theText = theText.replace("&emsp;", "&nbsp;"*4)
theText = theText.replace("<del>", "<span style='text-decoration: line-through;'>") theText = theText.replace("<del>", "<span style='text-decoration: line-through;'>")
theText = theText.replace("</del>", "</span>") theText = theText.replace("</del>", "</span>")
self.setHtml(theText) self.setHtml(theText)
self._updateBuildAge()
return return
def setStyleSheet(self, theStyles=[]): def setStyleSheet(self, theStyles=[]):
@@ -1007,4 +1043,51 @@ class GuiBuildNovelDocView(QTextBrowser):
self.qDocument.setDefaultStyleSheet("") self.qDocument.setDefaultStyleSheet("")
return return
##
# Events
##
def resizeEvent(self, theEvent):
"""Make sure the document title is the same width as the window.
"""
QTextBrowser.resizeEvent(self, theEvent)
self._updateDocMargins()
return
##
# Internal Functions
##
def _updateBuildAge(self):
"""Update the build time and the fuzzy age.
"""
if self.buildTime > 0:
strBuildTime = "%s (%s)" % (
datetime.fromtimestamp(self.buildTime).strftime("%x %X"),
fuzzyTime(time() - self.buildTime)
)
else:
strBuildTime = "Unknown"
self.theTitle.setText("<b>Build Time:</b> %s" % strBuildTime)
def _updateDocMargins(self):
"""Automatically adjust the header to fill the top of the
document within the viewport.
"""
vBar = self.verticalScrollBar()
if vBar.isVisible():
sW = vBar.width()
else:
sW = 0
tB = self.frameWidth()
tW = self.width() - 2*tB - sW
tH = self.theTitle.height()
self.theTitle.setGeometry(tB, tB, tW, tH)
self.setViewportMargins(0, tH, 0, 0)
return
# END Class GuiBuildNovelDocView # END Class GuiBuildNovelDocView
+100 -4
View File
@@ -30,17 +30,17 @@
import logging import logging
import nw import nw
from PyQt5.QtGui import QColor, QPalette, QPainter from PyQt5.QtGui import QColor, QPalette, QPainter, QFontMetrics
from PyQt5.QtCore import ( from PyQt5.QtCore import (
Qt, QRect, QPoint, QSize, QRectF, QPropertyAnimation, pyqtProperty Qt, QRect, QPoint, QSize, QRectF, QPropertyAnimation, pyqtProperty
) )
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QGridLayout, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QSizePolicy, QGridLayout, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QSizePolicy,
QAbstractButton, QDialog, QTabWidget, QTabBar, QStyle, QAbstractButton, QDialog, QTabWidget, QTabBar, QStyle, QDialogButtonBox,
QStylePainter, QStyleOptionTab QStylePainter, QStyleOptionTab, QListWidget, QListWidgetItem, QFrame
) )
from nw.constants import nwUnicode from nw.constants import nwUnicode, nwQuotes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -436,3 +436,99 @@ class VerticalTabBar(QTabBar):
return return
# END Class VerticalTabBar # 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
+113 -54
View File
@@ -45,11 +45,12 @@ from PyQt5.QtWidgets import (
QFrame QFrame
) )
from nw.core import NWDoc from nw.core import NWDoc, NWSpellSimple, countWords
from nw.gui.dochighlight import GuiDocHighlighter from nw.gui.dochighlight import GuiDocHighlighter
from nw.core import NWSpellSimple, countWords
from nw.constants import nwUnicode, nwDocAction, nwItemClass
from nw.common import transferCase from nw.common import transferCase
from nw.constants import (
nwAlert, nwUnicode, nwDocAction, nwDocInsert, nwInsertSymbols
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -76,6 +77,7 @@ class GuiDocEditor(QTextEdit):
self.paraCount = 0 self.paraCount = 0
self.lastEdit = 0 self.lastEdit = 0
self.bigDoc = False self.bigDoc = False
self.doReplace = False
self.nonWord = "\"'" self.nonWord = "\"'"
# Typography # Typography
@@ -86,7 +88,6 @@ class GuiDocEditor(QTextEdit):
# Core Elements # Core Elements
self.qDocument = self.document() self.qDocument = self.document()
self.qDocument.setDocumentMargin(self.mainConf.getTextMargin())
self.qDocument.contentsChange.connect(self._docChange) self.qDocument.contentsChange.connect(self._docChange)
# Document Title # Document Title
@@ -101,8 +102,7 @@ class GuiDocEditor(QTextEdit):
self.setContextMenuPolicy(Qt.CustomContextMenu) self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._openContextMenu) self.customContextMenuRequested.connect(self._openContextMenu)
# Editor State # Editor Settings
self.hasSelection = False
self.setMinimumWidth(self.mainConf.pxInt(300)) self.setMinimumWidth(self.mainConf.pxInt(300))
self.setAutoFillBackground(True) self.setAutoFillBackground(True)
self.setAcceptRichText(False) self.setAcceptRichText(False)
@@ -157,8 +157,7 @@ class GuiDocEditor(QTextEdit):
self.paraCount = 0 self.paraCount = 0
self.lastEdit = 0 self.lastEdit = 0
self.bigDoc = False self.bigDoc = False
self.doReplace = False
self.hasSelection = False
self.setDocumentChanged(False) self.setDocumentChanged(False)
self.docHeader.setTitleFromHandle(self.theHandle) self.docHeader.setTitleFromHandle(self.theHandle)
@@ -261,7 +260,9 @@ class GuiDocEditor(QTextEdit):
self.hLight.spellCheck = False self.hLight.spellCheck = False
bfTime = time() bfTime = time()
self._allowAutoReplace(False)
self.setPlainText(theDoc) self.setPlainText(theDoc)
self._allowAutoReplace(True)
afTime = time() afTime = time()
logger.debug("Document highlighted in %.3f milliseconds" % (1000*(afTime-bfTime))) logger.debug("Document highlighted in %.3f milliseconds" % (1000*(afTime-bfTime)))
@@ -356,7 +357,11 @@ class GuiDocEditor(QTextEdit):
else: else:
rH = 0 rH = 0
<<<<<<< HEAD
self.setViewportMargins(tM, max(cM, tH, rH), tM, max(cM, fH)) self.setViewportMargins(tM, max(cM, tH, rH), tM, max(cM, fH))
=======
self.setViewportMargins(tM, max(cM, tH, rH), tM, cM)
>>>>>>> master
return return
@@ -392,8 +397,8 @@ class GuiDocEditor(QTextEdit):
""" """
if self.mainConf.verQtValue >= 50900: if self.mainConf.verQtValue >= 50900:
theText = self.qDocument.toRawText() theText = self.qDocument.toRawText()
theText = theText.replace("\u2028", "\n") # Line separators theText = theText.replace(nwUnicode.U_LSEP, "\n") # Line separators
theText = theText.replace("\u2029", "\n") # Paragraph separators theText = theText.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
else: else:
theText = self.toPlainText() theText = self.toPlainText()
return theText return theText
@@ -500,6 +505,7 @@ class GuiDocEditor(QTextEdit):
if not self.theParent.hasProject: if not self.theParent.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
self._allowAutoReplace(False)
if theAction == nwDocAction.UNDO: if theAction == nwDocAction.UNDO:
self.undo() self.undo()
elif theAction == nwDocAction.REDO: elif theAction == nwDocAction.REDO:
@@ -548,9 +554,15 @@ class GuiDocEditor(QTextEdit):
self._formatBlock(nwDocAction.BLOCK_COM) self._formatBlock(nwDocAction.BLOCK_COM)
elif theAction == nwDocAction.BLOCK_TXT: elif theAction == nwDocAction.BLOCK_TXT:
self._formatBlock(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: else:
logger.debug("Unknown or unsupported document action %s" % str(theAction)) logger.debug("Unknown or unsupported document action %s" % str(theAction))
self._allowAutoReplace(True)
return False return False
self._allowAutoReplace(True)
return True return True
def isEmpty(self): def isEmpty(self):
@@ -574,6 +586,21 @@ class GuiDocEditor(QTextEdit):
)) ))
return 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): def closeSearch(self):
"""Close the search box. """Close the search box.
""" """
@@ -586,34 +613,23 @@ class GuiDocEditor(QTextEdit):
def keyPressEvent(self, keyEvent): def keyPressEvent(self, keyEvent):
"""Intercept key press events. """Intercept key press events.
We need to intercept key presses briefly to record the state of We need to intercept a few key sequences:
selection. This is in order to know whether we had a selection * The return key redirects here even if the search box has
prior to triggering the _docChange slot, as we do not want to focus. Since we need the return key to continue search, we
trigger autoreplace on selections. Autoreplace on selections block any further interaction here while it's in focus.
messes with undo/redo history. * The undo sequence bypasses the doAction pathway from the
We also need to intercept the Shift key modifier for certain key menu, so we redirect it back from here.
combinations that modifies standard keys like enter and space. * The default redo sequence is Ctrl+Shift+Z, which we don't
However, we don't want to spend a lot of time in this function use, so we block it.
as it is triggered on every keypress when typing.
""" """
self.hasSelection = self.textCursor().hasSelection()
if self.docSearch.searchBox.hasFocus(): if self.docSearch.searchBox.hasFocus():
# Block the event when the focus is on the search bar.
return return
elif keyEvent == QKeySequence.Redo:
if keyEvent.modifiers() == Qt.ShiftModifier: return
theKey = keyEvent.key() elif keyEvent == QKeySequence.Undo:
if theKey == Qt.Key_Return: self.docAction(nwDocAction.UNDO)
self._insertHardBreak()
elif theKey == Qt.Key_Enter:
self._insertHardBreak()
elif theKey == Qt.Key_Space:
self._insertNonBreakingSpace()
else:
QTextEdit.keyPressEvent(self, keyEvent)
else: else:
QTextEdit.keyPressEvent(self, keyEvent) QTextEdit.keyPressEvent(self, keyEvent)
return return
def mouseReleaseEvent(self, mEvent): def mouseReleaseEvent(self, mEvent):
@@ -649,7 +665,7 @@ class GuiDocEditor(QTextEdit):
self.setDocumentChanged(True) self.setDocumentChanged(True)
if not self.wcTimer.isActive(): if not self.wcTimer.isActive():
self.wcTimer.start() self.wcTimer.start()
if self.mainConf.doReplace and not self.hasSelection: if self.doReplace and charsAdded == 1:
self._docAutoReplace(self.qDocument.findBlock(thePos)) self._docAutoReplace(self.qDocument.findBlock(thePos))
return return
@@ -800,24 +816,6 @@ class GuiDocEditor(QTextEdit):
return True 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): def _openSpellContext(self):
"""Opens the spell check context menu at the current point of """Opens the spell check context menu at the current point of
the cursor. the cursor.
@@ -879,6 +877,58 @@ class GuiDocEditor(QTextEdit):
return 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): def _checkDocSize(self, theSize):
"""Check if document size crosses the big document limit set in """Check if document size crosses the big document limit set in
config. If so, we will set the big document flag to True. config. If so, we will set the big document flag to True.
@@ -1196,10 +1246,10 @@ class GuiDocEditor(QTextEdit):
self._beginSearch() self._beginSearch()
return return
theCursor = self.textCursor()
if not theCursor.hasSelection(): if not theCursor.hasSelection():
return return
theCursor = self.textCursor()
searchFor = self.docSearch.getSearchText() searchFor = self.docSearch.getSearchText()
replWith = self.docSearch.getReplaceText() replWith = self.docSearch.getReplaceText()
selText = theCursor.selectedText() selText = theCursor.selectedText()
@@ -1242,6 +1292,15 @@ class GuiDocEditor(QTextEdit):
return 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 # END Class GuiDocEditor
# =============================================================================================== # # =============================================================================================== #
@@ -1770,7 +1829,7 @@ class GuiDocEditHeader(QWidget):
"""Capture a click on the title and ensure that the item is """Capture a click on the title and ensure that the item is
selected in the project tree. selected in the project tree.
""" """
self.theParent.treeView.setSelectedHandle(self.theHandle) self.theParent.treeView.setSelectedHandle(self.theHandle, doScroll=True)
return return
# END Class GuiDocEditHeader # END Class GuiDocEditHeader
+1 -1
View File
@@ -132,7 +132,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
# Non-Breaking Spaces # Non-Breaking Spaces
self.hRules.append(( self.hRules.append((
"[%s]+" % nwUnicode.U_NBSP, { "[%s%s]+" % (nwUnicode.U_NBSP, nwUnicode.U_THNBSP), {
0 : self.hStyles["nobreak"], 0 : self.hStyles["nobreak"],
} }
)) ))
+1 -1
View File
@@ -120,7 +120,7 @@ class GuiDocMerge(QDialog):
self.theParent.treeView.revealTreeItem(nHandle) self.theParent.treeView.revealTreeItem(nHandle)
theDoc.openDocument(nHandle, False) theDoc.openDocument(nHandle, False)
theDoc.saveDocument(theText) theDoc.saveDocument(theText)
self.theParent.openDocument(nHandle) self.theParent.openDocument(nHandle, doScroll=True)
self.close() self.close()
+8 -13
View File
@@ -60,6 +60,7 @@ class GuiDocViewer(QTextBrowser):
self.qDocument = self.document() self.qDocument = self.document()
self.setMinimumWidth(self.mainConf.pxInt(300)) self.setMinimumWidth(self.mainConf.pxInt(300))
self.setAutoFillBackground(True)
self.setOpenExternalLinks(False) self.setOpenExternalLinks(False)
self.initViewer() self.initViewer()
@@ -104,11 +105,12 @@ class GuiDocViewer(QTextBrowser):
self.setFont(theFont) self.setFont(theFont)
docPalette = self.palette() docPalette = self.palette()
docPalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack))
docPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack)) docPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack))
docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText)) docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText))
self.setPalette(docPalette) self.setPalette(docPalette)
self.qDocument.setDocumentMargin(self.mainConf.getTextMargin()) self.qDocument.setDocumentMargin(0)
theOpt = QTextOption() theOpt = QTextOption()
if self.mainConf.doJustify: if self.mainConf.doJustify:
theOpt.setAlignment(Qt.AlignJustify) theOpt.setAlignment(Qt.AlignJustify)
@@ -242,19 +244,10 @@ class GuiDocViewer(QTextBrowser):
tH = self.docHeader.height() tH = self.docHeader.height()
fH = self.docFooter.height() fH = self.docFooter.height()
fY = self.height() - fH - tB fY = self.height() - fH - tB
tT = cM - tH
bT = cM - fH
self.docHeader.setGeometry(tB, tB, tW, tH) self.docHeader.setGeometry(tB, tB, tW, tH)
self.docFooter.setGeometry(tB, fY, tW, fH) self.docFooter.setGeometry(tB, fY, tW, fH)
self.setViewportMargins(0, tH, 0, fH) self.setViewportMargins(cM, max(cM, tH), cM, max(cM, fH))
docFormat = self.qDocument.rootFrame().frameFormat()
docFormat.setTopMargin(max(0, tT))
docFormat.setBottomMargin(max(0, bT))
self.qDocument.blockSignals(True)
self.qDocument.rootFrame().setFrameFormat(docFormat)
self.qDocument.blockSignals(False)
return return
@@ -536,6 +529,8 @@ class GuiDocViewHeader(QWidget):
def _refreshDocument(self): def _refreshDocument(self):
"""Reload the content of the document. """Reload the content of the document.
""" """
if self.docViewer.theHandle == self.theParent.docEditor.theHandle:
self.theParent.saveDocument()
self.docViewer.reloadText() self.docViewer.reloadText()
return return
@@ -547,7 +542,7 @@ class GuiDocViewHeader(QWidget):
"""Capture a click on the title and ensure that the item is """Capture a click on the title and ensure that the item is
selected in the project tree. selected in the project tree.
""" """
self.theParent.treeView.setSelectedHandle(self.theHandle) self.theParent.treeView.setSelectedHandle(self.theHandle, doScroll=True)
return return
# END Class GuiDocViewHeader # END Class GuiDocViewHeader
+153 -55
View File
@@ -33,7 +33,7 @@ from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QMenuBar, QAction, QMessageBox from PyQt5.QtWidgets import QMenuBar, QAction, QMessageBox
from nw.gui.about import GuiAbout from nw.gui.about import GuiAbout
from nw.constants import nwItemType, nwItemClass, nwDocAction from nw.constants import nwItemType, nwItemClass, nwDocAction, nwDocInsert
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -50,8 +50,9 @@ class GuiMainMenu(QMenuBar):
self._buildProjectMenu() self._buildProjectMenu()
self._buildDocumentMenu() self._buildDocumentMenu()
self._buildEditMenu() self._buildEditMenu()
self._buildViewMenu() self._buildInsertMenu()
self._buildFormatMenu() self._buildFormatMenu()
self._buildViewMenu()
self._buildToolsMenu() self._buildToolsMenu()
self._buildHelpMenu() self._buildHelpMenu()
@@ -59,6 +60,7 @@ class GuiMainMenu(QMenuBar):
self._docAction = self.theParent.passDocumentAction self._docAction = self.theParent.passDocumentAction
self._moveTreeItem = self.theParent.treeView.moveTreeItem self._moveTreeItem = self.theParent.treeView.moveTreeItem
self._newTreeItem = self.theParent.treeView.newTreeItem self._newTreeItem = self.theParent.treeView.newTreeItem
self._docInsert = self.theParent.docEditor.insertText
logger.debug("GuiMainMenu initialisation complete") logger.debug("GuiMainMenu initialisation complete")
@@ -97,6 +99,8 @@ class GuiMainMenu(QMenuBar):
## ##
def _menuExit(self): def _menuExit(self):
"""Exit novelWriter.
"""
self.theParent.closeMain() self.theParent.closeMain()
return return
@@ -123,19 +127,33 @@ class GuiMainMenu(QMenuBar):
return True return True
def _showAboutQt(self): def _showAboutQt(self):
"""Show Qt's own About dialog.
"""
msgBox = QMessageBox() msgBox = QMessageBox()
msgBox.aboutQt(self.theParent,"About Qt") msgBox.aboutQt(self.theParent,"About Qt")
return True return True
def _openHelp(self): def _openHelp(self):
"""Open the documentation URL in the system's default browser.
"""
QDesktopServices.openUrl(QUrl(nw.__docurl__)) QDesktopServices.openUrl(QUrl(nw.__docurl__))
return True return True
def _openIssue(self):
"""Open the issue tracker URL in the system's default browser.
"""
QDesktopServices.openUrl(QUrl(nw.__issuesurl__))
return True
def _showDocumentLocation(self): def _showDocumentLocation(self):
"""Open the dialog showing the location of the editor document.
"""
self.theParent.docEditor.revealLocation() self.theParent.docEditor.revealLocation()
return True return True
def _doBackup(self): def _doBackup(self):
"""Call the backup function for the project.
"""
self.theProject.zipIt(True) self.theProject.zipIt(True)
return True return True
@@ -228,14 +246,14 @@ class GuiMainMenu(QMenuBar):
self.projMenu.addSeparator() self.projMenu.addSeparator()
# Project > Edit # Project > Edit
self.aEditItem = QAction("&Edit Project Item", self) self.aEditItem = QAction("Edit Project Item", self)
self.aEditItem.setStatusTip("Change item settings") self.aEditItem.setStatusTip("Change item settings")
self.aEditItem.setShortcuts(["Ctrl+E", "F2"]) self.aEditItem.setShortcuts(["Ctrl+E", "F2"])
self.aEditItem.triggered.connect(self.theParent.editItem) self.aEditItem.triggered.connect(self.theParent.editItem)
self.projMenu.addAction(self.aEditItem) self.projMenu.addAction(self.aEditItem)
# Project > Delete # Project > Delete
self.aDeleteItem = QAction("&Delete Project Item", self) self.aDeleteItem = QAction("Delete Project Item", self)
self.aDeleteItem.setStatusTip("Delete selected item") self.aDeleteItem.setStatusTip("Delete selected item")
self.aDeleteItem.setShortcut("Ctrl+Del") self.aDeleteItem.setShortcut("Ctrl+Del")
self.aDeleteItem.triggered.connect(lambda : self.theParent.treeView.deleteItem(None)) self.aDeleteItem.triggered.connect(lambda : self.theParent.treeView.deleteItem(None))
@@ -265,21 +283,21 @@ class GuiMainMenu(QMenuBar):
self.docuMenu = self.addMenu("&Document") self.docuMenu = self.addMenu("&Document")
# Document > New # Document > New
self.aNewDoc = QAction("&New Document", self) self.aNewDoc = QAction("New Document", self)
self.aNewDoc.setStatusTip("Create new document") self.aNewDoc.setStatusTip("Create new document")
self.aNewDoc.setShortcut("Ctrl+N") self.aNewDoc.setShortcut("Ctrl+N")
self.aNewDoc.triggered.connect(lambda : self._newTreeItem(nwItemType.FILE, None)) self.aNewDoc.triggered.connect(lambda : self._newTreeItem(nwItemType.FILE, None))
self.docuMenu.addAction(self.aNewDoc) self.docuMenu.addAction(self.aNewDoc)
# Document > Open # Document > Open
self.aOpenDoc = QAction("&Open Document", self) self.aOpenDoc = QAction("Open Document", self)
self.aOpenDoc.setStatusTip("Open selected document") self.aOpenDoc.setStatusTip("Open selected document")
self.aOpenDoc.setShortcut("Ctrl+O") self.aOpenDoc.setShortcut("Ctrl+O")
self.aOpenDoc.triggered.connect(self.theParent.openSelectedItem) self.aOpenDoc.triggered.connect(self.theParent.openSelectedItem)
self.docuMenu.addAction(self.aOpenDoc) self.docuMenu.addAction(self.aOpenDoc)
# Document > Save # Document > Save
self.aSaveDoc = QAction("&Save Document", self) self.aSaveDoc = QAction("Save Document", self)
self.aSaveDoc.setStatusTip("Save current document") self.aSaveDoc.setStatusTip("Save current document")
self.aSaveDoc.setShortcut("Ctrl+S") self.aSaveDoc.setShortcut("Ctrl+S")
self.aSaveDoc.triggered.connect(self.theParent.saveDocument) self.aSaveDoc.triggered.connect(self.theParent.saveDocument)
@@ -341,53 +359,6 @@ class GuiMainMenu(QMenuBar):
return 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): def _buildEditMenu(self):
# Edit # Edit
@@ -497,6 +468,65 @@ class GuiMainMenu(QMenuBar):
return 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): def _buildFormatMenu(self):
# Format # Format
@@ -547,7 +577,7 @@ class GuiMainMenu(QMenuBar):
self.aFmtSQuote.triggered.connect(lambda: self._docAction(nwDocAction.S_QUOTE)) self.aFmtSQuote.triggered.connect(lambda: self._docAction(nwDocAction.S_QUOTE))
self.fmtMenu.addAction(self.aFmtSQuote) self.fmtMenu.addAction(self.aFmtSQuote)
# Edit > Separator # Format > Separator
self.fmtMenu.addSeparator() self.fmtMenu.addSeparator()
# Format > Header 1 # Format > Header 1
@@ -592,6 +622,68 @@ class GuiMainMenu(QMenuBar):
self.aFmtNoFormat.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_TXT)) self.aFmtNoFormat.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_TXT))
self.fmtMenu.addAction(self.aFmtNoFormat) 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 return
def _buildToolsMenu(self): def _buildToolsMenu(self):
@@ -703,6 +795,12 @@ class GuiMainMenu(QMenuBar):
self.aHelp.triggered.connect(self._openHelp) self.aHelp.triggered.connect(self._openHelp)
self.helpMenu.addAction(self.aHelp) 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 return
# END Class GuiMainMenu # END Class GuiMainMenu
+1 -1
View File
@@ -196,7 +196,7 @@ class GuiOutline(QTreeWidget):
except: except:
tLine = 1 tLine = 1
logger.verbose("User selected entry with handle %s on line %s" % (tHandle, tLine)) logger.verbose("User selected entry with handle %s on line %s" % (tHandle, tLine))
self.theParent.openDocument(tHandle, tLine - 1) self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True)
return return
def _itemSelected(self): def _itemSelected(self):
+63 -37
View File
@@ -37,9 +37,9 @@ from PyQt5.QtWidgets import (
QDialogButtonBox, QFileDialog, QFontDialog 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.core import NWSpellCheck, NWSpellSimple, NWSpellEnchant
from nw.constants import nwAlert, nwQuotes from nw.constants import nwQuotes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -816,51 +816,72 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addGroupLabel("Quotation Style") self.mainForm.addGroupLabel("Quotation Style")
qWidth = self.mainConf.pxInt(40) qWidth = self.mainConf.pxInt(40)
bWidth = int(2.5*self.theTheme.getTextWidth("..."))
## Single Quote Style ## Single Quote Style
self.quoteSingleStyleO = QLineEdit() self.quoteSingleStyleO = QLineEdit()
self.quoteSingleStyleO.setMaxLength(1) self.quoteSingleStyleO.setMaxLength(1)
self.quoteSingleStyleO.setReadOnly(True)
self.quoteSingleStyleO.setFixedWidth(qWidth) self.quoteSingleStyleO.setFixedWidth(qWidth)
self.quoteSingleStyleO.setAlignment(Qt.AlignCenter) self.quoteSingleStyleO.setAlignment(Qt.AlignCenter)
self.quoteSingleStyleO.setText(self.mainConf.fmtSingleQuotes[0]) self.quoteSingleStyleO.setText(self.mainConf.fmtSingleQuotes[0])
self.btnSingleStyleO = QPushButton("...")
self.btnSingleStyleO.setMaximumWidth(bWidth)
self.btnSingleStyleO.clicked.connect(self._getSingleOpen)
self.mainForm.addRow( self.mainForm.addRow(
"Single quote open style", "Single quote open style",
self.quoteSingleStyleO, self.quoteSingleStyleO,
"Auto-replaces apostrophe before words." "Auto-replaces apostrophe before words.",
theButton=self.btnSingleStyleO
) )
self.quoteSingleStyleC = QLineEdit() self.quoteSingleStyleC = QLineEdit()
self.quoteSingleStyleC.setMaxLength(1) self.quoteSingleStyleC.setMaxLength(1)
self.quoteSingleStyleC.setReadOnly(True)
self.quoteSingleStyleC.setFixedWidth(qWidth) self.quoteSingleStyleC.setFixedWidth(qWidth)
self.quoteSingleStyleC.setAlignment(Qt.AlignCenter) self.quoteSingleStyleC.setAlignment(Qt.AlignCenter)
self.quoteSingleStyleC.setText(self.mainConf.fmtSingleQuotes[1]) self.quoteSingleStyleC.setText(self.mainConf.fmtSingleQuotes[1])
self.btnSingleStyleC = QPushButton("...")
self.btnSingleStyleC.setMaximumWidth(bWidth)
self.btnSingleStyleC.clicked.connect(self._getSingleClose)
self.mainForm.addRow( self.mainForm.addRow(
"Single quote close style", "Single quote close style",
self.quoteSingleStyleC, self.quoteSingleStyleC,
"Auto-replaces apostrophe after words." "Auto-replaces apostrophe after words.",
theButton=self.btnSingleStyleC
) )
## Double Quote Style ## Double Quote Style
self.quoteDoubleStyleO = QLineEdit() self.quoteDoubleStyleO = QLineEdit()
self.quoteDoubleStyleO.setMaxLength(1) self.quoteDoubleStyleO.setMaxLength(1)
self.quoteDoubleStyleO.setReadOnly(True)
self.quoteDoubleStyleO.setFixedWidth(qWidth) self.quoteDoubleStyleO.setFixedWidth(qWidth)
self.quoteDoubleStyleO.setAlignment(Qt.AlignCenter) self.quoteDoubleStyleO.setAlignment(Qt.AlignCenter)
self.quoteDoubleStyleO.setText(self.mainConf.fmtDoubleQuotes[0]) self.quoteDoubleStyleO.setText(self.mainConf.fmtDoubleQuotes[0])
self.btnDoubleStyleO = QPushButton("...")
self.btnDoubleStyleO.setMaximumWidth(bWidth)
self.btnDoubleStyleO.clicked.connect(self._getDoubleOpen)
self.mainForm.addRow( self.mainForm.addRow(
"Double quote open style", "Double quote open style",
self.quoteDoubleStyleO, self.quoteDoubleStyleO,
"Auto-replaces straight quotes before words." "Auto-replaces straight quotes before words.",
theButton=self.btnDoubleStyleO
) )
self.quoteDoubleStyleC = QLineEdit() self.quoteDoubleStyleC = QLineEdit()
self.quoteDoubleStyleC.setMaxLength(1) self.quoteDoubleStyleC.setMaxLength(1)
self.quoteDoubleStyleC.setReadOnly(True)
self.quoteDoubleStyleC.setFixedWidth(qWidth) self.quoteDoubleStyleC.setFixedWidth(qWidth)
self.quoteDoubleStyleC.setAlignment(Qt.AlignCenter) self.quoteDoubleStyleC.setAlignment(Qt.AlignCenter)
self.quoteDoubleStyleC.setText(self.mainConf.fmtDoubleQuotes[1]) self.quoteDoubleStyleC.setText(self.mainConf.fmtDoubleQuotes[1])
self.btnDoubleStyleC = QPushButton("...")
self.btnDoubleStyleC.setMaximumWidth(bWidth)
self.btnDoubleStyleC.clicked.connect(self._getDoubleClose)
self.mainForm.addRow( self.mainForm.addRow(
"Double quote close style", "Double quote close style",
self.quoteDoubleStyleC, self.quoteDoubleStyleC,
"Auto-replaces straight quotes after words." "Auto-replaces straight quotes after words.",
theButton=self.btnDoubleStyleC
) )
return return
@@ -889,37 +910,10 @@ class GuiConfigEditAutoReplaceTab(QWidget):
fmtDoubleQuotesO = self.quoteDoubleStyleO.text() fmtDoubleQuotesO = self.quoteDoubleStyleO.text()
fmtDoubleQuotesC = self.quoteDoubleStyleC.text() fmtDoubleQuotesC = self.quoteDoubleStyleC.text()
if self._checkQuoteSymbol(fmtSingleQuotesO): self.mainConf.fmtSingleQuotes[0] = fmtSingleQuotesO
self.mainConf.fmtSingleQuotes[0] = fmtSingleQuotesO self.mainConf.fmtSingleQuotes[1] = fmtSingleQuotesC
else: self.mainConf.fmtDoubleQuotes[0] = fmtDoubleQuotesO
self.theParent.makeAlert( self.mainConf.fmtDoubleQuotes[1] = fmtDoubleQuotesC
"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.confChanged = True self.mainConf.confChanged = True
@@ -939,6 +933,38 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.autoReplaceDots.setEnabled(theState) self.autoReplaceDots.setEnabled(theState)
return 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 # Internal Functions
## ##
+39 -11
View File
@@ -31,10 +31,9 @@ import nw
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QLineEdit, QPlainTextEdit, QHBoxLayout, QVBoxLayout, QGridLayout, QLineEdit, QPlainTextEdit, QLabel,
QLabel, QWidget, QTabWidget, QDialogButtonBox, QListWidget, QPushButton, QWidget, QDialogButtonBox, QListWidget, QPushButton, QListWidgetItem,
QListWidgetItem, QColorDialog, QAbstractItemView, QTreeWidget, QCheckBox, QColorDialog, QAbstractItemView, QTreeWidget, QTreeWidgetItem
QTreeWidgetItem
) )
from nw.constants import nwAlert from nw.constants import nwAlert
@@ -52,9 +51,17 @@ class GuiProjectSettings(PagedDialog):
self.mainConf = nw.CONFIG self.mainConf = nw.CONFIG
self.theParent = theParent self.theParent = theParent
self.theProject = theProject self.theProject = theProject
self.optState = theProject.optState
self.theProject.countStatus() self.theProject.countStatus()
self.setWindowTitle("Project Settings") 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.tabMain = GuiProjectEditMain(self.theParent, self.theProject)
self.tabMeta = GuiProjectEditMeta(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.buttonBox.rejected.connect(self._doClose)
self.addControls(self.buttonBox) self.addControls(self.buttonBox)
self.show()
logger.debug("GuiProjectSettings initialisation complete") logger.debug("GuiProjectSettings initialisation complete")
return return
##
# Slots
##
def _doSave(self): def _doSave(self):
"""Save settings and close dialog.
"""
logger.verbose("GuiProjectSettings save button clicked") logger.verbose("GuiProjectSettings save button clicked")
projName = self.tabMain.editName.text() projName = self.tabMain.editName.text()
@@ -103,13 +114,23 @@ class GuiProjectSettings(PagedDialog):
newList = self.tabReplace.getNewList() newList = self.tabReplace.getNewList()
self.theProject.setAutoReplace(newList) self.theProject.setAutoReplace(newList)
self.close() self._doClose()
return return
def _doClose(self): 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() self.close()
return return
# END Class GuiProjectSettings # END Class GuiProjectSettings
@@ -455,16 +476,23 @@ class GuiProjectEditReplace(QWidget):
def __init__(self, theParent, theProject): def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent) QWidget.__init__(self, theParent)
self.mainConf = nw.CONFIG
self.theParent = theParent self.theParent = theParent
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
self.theProject = theProject self.theProject = theProject
self.optState = theProject.optState
self.arChanged = False self.arChanged = False
self.outerBox = QVBoxLayout() self.outerBox = QVBoxLayout()
self.bottomBox = QHBoxLayout() self.bottomBox = QHBoxLayout()
self.listBox = QTreeWidget()
wCol0 = self.mainConf.pxInt(
self.optState.getInt("GuiProjectSettings", "replaceColW", 100)
)
self.listBox = QTreeWidget()
self.listBox.setHeaderLabels(["Keyword","Replace With"]) self.listBox.setHeaderLabels(["Keyword","Replace With"])
self.listBox.itemSelectionChanged.connect(self._selectedItem) self.listBox.itemSelectionChanged.connect(self._selectedItem)
self.listBox.setColumnWidth(0, wCol0)
self.listBox.setIndentation(0) self.listBox.setIndentation(0)
for aKey, aVal in self.theProject.autoReplace.items(): for aKey, aVal in self.theProject.autoReplace.items():
+27 -22
View File
@@ -54,6 +54,7 @@ class GuiProjectTree(QTreeWidget):
QTreeWidget.__init__(self, theParent) QTreeWidget.__init__(self, theParent)
logger.debug("Initialising GuiProjectTree ...") logger.debug("Initialising GuiProjectTree ...")
self.mainConf = nw.CONFIG self.mainConf = nw.CONFIG
self.theParent = theParent self.theParent = theParent
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
@@ -237,8 +238,8 @@ class GuiProjectTree(QTreeWidget):
""" """
if qApp.focusWidget() == self and self.theParent.hasProject: if qApp.focusWidget() == self and self.theParent.hasProject:
tHandle = self.getSelectedHandle() tHandle = self.getSelectedHandle()
tItem = self._getTreeItem(tHandle) tItem = self._getTreeItem(tHandle)
pItem = tItem.parent() pItem = tItem.parent()
if pItem is None: if pItem is None:
tIndex = self.indexOfTopLevelItem(tItem) tIndex = self.indexOfTopLevelItem(tItem)
nChild = self.topLevelItemCount() nChild = self.topLevelItemCount()
@@ -367,6 +368,7 @@ class GuiProjectTree(QTreeWidget):
if nwItemS is None: if nwItemS is None:
return False return False
wCount = int(trItemS.text(self.C_COUNT))
if nwItemS.itemType == nwItemType.FILE: if nwItemS.itemType == nwItemType.FILE:
logger.debug("User requested file %s moved to trash" % tHandle) logger.debug("User requested file %s moved to trash" % tHandle)
trItemP = trItemS.parent() trItemP = trItemS.parent()
@@ -393,7 +395,8 @@ class GuiProjectTree(QTreeWidget):
if doPermanent: if doPermanent:
logger.debug("Permanently deleting file with handle %s" % tHandle) logger.debug("Permanently deleting file with handle %s" % tHandle)
tIndex = trItemP.indexOfChild(trItemS) self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex) trItemC = trItemP.takeChild(tIndex)
if self.theParent.docEditor.theHandle == tHandle: if self.theParent.docEditor.theHandle == tHandle:
@@ -422,10 +425,12 @@ class GuiProjectTree(QTreeWidget):
if pHandle is None: if pHandle is None:
logger.warning("File has no parent item") logger.warning("File has no parent item")
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS) tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex) trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC) trItemT.addChild(trItemC)
nwItemS.setParent(self.theProject.projTree.trashRoot()) nwItemS.setParent(self.theProject.projTree.trashRoot())
self.propagateCount(tHandle, wCount)
self._setTreeChanged(True) self._setTreeChanged(True)
self.theParent.theIndex.deleteHandle(tHandle) self.theParent.theIndex.deleteHandle(tHandle)
@@ -504,7 +509,7 @@ class GuiProjectTree(QTreeWidget):
""" """
tItem = self._getTreeItem(tHandle) tItem = self._getTreeItem(tHandle)
if tItem is not None: if tItem is not None:
tItem.setText(self.C_COUNT,str(theCount)) tItem.setText(self.C_COUNT, str(theCount))
pItem = tItem.parent() pItem = tItem.parent()
if pItem is not None: if pItem is not None:
pCount = 0 pCount = 0
@@ -520,7 +525,7 @@ class GuiProjectTree(QTreeWidget):
relevant values in the project and on the status bar. This call relevant values in the project and on the status bar. This call
is a fast way of getting this number, and depends on the is a fast way of getting this number, and depends on the
propagateCount function being called when it should to maintain propagateCount function being called when it should to maintain
the correct count. the correct count. Orphan folder is not included in the total.
""" """
nWords = 0 nWords = 0
for n in range(self.topLevelItemCount()): for n in range(self.topLevelItemCount()):
@@ -569,14 +574,14 @@ class GuiProjectTree(QTreeWidget):
selHandles.append(selItems[n].data(self.C_NAME, Qt.UserRole)) selHandles.append(selItems[n].data(self.C_NAME, Qt.UserRole))
return selHandles return selHandles
def setSelectedHandle(self, tHandle): def setSelectedHandle(self, tHandle, doScroll=False):
"""Set a specific handle as the selected item. """Set a specific handle as the selected item.
""" """
if tHandle in self.theMap: if tHandle in self.theMap:
self.clearSelection() self.clearSelection()
self.theMap[tHandle].setSelected(True) self.theMap[tHandle].setSelected(True)
selItems = self.selectedIndexes() selItems = self.selectedIndexes()
if selItems: if selItems and doScroll:
self.scrollTo( self.scrollTo(
selItems[0], QAbstractItemView.PositionAtCenter selItems[0], QAbstractItemView.PositionAtCenter
) )
@@ -629,23 +634,26 @@ class GuiProjectTree(QTreeWidget):
logger.error("Invalid drop index") logger.error("Invalid drop index")
return return
dItem = self.itemFromIndex(dIndex) sItem = self._getTreeItem(sHandle)
dItem = self.itemFromIndex(dIndex)
dHandle = dItem.data(self.C_NAME, Qt.UserRole) dHandle = dItem.data(self.C_NAME, Qt.UserRole)
snItem = self.theProject.projTree[sHandle] snItem = self.theProject.projTree[sHandle]
dnItem = self.theProject.projTree[dHandle] dnItem = self.theProject.projTree[dHandle]
if dnItem is None: if dnItem is None:
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR) self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
return return
isSame = snItem.itemClass == dnItem.itemClass wCount = int(sItem.text(self.C_COUNT))
isNone = snItem.itemClass == nwItemClass.NO_CLASS isSame = snItem.itemClass == dnItem.itemClass
isNote = snItem.itemLayout == nwItemLayout.NOTE isNone = snItem.itemClass == nwItemClass.NO_CLASS
onFile = dnItem.itemType == nwItemType.FILE isNote = snItem.itemLayout == nwItemLayout.NOTE
isRoot = snItem.itemType == nwItemType.ROOT onFile = dnItem.itemType == nwItemType.FILE
onRoot = dnItem.itemType == nwItemType.ROOT isRoot = snItem.itemType == nwItemType.ROOT
onRoot = dnItem.itemType == nwItemType.ROOT
isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem
if (isSame or isNone or isNote) and not (onFile and isOnTop) and not isRoot: if (isSame or isNone or isNote) and not (onFile and isOnTop) and not isRoot:
logger.debug("Drag'n'drop of item %s accepted" % sHandle) logger.debug("Drag'n'drop of item %s accepted" % sHandle)
self.propagateCount(sHandle, 0)
QTreeWidget.dropEvent(self, theEvent) QTreeWidget.dropEvent(self, theEvent)
if isNone: if isNone:
self._moveOrphanedItem(sHandle, dHandle) self._moveOrphanedItem(sHandle, dHandle)
@@ -660,6 +668,7 @@ class GuiProjectTree(QTreeWidget):
)) ))
snItem.setClass(dnItem.itemClass) snItem.setClass(dnItem.itemClass)
self.setTreeItemValues(sHandle) self.setTreeItemValues(sHandle)
self.propagateCount(sHandle, wCount)
else: else:
logger.debug("Drag'n'drop of item %s not accepted" % sHandle) logger.debug("Drag'n'drop of item %s not accepted" % sHandle)
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR) self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
@@ -794,8 +803,7 @@ class GuiProjectTree(QTreeWidget):
def _updateItemParent(self, tHandle): def _updateItemParent(self, tHandle):
"""Update the parent handle of an item so that the information """Update the parent handle of an item so that the information
in the project is consistent with the treeView. Also move the in the project is consistent with the treeView.
word count over to the new parent tree.
""" """
trItemS = self._getTreeItem(tHandle) trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.projTree[tHandle] nwItemS = self.theProject.projTree[tHandle]
@@ -805,10 +813,7 @@ class GuiProjectTree(QTreeWidget):
return False return False
pHandle = trItemP.data(self.C_NAME, Qt.UserRole) pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
wC = int(trItemS.text(self.C_COUNT))
self.propagateCount(tHandle, -wC)
nwItemS.setParent(pHandle) nwItemS.setParent(pHandle)
self.propagateCount(tHandle, wC)
self.setTreeItemValues(tHandle) self.setTreeItemValues(tHandle)
self._setTreeChanged(True) self._setTreeChanged(True)
@@ -929,7 +934,7 @@ class GuiProjectTreeMenu(QMenu):
"""Forward the open document call to the main GUI window. """Forward the open document call to the main GUI window.
""" """
if self.theItem is not None: if self.theItem is not None:
self.theTree.theParent.openDocument(self.theItem.itemHandle) self.theTree.theParent.openDocument(self.theItem.itemHandle, doScroll=False)
return return
def _doViewItem(self): def _doViewItem(self):
+407 -110
View File
@@ -26,78 +26,105 @@
""" """
import logging import logging
import json
import nw import nw
from os import path from os import path
from datetime import datetime from datetime import datetime
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont, QPixmap
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout,
QGridLayout, QLabel, QGroupBox, QCheckBox QLabel, QGroupBox, QMenu, QAction, QFileDialog
) )
from nw.constants import nwConst, nwFiles, nwAlert from nw.constants import nwConst, nwFiles, nwAlert
from nw.gui.custom import QSwitch
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GuiSessionLogView(QDialog): class GuiSessionLog(QDialog):
C_TIME = 0
C_LENGTH = 1
C_COUNT = 2
C_BAR = 3
FMT_JSON = 0
FMT_CSV = 1
def __init__(self, theParent, theProject): def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent) QDialog.__init__(self, theParent)
logger.debug("Initialising SessionLogView ...") logger.debug("Initialising GuiSessionLog ...")
self.mainConf = nw.CONFIG self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent self.theParent = theParent
self.optState = self.theProject.optState self.theProject = theProject
self.theTheme = theParent.theTheme
self.optState = theProject.optState
self.logData = []
self.filterData = []
self.timeFilter = 0.0 self.timeFilter = 0.0
self.timeTotal = 0.0 self.wordOffset = 0
self.outerBox = QGridLayout()
self.bottomBox = QHBoxLayout()
self.setWindowTitle("Session Log") self.setWindowTitle("Session Log")
self.setMinimumWidth(self.mainConf.pxInt(420)) self.setMinimumWidth(self.mainConf.pxInt(420))
self.setMinimumHeight(self.mainConf.pxInt(400)) self.setMinimumHeight(self.mainConf.pxInt(400))
self.resize(
self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "winWidth", 550)),
self.mainConf.pxInt(self.optState.getInt("GuiSessionLog", "winHeight", 500))
)
wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol0", 180)) # List Box
wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol1", 80)) wCol0 = self.mainConf.pxInt(
wCol2 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol2", 80)) self.optState.getInt("GuiSessionLog", "widthCol0", 180)
)
wCol1 = self.mainConf.pxInt(
self.optState.getInt("GuiSessionLog", "widthCol1", 80)
)
wCol2 = self.mainConf.pxInt(
self.optState.getInt("GuiSessionLog", "widthCol2", 80)
)
self.listBox = QTreeWidget() self.listBox = QTreeWidget()
self.listBox.setHeaderLabels(["Session Start","Length","Words",""]) self.listBox.setHeaderLabels(["Session Start","Length","Words","Histogram"])
self.listBox.setIndentation(0) self.listBox.setIndentation(0)
self.listBox.setColumnWidth(0, wCol0) self.listBox.setColumnWidth(self.C_TIME, wCol0)
self.listBox.setColumnWidth(1, wCol1) self.listBox.setColumnWidth(self.C_LENGTH, wCol1)
self.listBox.setColumnWidth(2, wCol2) self.listBox.setColumnWidth(self.C_COUNT, wCol2)
self.listBox.setColumnWidth(3, 0)
hHeader = self.listBox.headerItem() hHeader = self.listBox.headerItem()
hHeader.setTextAlignment(1,Qt.AlignRight) hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
hHeader.setTextAlignment(2,Qt.AlignRight) hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight)
self.monoFont = QFont("Monospace", 10) self.monoFont = QFont()
self.monoFont.setPointSizeF(0.9*self.theTheme.fontPointSize)
self.monoFont.setFamily(self.theTheme.guiFontFixed.family())
sortValid = (Qt.AscendingOrder, Qt.DescendingOrder) sortValid = (Qt.AscendingOrder, Qt.DescendingOrder)
sortCol = self.optState.validIntRange( sortCol = self.optState.validIntRange(
self.optState.getInt("GuiSession", "sortCol", 0), 0, 2, 0 self.optState.getInt("GuiSessionLog", "sortCol", 0), 0, 2, 0
) )
sortOrder = self.optState.validIntTuple( sortOrder = self.optState.validIntTuple(
self.optState.getInt("GuiSession", "sortOrder", Qt.DescendingOrder), self.optState.getInt("GuiSessionLog", "sortOrder", Qt.DescendingOrder),
sortValid, Qt.DescendingOrder sortValid, Qt.DescendingOrder
) )
self.listBox.sortByColumn(sortCol, sortOrder) self.listBox.sortByColumn(sortCol, sortOrder)
self.listBox.setSortingEnabled(True) self.listBox.setSortingEnabled(True)
# Word Bar
self.barHeight = int(round(0.5*self.theTheme.fontPixelSize))
self.barWidth = self.mainConf.pxInt(200)
self.barImage = QPixmap(self.barHeight, self.barHeight)
self.barImage.fill(self.palette().highlight().color())
# Session Info # Session Info
self.infoBox = QGroupBox("Sum Total Time", self) self.infoBox = QGroupBox("Sum Totals", self)
self.infoBoxForm = QGridLayout(self) self.infoForm = QGridLayout(self)
self.infoBox.setLayout(self.infoBoxForm) self.infoBox.setLayout(self.infoForm)
self.labelTotal = QLabel(self._formatTime(0)) self.labelTotal = QLabel(self._formatTime(0))
self.labelTotal.setFont(self.monoFont) self.labelTotal.setFont(self.monoFont)
@@ -107,105 +134,296 @@ class GuiSessionLogView(QDialog):
self.labelFilter.setFont(self.monoFont) self.labelFilter.setFont(self.monoFont)
self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.infoBoxForm.addWidget(QLabel("All:"), 0, 0) self.novelWords = QLabel("0")
self.infoBoxForm.addWidget(self.labelTotal, 0, 1) self.novelWords.setFont(self.monoFont)
self.infoBoxForm.addWidget(QLabel("Filtered:"), 1, 0) self.novelWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.infoBoxForm.addWidget(self.labelFilter, 1, 1)
self.notesWords = QLabel("0")
self.notesWords.setFont(self.monoFont)
self.notesWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.totalWords = QLabel("0")
self.totalWords.setFont(self.monoFont)
self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.infoForm.addWidget(QLabel("Total Time:"), 0, 0)
self.infoForm.addWidget(QLabel("Filtered Time:"), 1, 0)
self.infoForm.addWidget(QLabel("Novel Word Count:"), 2, 0)
self.infoForm.addWidget(QLabel("Notes Word Count:"), 3, 0)
self.infoForm.addWidget(QLabel("Total Word Count:"), 4, 0)
self.infoForm.addWidget(self.labelTotal, 0, 1)
self.infoForm.addWidget(self.labelFilter, 1, 1)
self.infoForm.addWidget(self.novelWords, 2, 1)
self.infoForm.addWidget(self.notesWords, 3, 1)
self.infoForm.addWidget(self.totalWords, 4, 1)
self.infoForm.setRowStretch(5, 1)
# Filter Options # Filter Options
self.filterBox = QGroupBox("Filters", self) sPx = self.theTheme.baseIconSize
self.filterBoxForm = QGridLayout(self)
self.filterBox.setLayout(self.filterBoxForm)
self.hideZeros = QCheckBox("Hide zero word count", self) self.filterBox = QGroupBox("Filters", self)
self.filterForm = QGridLayout(self)
self.filterBox.setLayout(self.filterForm)
self.incNovel = QSwitch(width=2*sPx, height=sPx)
self.incNovel.setChecked(
self.optState.getBool("GuiSessionLog", "incNovel", True)
)
self.incNovel.clicked.connect(self._updateListBox)
self.incNotes = QSwitch(width=2*sPx, height=sPx)
self.incNotes.setChecked(
self.optState.getBool("GuiSessionLog", "incNotes", True)
)
self.incNotes.clicked.connect(self._updateListBox)
self.hideZeros = QSwitch(width=2*sPx, height=sPx)
self.hideZeros.setChecked( self.hideZeros.setChecked(
self.optState.getBool("GuiSession", "hideZeros", True) self.optState.getBool("GuiSessionLog", "hideZeros", True)
) )
self.hideZeros.stateChanged.connect(self._doHideZeros) self.hideZeros.clicked.connect(self._updateListBox)
self.hideNegative = QCheckBox("Hide negative word count", self) self.hideNegative = QSwitch(width=2*sPx, height=sPx)
self.hideNegative.setChecked( self.hideNegative.setChecked(
self.optState.getBool("GuiSession", "hideNegative", False) self.optState.getBool("GuiSessionLog", "hideNegative", False)
) )
self.hideNegative.stateChanged.connect(self._doHideNegative) self.hideNegative.clicked.connect(self._updateListBox)
self.filterBoxForm.addWidget(self.hideZeros, 0, 0) self.groupByDay = QSwitch(width=2*sPx, height=sPx)
self.filterBoxForm.addWidget(self.hideNegative, 1, 0) self.groupByDay.setChecked(
self.optState.getBool("GuiSessionLog", "groupByDay", False)
)
self.groupByDay.clicked.connect(self._updateListBox)
self.filterForm.addWidget(QLabel("Count novel files"), 0, 0)
self.filterForm.addWidget(QLabel("Count note files"), 1, 0)
self.filterForm.addWidget(QLabel("Hide zero word count"), 2, 0)
self.filterForm.addWidget(QLabel("Hide negative word count"), 3, 0)
self.filterForm.addWidget(QLabel("Group entries by day"), 4, 0)
self.filterForm.addWidget(self.incNovel, 0, 1)
self.filterForm.addWidget(self.incNotes, 1, 1)
self.filterForm.addWidget(self.hideZeros, 2, 1)
self.filterForm.addWidget(self.hideNegative, 3, 1)
self.filterForm.addWidget(self.groupByDay, 4, 1)
self.filterForm.setRowStretch(5, 1)
# Buttons # Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
self.buttonBox.rejected.connect(self._doClose) self.buttonBox.rejected.connect(self._doClose)
self.btnSave = self.buttonBox.addButton("Save As", QDialogButtonBox.ActionRole)
self.saveMenu = QMenu(self)
self.btnSave.setMenu(self.saveMenu)
self.saveJSON = QAction("JSON Data File (.json)", self)
self.saveJSON.triggered.connect(lambda: self._saveData(self.FMT_JSON))
self.saveMenu.addAction(self.saveJSON)
self.saveCSV = QAction("CSV Data File (.csv)", self)
self.saveCSV.triggered.connect(lambda: self._saveData(self.FMT_CSV))
self.saveMenu.addAction(self.saveCSV)
# Assemble # Assemble
self.outerBox = QGridLayout()
self.outerBox.addWidget(self.listBox, 0, 0, 1, 2) self.outerBox.addWidget(self.listBox, 0, 0, 1, 2)
self.outerBox.addWidget(self.infoBox, 1, 0) self.outerBox.addWidget(self.infoBox, 1, 0)
self.outerBox.addWidget(self.filterBox, 1, 1) self.outerBox.addWidget(self.filterBox, 1, 1)
self.outerBox.addWidget(self.buttonBox, 2, 0, 1, 2) self.outerBox.addWidget(self.buttonBox, 2, 0, 1, 2)
self.outerBox.setRowStretch(0, 1)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
self.show() logger.debug("GuiSessionLog initialisation complete")
logger.debug("SessionLogView initialisation complete") qApp.processEvents()
self._loadLogFile()
self._loadSessionLog() self._updateListBox()
return return
def _loadSessionLog(self): ##
# Slots
##
logFile = path.join(self.theProject.projMeta, nwFiles.SESS_INFO) def _doClose(self):
if not path.isfile(logFile): """Save the state of the window, clear cache, end close.
logger.warning("No session log file found for this project.") """
return False self.logData = []
self.listBox.clear()
self.timeFilter = 0.0
self.timeTotal = 0.0
winWidth = self.mainConf.rpxInt(self.width())
winHeight = self.mainConf.rpxInt(self.height())
widthCol0 = self.mainConf.rpxInt(self.listBox.columnWidth(0))
widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1))
widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2))
sortCol = self.listBox.sortColumn()
sortOrder = self.listBox.header().sortIndicatorOrder()
incNovel = self.incNovel.isChecked()
incNotes = self.incNotes.isChecked()
hideZeros = self.hideZeros.isChecked() hideZeros = self.hideZeros.isChecked()
hideNegative = self.hideNegative.isChecked() hideNegative = self.hideNegative.isChecked()
groupByDay = self.groupByDay.isChecked()
self.optState.setValue("GuiSessionLog", "winWidth", winWidth)
self.optState.setValue("GuiSessionLog", "winHeight", winHeight)
self.optState.setValue("GuiSessionLog", "widthCol0", widthCol0)
self.optState.setValue("GuiSessionLog", "widthCol1", widthCol1)
self.optState.setValue("GuiSessionLog", "widthCol2", widthCol2)
self.optState.setValue("GuiSessionLog", "sortCol", sortCol)
self.optState.setValue("GuiSessionLog", "sortOrder", sortOrder)
self.optState.setValue("GuiSessionLog", "incNovel", incNovel)
self.optState.setValue("GuiSessionLog", "incNotes", incNotes)
self.optState.setValue("GuiSessionLog", "hideZeros", hideZeros)
self.optState.setValue("GuiSessionLog", "hideNegative", hideNegative)
self.optState.setValue("GuiSessionLog", "groupByDay", groupByDay)
self.optState.saveSettings()
self.close()
return
def _saveData(self, dataFmt):
"""Save the content of the list box to a file.
"""
fileExt = ""
textFmt = ""
if dataFmt == self.FMT_JSON:
fileExt = "json"
textFmt = "JSON Data File"
elif dataFmt == self.FMT_CSV:
fileExt = "csv"
textFmt = "CSV Data File"
else:
return False
# Generate the file name
if fileExt:
fileName = "sessionStats.%s" % fileExt
saveDir = self.mainConf.lastPath
savePath = path.join(saveDir, fileName)
if not path.isdir(saveDir):
saveDir = self.mainConf.homePath
if self.mainConf.showGUI:
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog
saveTo = QFileDialog.getSaveFileName(
self, "Save Document As", savePath, options=dlgOpt
)
if saveTo[0]:
savePath = saveTo[0]
else:
return False
self.mainConf.setLastPath(savePath)
else:
return False
# Do the actual writing
wSuccess = False
errMsg = ""
logger.debug("Loading session log file")
try: try:
with open(logFile,mode="r",encoding="utf8") as inFile: with open(savePath, mode="w", encoding="utf8") as outFile:
if dataFmt == self.FMT_JSON:
jsonData = []
for _, sD, tT, wD, wA, wB in self.filterData:
jsonData.append({
"date": sD,
"length": tT,
"newWords": wD,
"novelWords": wA,
"noteWords": wB,
})
outFile.write(json.dumps(jsonData, indent=2))
wSuccess = True
elif dataFmt == self.FMT_CSV:
outFile.write(
"\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n" % (
"Date", "Length (sec)", "Words Changed", "Novel Words", "Note Words"
)
)
for _, sD, tT, wD, wA, wB in self.filterData:
outFile.write(
"\"%s\",%d,%d,%d,%d\n" % (sD, tT, wD, wA, wB)
)
wSuccess = True
else:
errMsg = "Unknown format"
except Exception as e:
errMsg = str(e)
# Report to user
if self.mainConf.showGUI:
if wSuccess:
self.theParent.makeAlert(
"%s file successfully written to:<br> %s" % (
textFmt, savePath
), nwAlert.INFO
)
else:
self.theParent.makeAlert(
"Failed to write %s file. %s" % (
textFmt, errMsg
), nwAlert.ERROR
)
return True
##
# Internal Functions
##
def _loadLogFile(self):
"""Load the content of the log file into a buffer.
"""
logger.debug("Loading session log file")
self.logData = []
ttNovel = 0
ttNotes = 0
ttTime = 0
try:
logFile = path.join(self.theProject.projMeta, nwFiles.SESS_STATS)
with open(logFile, mode="r", encoding="utf8") as inFile:
for inLine in inFile: for inLine in inFile:
inData = inLine.split() if inLine.startswith("#"):
if len(inData) != 8: if inLine.startswith("# Offset"):
self.wordOffset = int(inLine[9:].strip())
logger.verbose(
"Initial word count when log was started is %d" % self.wordOffset
)
continue continue
inData = inLine.split()
if len(inData) != 6:
continue
dStart = datetime.strptime( dStart = datetime.strptime(
"%s %s" % (inData[1],inData[2]), nwConst.tStampFmt "%s %s" % (inData[0], inData[1]), nwConst.tStampFmt
) )
dEnd = datetime.strptime( dEnd = datetime.strptime(
"%s %s" % (inData[4],inData[5]), nwConst.tStampFmt "%s %s" % (inData[2], inData[3]), nwConst.tStampFmt
) )
nWords = int(inData[7])
tDiff = dEnd - dStart tDiff = dEnd - dStart
sDiff = tDiff.total_seconds() sDiff = tDiff.total_seconds()
ttTime += sDiff
self.timeTotal += sDiff wcNovel = int(inData[4])
if abs(nWords) > 0: wcNotes = int(inData[5])
self.timeFilter += sDiff ttNovel = wcNovel
ttNotes = wcNotes
if hideZeros and nWords == 0: self.logData.append((dStart, sDiff, wcNovel, wcNotes))
continue
if hideNegative and nWords < 0:
continue
newItem = QTreeWidgetItem(
[str(dStart), self._formatTime(sDiff), str(nWords), ""]
)
newItem.setTextAlignment(1,Qt.AlignRight)
newItem.setTextAlignment(2,Qt.AlignRight)
newItem.setFont(0,self.monoFont)
newItem.setFont(1,self.monoFont)
newItem.setFont(2,self.monoFont)
self.listBox.addTopLevelItem(newItem)
except Exception as e: except Exception as e:
self.theParent.makeAlert( self.theParent.makeAlert(
@@ -213,47 +431,126 @@ class GuiSessionLogView(QDialog):
) )
return False return False
self.labelFilter.setText(self._formatTime(self.timeFilter)) self.labelTotal.setText(self._formatTime(ttTime))
self.labelTotal.setText(self._formatTime(self.timeTotal)) self.novelWords.setText("{:n}".format(ttNovel))
self.notesWords.setText("{:n}".format(ttNotes))
self.totalWords.setText("{:n}".format(ttNovel + ttNotes))
return True return True
def _doClose(self): def _updateListBox(self):
"""Load/reload the content of the list box.
"""
self.listBox.clear()
self.timeFilter = 0.0
widthCol0 = self.mainConf.rpxInt(self.listBox.columnWidth(0)) incNovel = self.incNovel.isChecked()
widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1)) incNotes = self.incNotes.isChecked()
widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2))
sortCol = self.listBox.sortColumn()
sortOrder = self.listBox.header().sortIndicatorOrder()
hideZeros = self.hideZeros.isChecked() hideZeros = self.hideZeros.isChecked()
hideNegative = self.hideNegative.isChecked() hideNegative = self.hideNegative.isChecked()
groupByDay = self.groupByDay.isChecked()
self.optState.setValue("GuiSession", "widthCol0", widthCol0) # Group the data
self.optState.setValue("GuiSession", "widthCol1", widthCol1) if groupByDay:
self.optState.setValue("GuiSession", "widthCol2", widthCol2) tempData = []
self.optState.setValue("GuiSession", "sortCol", sortCol) sessDate = None
self.optState.setValue("GuiSession", "sortOrder", sortOrder) sessTime = 0
self.optState.setValue("GuiSession", "hideZeros", hideZeros) lstNovel = 0
self.optState.setValue("GuiSession", "hideNegative", hideNegative) lstNotes = 0
self.optState.saveSettings() for n, (dStart, sDiff, wcNovel, wcNotes) in enumerate(self.logData):
self.close() if n == 0:
sessDate = dStart.date()
if sessDate != dStart.date():
tempData.append((sessDate, sessTime, lstNovel, lstNotes))
sessDate = dStart.date()
sessTime = sDiff
lstNovel = wcNovel
lstNotes = wcNotes
else:
sessTime += sDiff
lstNovel = wcNovel
lstNotes = wcNotes
return if sessDate is not None:
tempData.append((sessDate, sessTime, lstNovel, lstNotes))
def _doHideZeros(self, newState): else:
self._loadSessionLog() tempData = self.logData
return
def _doHideNegative(self, newState): # Calculate Word Diff
self._loadSessionLog() self.filterData = []
return pcTotal = 0
listMax = 0
isFirst = True
for dStart, sDiff, wcNovel, wcNotes in tempData:
wcTotal = 0
if incNovel:
wcTotal += wcNovel
if incNotes:
wcTotal += wcNotes
dwTotal = wcTotal - pcTotal
if hideZeros and dwTotal == 0:
continue
if hideNegative and dwTotal < 0:
continue
if isFirst:
# Subtract the offset from the first list entry
dwTotal -= self.wordOffset
dwTotal = max(dwTotal, 1) # Don't go zero or negative
isFirst = False
if groupByDay:
sStart = dStart.strftime(nwConst.dStampFmt)
else:
sStart = dStart.strftime(nwConst.tStampFmt)
self.filterData.append((dStart, sStart, sDiff, dwTotal, wcNovel, wcNotes))
listMax = max(listMax, dwTotal)
pcTotal = wcTotal
# Populate the list
for _, sStart, sDiff, nWords, _, _ in self.filterData:
newItem = QTreeWidgetItem()
newItem.setText(self.C_TIME, sStart)
newItem.setText(self.C_LENGTH, self._formatTime(sDiff))
newItem.setText(self.C_COUNT, "{:n}".format(nWords))
if nWords > 0 and listMax > 0:
theBar = self.barImage.scaled(
int(200*nWords/listMax),
self.barHeight,
Qt.IgnoreAspectRatio,
Qt.FastTransformation
)
newItem.setData(self.C_BAR, Qt.DecorationRole, theBar)
newItem.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
newItem.setTextAlignment(self.C_BAR, Qt.AlignLeft | Qt.AlignVCenter)
newItem.setFont(self.C_TIME, self.monoFont)
newItem.setFont(self.C_LENGTH, self.monoFont)
newItem.setFont(self.C_COUNT, self.monoFont)
self.listBox.addTopLevelItem(newItem)
self.timeFilter += sDiff
self.labelFilter.setText(self._formatTime(self.timeFilter))
return True
def _formatTime(self, tS): def _formatTime(self, tS):
"""Format the time spent in 00:00:00 format.
"""
tM = int(tS/60) tM = int(tS/60)
tH = int(tM/60) tH = int(tM/60)
tM = tM - tH*60 tM = tM - tH*60
tS = tS - tM*60 - tH*3600 tS = tS - tM*60 - tH*3600
return "%02d:%02d:%02d" % (tH,tM,tS) return "%02d:%02d:%02d" % (tH,tM,tS)
# END Class GuiSessionLogView # END Class GuiSessionLog
+2
View File
@@ -139,6 +139,8 @@ class GuiTheme:
logger.verbose("GUI Scale: %.2f" % self.guiScale) logger.verbose("GUI Scale: %.2f" % self.guiScale)
self.guiFont = qApp.font() self.guiFont = qApp.font()
self.guiFontFixed = QFontDatabase.systemFont(QFontDatabase.FixedFont)
qMetric = QFontMetrics(self.guiFont) qMetric = QFontMetrics(self.guiFont)
self.fontPointSize = self.guiFont.pointSizeF() self.fontPointSize = self.guiFont.pointSizeF()
self.fontPixelSize = int(round(qMetric.height())) self.fontPixelSize = int(round(qMetric.height()))
+55 -22
View File
@@ -43,7 +43,7 @@ from nw.gui import (
GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails, GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails,
GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus, GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus,
GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, GuiTheme, GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, GuiTheme,
GuiProjectSettings, GuiProjectTree, GuiSessionLogView GuiProjectSettings, GuiProjectTree, GuiSessionLog
) )
from nw.core import NWProject, NWDoc, NWIndex from nw.core import NWProject, NWDoc, NWIndex
from nw.constants import nwFiles, nwItemType, nwAlert from nw.constants import nwFiles, nwItemType, nwAlert
@@ -419,7 +419,7 @@ class GuiMain(QMainWindow):
# Restore previously open documents, if any # Restore previously open documents, if any
if self.theProject.lastEdited is not None: if self.theProject.lastEdited is not None:
self.openDocument(self.theProject.lastEdited) self.openDocument(self.theProject.lastEdited, doScroll=True)
if self.theProject.lastViewed is not None: if self.theProject.lastViewed is not None:
self.viewDocument(self.theProject.lastViewed) self.viewDocument(self.theProject.lastViewed)
@@ -463,7 +463,7 @@ class GuiMain(QMainWindow):
self.docEditor.clearEditor() self.docEditor.clearEditor()
return True return True
def openDocument(self, tHandle, tLine=None, changeFocus=True): def openDocument(self, tHandle, tLine=None, changeFocus=True, doScroll=False):
"""Open a specific document, optionally at a given line. """Open a specific document, optionally at a given line.
""" """
if self.hasProject: if self.hasProject:
@@ -473,7 +473,7 @@ class GuiMain(QMainWindow):
if changeFocus: if changeFocus:
self.docEditor.setFocus() self.docEditor.setFocus()
self.theProject.setLastEdited(tHandle) self.theProject.setLastEdited(tHandle)
self.treeView.setSelectedHandle(tHandle) self.treeView.setSelectedHandle(tHandle, doScroll=doScroll)
else: else:
return False return False
return True return True
@@ -501,10 +501,10 @@ class GuiMain(QMainWindow):
break break
if nHandle is not None: if nHandle is not None:
self.openDocument(nHandle, tLine=0) self.openDocument(nHandle, tLine=0, doScroll=True)
return True return True
elif wrapAround: elif wrapAround:
self.openDocument(fHandle, tLine=0) self.openDocument(fHandle, tLine=0, doScroll=True)
return False return False
return False return False
@@ -520,20 +520,30 @@ class GuiMain(QMainWindow):
"""Load a document for viewing in the view panel. """Load a document for viewing in the view panel.
""" """
if tHandle is None: if tHandle is None:
tHandle = self.treeView.getSelectedHandle() logger.debug("Viewing document, but no handle provided")
if tHandle is None:
logger.debug("No document selected, trying editor document") if self.docEditor.hasFocus():
tHandle = self.docEditor.theHandle logger.verbose("Trying editor document")
if tHandle is None: tHandle = self.docEditor.theHandle
logger.debug("No document selected, trying last viewed")
tHandle = self.theProject.lastViewed if tHandle is not None:
if tHandle is None: self.saveDocument()
logger.debug("No document selected, giving up") else:
return False 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 # Make sure main tab is in Editor view
self.tabWidget.setCurrentWidget(self.splitDocs) self.tabWidget.setCurrentWidget(self.splitDocs)
logger.debug("Viewing document with handle %s" % tHandle)
if self.docViewer.loadText(tHandle): if self.docViewer.loadText(tHandle):
if not self.splitView.isVisible(): if not self.splitView.isVisible():
bPos = self.splitMain.sizes() bPos = self.splitMain.sizes()
@@ -648,7 +658,7 @@ class GuiMain(QMainWindow):
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.projTree[tHandle]
if nwItem.itemType == nwItemType.FILE: if nwItem.itemType == nwItemType.FILE:
logger.verbose("Requested item %s is a file" % tHandle) logger.verbose("Requested item %s is a file" % tHandle)
self.openDocument(tHandle) self.openDocument(tHandle, doScroll=False)
else: else:
logger.verbose("Requested item %s is not a file" % tHandle) logger.verbose("Requested item %s is not a file" % tHandle)
@@ -802,7 +812,7 @@ class GuiMain(QMainWindow):
"""Open the session log dialog. """Open the session log dialog.
""" """
if self.hasProject: if self.hasProject:
dlgTLine = GuiSessionLogView(self, self.theProject) dlgTLine = GuiSessionLog(self, self.theProject)
dlgTLine.exec_() dlgTLine.exec_()
return True return True
@@ -968,12 +978,15 @@ class GuiMain(QMainWindow):
"""Connect to the main window all menu actions that need to be """Connect to the main window all menu actions that need to be
available also when the main menu is hidden. available also when the main menu is hidden.
""" """
# Project
self.addAction(self.mainMenu.aSaveProject) self.addAction(self.mainMenu.aSaveProject)
self.addAction(self.mainMenu.aExitNW) self.addAction(self.mainMenu.aExitNW)
# Document
self.addAction(self.mainMenu.aSaveDoc) self.addAction(self.mainMenu.aSaveDoc)
self.addAction(self.mainMenu.aFileDetails) 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.aEditUndo)
self.addAction(self.mainMenu.aEditRedo) self.addAction(self.mainMenu.aEditRedo)
self.addAction(self.mainMenu.aEditCut) self.addAction(self.mainMenu.aEditCut)
@@ -981,6 +994,17 @@ class GuiMain(QMainWindow):
self.addAction(self.mainMenu.aEditPaste) self.addAction(self.mainMenu.aEditPaste)
self.addAction(self.mainMenu.aSelectAll) self.addAction(self.mainMenu.aSelectAll)
self.addAction(self.mainMenu.aSelectPar) self.addAction(self.mainMenu.aSelectPar)
# Insert
self.addAction(self.mainMenu.aInsENDash)
self.addAction(self.mainMenu.aInsEMDash)
self.addAction(self.mainMenu.aInsEllipsis)
self.addAction(self.mainMenu.aInsHardBreak)
self.addAction(self.mainMenu.aInsNBSpace)
self.addAction(self.mainMenu.aInsThinSpace)
self.addAction(self.mainMenu.aInsThinNBSpace)
# Format
self.addAction(self.mainMenu.aFmtItalic) self.addAction(self.mainMenu.aFmtItalic)
self.addAction(self.mainMenu.aFmtBold) self.addAction(self.mainMenu.aFmtBold)
self.addAction(self.mainMenu.aFmtBoldIt) self.addAction(self.mainMenu.aFmtBoldIt)
@@ -992,10 +1016,19 @@ class GuiMain(QMainWindow):
self.addAction(self.mainMenu.aFmtHead4) self.addAction(self.mainMenu.aFmtHead4)
self.addAction(self.mainMenu.aFmtComment) self.addAction(self.mainMenu.aFmtComment)
self.addAction(self.mainMenu.aFmtNoFormat) 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.aSpellCheck)
self.addAction(self.mainMenu.aReRunSpell) self.addAction(self.mainMenu.aReRunSpell)
self.addAction(self.mainMenu.aPreferences) self.addAction(self.mainMenu.aPreferences)
# Help
self.addAction(self.mainMenu.aHelp) self.addAction(self.mainMenu.aHelp)
return True return True
def _setWindowTitle(self, projName=None): def _setWindowTitle(self, projName=None):
@@ -1070,7 +1103,7 @@ class GuiMain(QMainWindow):
if nwItem is not None: if nwItem is not None:
if nwItem.itemType == nwItemType.FILE: if nwItem.itemType == nwItemType.FILE:
logger.verbose("Requested item %s is a file" % tHandle) logger.verbose("Requested item %s is a file" % tHandle)
self.openDocument(tHandle, changeFocus=False) self.openDocument(tHandle, changeFocus=False, doScroll=False)
else: else:
logger.verbose("Requested item %s is a folder" % tHandle) logger.verbose("Requested item %s is a folder" % tHandle)
return return
@@ -1086,7 +1119,7 @@ class GuiMain(QMainWindow):
if nwItem is not None: if nwItem is not None:
if nwItem.itemType == nwItemType.FILE: if nwItem.itemType == nwItemType.FILE:
logger.verbose("Requested item %s is a file" % tHandle) logger.verbose("Requested item %s is a file" % tHandle)
self.openDocument(tHandle, changeFocus=False) self.openDocument(tHandle, changeFocus=False, doScroll=False)
else: else:
logger.verbose("Requested item %s is a folder" % tHandle) logger.verbose("Requested item %s is a folder" % tHandle)
return return
+4 -2
View File
@@ -13,10 +13,12 @@ In addition, the editor supports automatic formatting of “quotes”, both doub
If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, lets auto-replace this A with <A>, and this C with <C>. While <E> is just <E>. Press Ctrl+R to see what this looks like in the view pane. 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, lets auto-replace this A with <A>, and this C with <C>. While <E> is just <E>. 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: 25kg.
#### Some Section Here #### Some Section Here
If you need to split a scene file up into further pieces, you can do so with the level four heading, like above. This is referred to as a section. If you need to split a scene file up into further pieces, you can do so with the level four heading, like above. This is referred to as a section.
Both scene and section titles can be left out of the final exported document. The formatting of titles can be selected from the export dialog. Both scene and section titles can be left out of the final exported document. The formatting of titles can be selected from the Build Novel Project dialog. You can also have them replaced with scene separators like “* * *”.
+15 -13
View File
@@ -1,25 +1,27 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.9.0rc1" hexVersion="0x000900c1" fileVersion="1.1" timeStamp="2020-06-13 10:26:35"> <novelWriterXML appVersion="0.10.0rc1" hexVersion="0x001000c1" fileVersion="1.2" timeStamp="2020-06-27 17:29:20">
<project> <project>
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
<author>Jane Smith</author> <author>Jane Smith</author>
<author>Jay Doh</author> <author>Jay Doh</author>
<saveCount>332</saveCount> <saveCount>593</saveCount>
<autoCount>56</autoCount> <autoCount>103</autoCount>
<editTime>10692</editTime> <editTime>24563</editTime>
</project> </project>
<settings> <settings>
<doBackup>False</doBackup> <doBackup>False</doBackup>
<spellCheck>True</spellCheck> <spellCheck>True</spellCheck>
<autoOutline>True</autoOutline> <autoOutline>True</autoOutline>
<lastEdited>636b6aa9b697b</lastEdited> <lastEdited>636b6aa9b697b</lastEdited>
<lastViewed>636b6aa9b697b</lastViewed> <lastViewed>b3e74dbc1f584</lastViewed>
<lastWordCount>927</lastWordCount> <lastWordCount>982</lastWordCount>
<novelWordCount>606</novelWordCount>
<notesWordCount>376</notesWordCount>
<autoReplace> <autoReplace>
<A>B</A> <entry key="A">B</entry>
<B>E</B> <entry key="B">E</entry>
<C>D</C> <entry key="C">D</entry>
</autoReplace> </autoReplace>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
@@ -114,10 +116,10 @@
<status>1st Draft</status> <status>1st Draft</status>
<exported>True</exported> <exported>True</exported>
<layout>SCENE</layout> <layout>SCENE</layout>
<charCount>1240</charCount> <charCount>1564</charCount>
<wordCount>223</wordCount> <wordCount>278</wordCount>
<paraCount>7</paraCount> <paraCount>8</paraCount>
<cursorPos>403</cursorPos> <cursorPos>1633</cursorPos>
</item> </item>
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a"> <item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
<name>Another Scene</name> <name>Another Scene</name>
+1 -1
View File
@@ -6,7 +6,7 @@ with open("README.md", "r") as inFile:
setuptools.setup( setuptools.setup(
name = "novelWriter", name = "novelWriter",
version = "0.9rc1", version = "0.10.0",
author = "Veronica Berglyd Olsen", author = "Veronica Berglyd Olsen",
author_email = "code@vkbo.net", author_email = "code@vkbo.net",
description = "A markdown-like document editor for writing novels", description = "A markdown-like document editor for writing novels",
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited> <lastEdited>None</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>31489056e0916</lastEdited> <lastEdited>31489056e0916</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>86</lastWordCount> <lastWordCount>86</lastWordCount>
<novelWordCount>59</novelWordCount>
<notesWordCount>27</notesWordCount>
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
+4 -2
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 21:07:03"> <novelWriterXML appVersion="0.10.0rc1" hexVersion="0x001000c1" fileVersion="1.1" timeStamp="2020-06-26 19:32:47">
<project> <project>
<name>Project Name</name> <name>Project Name</name>
<title>Project Title</title> <title>Project Title</title>
@@ -16,8 +16,10 @@
<lastEdited>None</lastEdited> <lastEdited>None</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace> <autoReplace>
<This>With This Stuff </This> <entry key="This">With This Stuff </entry>
</autoReplace> </autoReplace>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
+8 -6
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.8.0rc1" hexVersion="0x000800c1" fileVersion="1.1" timeStamp="2020-06-05 21:07:51"> <novelWriterXML appVersion="0.9.0rc1" hexVersion="0x000900c1" fileVersion="1.1" timeStamp="2020-06-18 22:17:32">
<project> <project>
<name>New Project</name> <name>New Project</name>
<title></title> <title></title>
@@ -11,9 +11,11 @@
<doBackup>True</doBackup> <doBackup>True</doBackup>
<spellCheck>False</spellCheck> <spellCheck>False</spellCheck>
<autoOutline>True</autoOutline> <autoOutline>True</autoOutline>
<lastEdited>None</lastEdited> <lastEdited>31489056e0916</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>59</lastWordCount>
<novelWordCount>59</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
@@ -57,9 +59,9 @@
<status>Note</status> <status>Note</status>
<exported>False</exported> <exported>False</exported>
<layout>PAGE</layout> <layout>PAGE</layout>
<charCount>0</charCount> <charCount>331</charCount>
<wordCount>0</wordCount> <wordCount>59</wordCount>
<paraCount>0</paraCount> <paraCount>2</paraCount>
<cursorPos>0</cursorPos> <cursorPos>0</cursorPos>
</item> </item>
<item handle="44cb730c42048" order="1" parent="None"> <item handle="44cb730c42048" order="1" parent="None">
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited> <lastEdited>None</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited> <lastEdited>None</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited> <lastEdited>None</lastEdited>
<lastViewed>None</lastViewed> <lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount> <lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
<title>%title%</title> <title>%title%</title>
+259 -4
View File
@@ -2,7 +2,7 @@
"""novelWriter Main GUI Class Tester """novelWriter Main GUI Class Tester
""" """
import nw, pytest, sys import nw, pytest, sys, json
from nwtools import * from nwtools import *
from os import path, unlink from os import path, unlink
@@ -10,7 +10,7 @@ from PyQt5.QtCore import Qt
from nw.gui import ( from nw.gui import (
GuiProjectSettings, GuiItemEditor, GuiAbout, GuiBuildNovel, GuiProjectSettings, GuiItemEditor, GuiAbout, GuiBuildNovel,
GuiDocMerge, GuiDocSplit GuiDocMerge, GuiDocSplit, GuiSessionLog
) )
from nw.constants import * from nw.constants import *
@@ -127,6 +127,11 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI.treeView.newTreeItem(nwItemType.FILE, None) nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
assert nwGUI.openSelectedItem() 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 # Type something into the document
nwGUI.setFocus(2) nwGUI.setFocus(2)
for c in "# Main Location": for c in "# Main Location":
@@ -273,6 +278,7 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI.mainConf.backupPath = nwTempGUI nwGUI.mainConf.backupPath = nwTempGUI
projEdit = GuiProjectSettings(nwGUI, nwGUI.theProject) projEdit = GuiProjectSettings(nwGUI, nwGUI.theProject)
projEdit.show()
qtbot.addWidget(projEdit) qtbot.addWidget(projEdit)
projEdit.tabMain.editName.setText("") projEdit.tabMain.editName.setText("")
@@ -358,6 +364,7 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
# Create new, save, open project # Create new, save, open project
nwGUI.theProject.projTree.setSeed(42) nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject(nwTempGUI, True) assert nwGUI.newProject(nwTempGUI, True)
assert nwGUI.openDocument("31489056e0916")
itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916") itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916")
qtbot.addWidget(itemEdit) qtbot.addWidget(itemEdit)
@@ -374,7 +381,6 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
itemEdit.editExport.setChecked(False) itemEdit.editExport.setChecked(False)
assert not itemEdit.editExport.isChecked() assert not itemEdit.editExport.isChecked()
itemEdit._doSave() itemEdit._doSave()
itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916") 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.editName.text() == "Just a Page"
assert itemEdit.editStatus.currentData() == "Note" assert itemEdit.editStatus.currentData() == "Note"
assert itemEdit.editLayout.currentData() == nwItemLayout.PAGE assert itemEdit.editLayout.currentData() == nwItemLayout.PAGE
itemEdit._doClose() 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) qtbot.wait(stepDelay)
assert nwGUI.saveProject() assert nwGUI.saveProject()
qtbot.wait(stepDelay) qtbot.wait(stepDelay)
@@ -396,6 +409,115 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI.closeMain() nwGUI.closeMain()
# qtbot.stopForInteraction() # qtbot.stopForInteraction()
@pytest.mark.gui
def testSessionLogExport(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
qtbot.wait(stepDelay)
assert nwGUI.openProject(nwTempGUI)
qtbot.wait(stepDelay)
nwGUI.mainConf.lastPath = nwTempGUI
sessLog = GuiSessionLog(nwGUI, nwGUI.theProject)
sessLog.show()
qtbot.wait(stepDelay)
assert sessLog._saveData(sessLog.FMT_CSV)
qtbot.wait(stepDelay)
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = path.join(nwTempGUI, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.loads(inFile.read())
assert len(jsonData) == 3
assert jsonData[0]["length"] > 0
assert jsonData[0]["newWords"] == 86
assert jsonData[0]["novelWords"] == 59
assert jsonData[0]["noteWords"] == 27
# No Novel Files
qtbot.mouseClick(sessLog.incNovel, Qt.LeftButton)
qtbot.wait(stepDelay)
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = path.join(nwTempGUI, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.loads(inFile.read())
assert len(jsonData) == 2
assert jsonData[0]["length"] > 0
assert jsonData[0]["newWords"] == 27
assert jsonData[0]["novelWords"] == 59
assert jsonData[0]["noteWords"] == 27
# No Note Files
qtbot.mouseClick(sessLog.incNovel, Qt.LeftButton)
qtbot.mouseClick(sessLog.incNotes, Qt.LeftButton)
qtbot.wait(stepDelay)
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = path.join(nwTempGUI, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.loads(inFile.read())
assert len(jsonData) == 3
assert jsonData[0]["length"] > 0
assert jsonData[0]["newWords"] == 59
assert jsonData[0]["novelWords"] == 59
assert jsonData[0]["noteWords"] == 27
# No Negative Entries
qtbot.mouseClick(sessLog.incNotes, Qt.LeftButton)
qtbot.mouseClick(sessLog.hideNegative, Qt.LeftButton)
qtbot.wait(stepDelay)
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = path.join(nwTempGUI, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.loads(inFile.read())
assert len(jsonData) == 1
# Un-hide Zero Entries
qtbot.mouseClick(sessLog.hideNegative, Qt.LeftButton)
qtbot.mouseClick(sessLog.hideZeros, Qt.LeftButton)
qtbot.wait(stepDelay)
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = path.join(nwTempGUI, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.loads(inFile.read())
assert len(jsonData) == 4
# Group by Day
qtbot.mouseClick(sessLog.groupByDay, Qt.LeftButton)
qtbot.wait(stepDelay)
assert sessLog._saveData(sessLog.FMT_JSON)
qtbot.wait(stepDelay)
jsonStats = path.join(nwTempGUI, "sessionStats.json")
with open(jsonStats, mode="r", encoding="utf-8") as inFile:
jsonData = json.loads(inFile.read())
# Check against both 1 and 2 as this can be 2 if test was started just before midnight.
# A failed test should in any case produce a 4
assert len(jsonData) in (1, 2)
# qtbot.stopForInteraction()
sessLog._doClose()
nwGUI.closeMain()
@pytest.mark.gui @pytest.mark.gui
def testAboutBox(qtbot, nwTempGUI, nwRef, nwTemp): def testAboutBox(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp]) nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
@@ -610,3 +732,136 @@ def testSplitTool(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp):
# qtbot.stopForInteraction() # qtbot.stopForInteraction()
nwGUI.closeMain() 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()