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:
precision: 2
round: down
round: up
range: "70...100"
status:
project: yes
patch: yes
project:
default:
threshold: 1%
patch: no
changes: no
parsers:
+50 -5
View File
@@ -1,20 +1,65 @@
# 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**
* 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.
* 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**
* 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**
* 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]
@@ -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.
* 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.
* 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 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.
+2 -2
View File
@@ -24,9 +24,9 @@ copyright = "2018-2020, Veronica Berglyd Olsen"
author = "Veronica Berglyd Olsen"
# The short X.Y version
version = "0.9"
version = "0.10.0"
# The full version, including alpha/beta/rc tags
release = "0.9.0rc1"
release = "0.10.0rc1"
# -- 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."
"``@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.
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.
A non-breaking space is inserted with :kbd:`Shift-Space`.
Alternatively, the user can press :kbd:`Ctrl-K, Return` to insert this.
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.
@@ -133,7 +139,6 @@ These are as following:
":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-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-W`", "Close the current document in the editor."
":kbd:`Ctrl-X`", "Cut selected text to clipboard."
@@ -174,3 +179,17 @@ These are as following:
.. note::
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"
__copyright__ = "Copyright 20182020, Veronica Berglyd Olsen"
__license__ = "GPLv3"
__version__ = "0.9.0rc1"
__hexversion__ = "0x000900c1"
__date__ = "2020-06-14"
__version__ = "0.10.0rc1"
__hexversion__ = "0x001000c1"
__date__ = "2020-06-26"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__status__ = "Pre-Release"
__url__ = "https://github.com/vkbo/novelWriter"
__issuesurl__ = "https://github.com/vkbo/novelWriter/issues"
__domain__ = "novelwriter.io"
__docurl__ = "https://novelwriter.readthedocs.io"
__credits__ = [
+34
View File
@@ -193,3 +193,37 @@ def transferCase(theSource, theTarget):
theResult = theTarget.lower()
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 -*-
from nw.constants.iso import isoLanguage, isoCountry
from nw.constants.constants import (
nwConst, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode
nwConst, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode,
nwInsertSymbols
)
from nw.constants.enum import (
nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline
nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline,
nwDocInsert
)
__all__ = [
@@ -17,10 +19,12 @@ __all__ = [
"nwLabels",
"nwQuotes",
"nwUnicode",
"nwInsertSymbols",
"nwAlert",
"nwDocAction",
"nwItemClass",
"nwItemLayout",
"nwItemType",
"nwOutline",
"nwDocInsert",
]
+55 -26
View File
@@ -25,12 +25,13 @@
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():
tStampFmt = "%Y-%m-%d %H:%M:%S" # Default format
fStampFmt = "%Y-%m-%d %H.%M.%S" # FileName safe format
dStampFmt = "%Y-%m-%d" # Date only format
# END Class nwConst
@@ -50,7 +51,7 @@ class nwFiles():
PROJ_LOCK = "nwProject.lock"
TOC_TXT = "ToC.txt"
TOC_JSON = "ToC.json"
SESS_INFO = "sessionInfo.log"
SESS_STATS = "sessionStats.log"
INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json"
RECENT_FILE = "recentProjects.json"
@@ -169,32 +170,35 @@ class nwQuotes():
Source: https://en.wikipedia.org/wiki/Quotation_mark
"""
SYMBOLS = [
"\u0022", # Quotation mark
"\u0027", # Apostrophe
"\u00ab", # Left-pointing double angle quotation mark
"\u00bb", # Right-pointing double angle quotation mark
"\u2018", # Left single quotation mark
"\u2019", # Right single quotation mark
"\u201a", # Single low-9 quotation mark
"\u201b", # Single high-reversed-9 quotation mark
"\u201c", # Left double quotation mark
"\u201d", # Right double quotation mark
"\u201e", # Double low-9 quotation mark
"\u201f", # Double high-reversed-9 quotation mark
"\u2039", # Single left-pointing angle quotation mark
"\u203a", # Single right-pointing angle quotation mark
"\u2e42", # Double low-reversed-9 quotation mark
"\u300c", # Left corner bracket
"\u300d", # Right corner bracket
"\u300e", # Left white corner bracket
"\u300f", # Right white corner bracket
]
SYMBOLS = {
"\u0027" : "Straight single quotation mark",
"\u0022" : "Straight double quotation mark",
"\u2018" : "Left single quotation mark",
"\u2019" : "Right single quotation mark",
"\u201a" : "Single low-9 quotation mark",
"\u201b" : "Single high-reversed-9 quotation mark",
"\u201c" : "Left double quotation mark",
"\u201d" : "Right double quotation mark",
"\u201e" : "Double low-9 quotation mark",
"\u201f" : "Double high-reversed-9 quotation mark",
"\u2e42" : "Double low-reversed-9 quotation mark",
"\u2039" : "Single left-pointing angle quotation mark",
"\u203a" : "Single right-pointing angle quotation mark",
"\u00ab" : "Left-pointing double angle quotation mark",
"\u00bb" : "Right-pointing double angle quotation mark",
"\u300c" : "Left corner bracket",
"\u300d" : "Right corner bracket",
"\u300e" : "Left white corner bracket",
"\u300f" : "Right white corner bracket",
}
# END Class nwQuotes
class nwUnicode:
"""Suppoted unicode character constants and translation maps for HTML.
"""Supported unicode character constants and translation maps for HTML.
"""
# Unicode Constants
@@ -226,8 +230,14 @@ class nwUnicode:
U_EMDASH = "\u2014" # Long dash
U_HELLIP = "\u2026" # Ellipsis
## Other
## Spaces and Lines
U_NBSP = "\u00a0" # Non-breaking space
U_THNSP = "\u2009" # Thin space
U_THNBSP = "\u202f" # Thin non-breaking space
U_LSEP = "\u2028" # Line separator
U_PSEP = "\u2029" # Paragraph separator
## Symbols
U_CHECK = "\u2714" # Heavy check mark
U_MULT = "\u2715" # Multiplication x
@@ -270,8 +280,12 @@ class nwUnicode:
H_EMDASH = "&mdash;"
H_HELLIP = "&hellip;"
## Other
## Spaces
H_NBSP = "&nbsp;"
H_THNSP = "&thinsp;"
H_THNBSP = "&#8239;"
## Symbols
H_CHECK = "&#10004;"
H_MULT = "&#10005;"
@@ -286,3 +300,18 @@ class nwUnicode:
H_LTRIS = "&#9666;"
# 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_COM = 23
BLOCK_TXT = 24
REPL_SNG = 25
REPL_DBL = 26
# END Enum nwDocAction
class nwDocInsert(Enum):
NO_INSERT = 0
HARD_BREAK = 1
NB_SPACE = 2
THIN_SPACE = 3
THIN_NB_SPACE = 4
SHORT_DASH = 5
LONG_DASH = 6
ELLIPSIS = 7
# END Enum nwDocInsert
class nwAlert(Enum):
INFO = 0
+11 -1
View File
@@ -45,14 +45,19 @@ class OptionState():
self.theState = {}
self.validMap = {
"GuiSession": {
"GuiSessionLog": {
"winWidth",
"winHeight",
"widthCol0",
"widthCol1",
"widthCol2",
"sortCol",
"sortOrder",
"incNovel",
"incNotes",
"hideZeros",
"hideNegative",
"groupByDay",
},
"GuiDocSplit": {
"spLevel",
@@ -78,6 +83,11 @@ class OptionState():
"columnWidth",
"columnHidden",
},
"GuiProjectSettings": {
"winWidth",
"winHeight",
"replaceColW",
}
}
return
+69 -24
View File
@@ -93,6 +93,8 @@ class NWProject():
self.lastViewed = None # The handle of the last file to be viewed
self.lastWCount = 0 # The project word count from last 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
# Set Defaults
@@ -231,6 +233,8 @@ class NWProject():
self.lastViewed = None
self.lastWCount = 0
self.currWCount = 0
self.novelWCount = 0
self.notesWCount = 0
return
@@ -352,6 +356,15 @@ class NWProject():
# 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":
msgBox = QMessageBox()
msgRes = msgBox.question(self.theParent, "Old Project Version", (
@@ -365,12 +378,14 @@ class NWProject():
if msgRes != QMessageBox.Yes:
return False
elif fileVersion != "1.1":
elif fileVersion != "1.1" and fileVersion != "1.2":
self.makeAlert((
"Unknown or unsupported %s project format. "
"The project cannot be opened by this version of %s."
) % (
nw.__package__, nw.__package__
"Unknown or unsupported {nw:s} project file format. "
"The project cannot be opened by this version of {nw:s}. "
"The file was saved with {nw:s} version {vers:s}."
).format(
nw = nw.__package__,
vers = appVersion,
), nwAlert.ERROR)
return False
@@ -435,13 +450,25 @@ class NWProject():
self.lastViewed = checkString(xItem.text, None, True)
elif xItem.tag == "lastWordCount":
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":
self.statusItems.unpackEntries(xItem)
elif xItem.tag == "importance":
self.importItems.unpackEntries(xItem)
elif xItem.tag == "autoReplace":
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":
titleFormat = self.titleFormat.copy()
for xEntry in xItem:
@@ -493,14 +520,18 @@ class NWProject():
# Root element and project details
logger.debug("Writing project meta")
nwXML = etree.Element("novelWriterXML",attrib={
nwXML = etree.Element("novelWriterXML", attrib={
"appVersion" : str(nw.__version__),
"hexVersion" : str(nw.__hexversion__),
"fileVersion" : "1.1",
"fileVersion" : "1.2",
"timeStamp" : formatTimeStamp(saveTime),
})
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
xProject = etree.SubElement(nwXML, "project")
@@ -519,11 +550,9 @@ class NWProject():
self._packProjectValue(xSettings, "lastEdited", self.lastEdited)
self._packProjectValue(xSettings, "lastViewed", self.lastViewed)
self._packProjectValue(xSettings, "lastWordCount", self.currWCount)
xAutoRep = etree.SubElement(xSettings, "autoReplace")
for aKey, aValue in self.autoReplace.items():
if len(aKey) > 0:
self._packProjectValue(xAutoRep, aKey, aValue)
self._packProjectValue(xSettings, "novelWordCount", wcNovel)
self._packProjectValue(xSettings, "notesWordCount", wcNotes)
self._packProjectKeyValue(xSettings, "autoReplace", self.autoReplace)
xTitleFmt = etree.SubElement(xSettings, "titleFormat")
for aKey, aValue in self.titleFormat.items():
@@ -1027,10 +1056,20 @@ class NWProject():
if not isinstance(aValue, str):
aValue = str(aValue)
if aValue == "" and not allowNone: continue
xItem = etree.SubElement(xParent,theName)
xItem = etree.SubElement(xParent, theName)
xItem.text = aValue
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):
"""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
@@ -1101,18 +1140,24 @@ class NWProject():
if not self.ensureFolderStructure():
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:
print((
"Start: {opened:s} "
"End: {closed:s} "
"Words: {words:8d}"
).format(
opened = formatTimeStamp(self.projOpened),
closed = formatTimeStamp(time()),
words = self.getSessionWordCount(),
), file=outFile)
if not isFile:
# It's a new file, so add a header
if self.lastWCount > 0:
outFile.write("# Offset %d\n" % self.lastWCount)
outFile.write("# %-17s %-19s %8s %8s\n" % (
"Start Time", "End Time", "Novel", "Notes"
))
outFile.write("%-19s %-19s %8d %8d\n" % (
formatTimeStamp(self.projOpened),
formatTimeStamp(time()),
self.novelWCount,
self.notesWCount,
))
return True
+2
View File
@@ -54,6 +54,8 @@ class ToHtml(Tokenizer):
nwUnicode.U_EMDASH : nwUnicode.H_EMDASH,
nwUnicode.U_HELLIP : nwUnicode.H_HELLIP,
nwUnicode.U_NBSP : nwUnicode.H_NBSP,
nwUnicode.U_THNSP : nwUnicode.H_THNSP,
nwUnicode.U_THNBSP : nwUnicode.H_THNBSP,
}
self.revDict = {}
self.reReplace = []
+18 -1
View File
@@ -36,7 +36,7 @@ from time import time
from nw.core.item import NWItem
from nw.common import checkString
from nw.constants import nwFiles, nwItemType, nwItemClass
from nw.constants import nwFiles, nwItemType, nwItemClass, nwItemLayout
logger = logging.getLogger(__name__)
@@ -182,6 +182,23 @@ class NWTree():
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
##
+2 -2
View File
@@ -15,7 +15,7 @@ from nw.gui.preferences import GuiPreferences
from nw.gui.projload import GuiProjectLoad
from nw.gui.projsettings import GuiProjectSettings
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.theme import GuiIcons, GuiTheme
@@ -36,7 +36,7 @@ __all__ = [
"GuiProjectLoad",
"GuiProjectSettings",
"GuiProjectTree",
"GuiSessionLogView",
"GuiSessionLog",
"GuiMainStatus",
"GuiIcons",
"GuiTheme",
+95 -12
View File
@@ -31,8 +31,9 @@ import nw
from os import path
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.QtGui import (
QPalette, QColor, QTextDocumentWriter, QFont
@@ -43,6 +44,7 @@ from PyQt5.QtWidgets import (
QFileDialog, QFontDialog, QSpinBox
)
from nw.common import fuzzyTime
from nw.gui.custom import QSwitch
from nw.core import ToHtml
from nw.constants import (
@@ -76,6 +78,7 @@ class GuiBuildNovel(QDialog):
self.htmlText = [] # List of html document
self.htmlStyle = [] # List of html styles
self.nwdText = [] # List of markdown documents
self.buildTime = 0 # The timestamp of the last build
self.setWindowTitle("Build Novel Project")
self.setMinimumWidth(self.mainConf.pxInt(900))
@@ -411,11 +414,12 @@ class GuiBuildNovel(QDialog):
self.docView.clearStyleSheet()
else:
self.docView.setStyleSheet(self.htmlStyle)
self.docView.setContent(self.htmlText)
self.docView.setContent(self.htmlText, self.buildTime)
else:
self.htmlText = []
self.htmlStyle = []
self.nwdText = []
self.buildTime = 0
return
@@ -509,6 +513,7 @@ class GuiBuildNovel(QDialog):
tEnd = time()
logger.debug("Built project in %.3f ms" % (1000*(tEnd-tStart)))
self.htmlStyle = makeHtml.getStyleSheet()
self.buildTime = tEnd
# Load the preview document with the html data
self.docView.setTextFont(textFont, textSize)
@@ -517,7 +522,7 @@ class GuiBuildNovel(QDialog):
self.docView.clearStyleSheet()
else:
self.docView.setStyleSheet(self.htmlStyle)
self.docView.setContent(self.htmlText)
self.docView.setContent(self.htmlText, self.buildTime)
self._saveCache()
@@ -563,9 +568,6 @@ class GuiBuildNovel(QDialog):
def _saveDocument(self, theFormat):
"""Save the document to various formats.
"""
# FMT_PDF
byteFmt = QByteArray()
fileExt = ""
textFmt = ""
@@ -694,6 +696,7 @@ class GuiBuildNovel(QDialog):
"workingTitle" : self.theProject.projName,
"novelTitle" : self.theProject.bookTitle,
"authors" : self.theProject.bookAuthors,
"buildTime" : self.buildTime,
}
}
@@ -808,6 +811,8 @@ class GuiBuildNovel(QDialog):
if "nwdText" in theData.keys():
self.nwdText = theData["nwdText"]
dataCount += 1
if "buildTime" in theData.keys():
self.buildTime = theData["buildTime"]
return dataCount == 3
@@ -828,6 +833,7 @@ class GuiBuildNovel(QDialog):
"htmlText" : self.htmlText,
"htmlStyle" : self.htmlStyle,
"nwdText" : self.nwdText,
"buildTime" : self.buildTime,
}, indent=nIndent))
except Exception as e:
logger.error("Failed to save build cache")
@@ -871,8 +877,8 @@ class GuiBuildNovel(QDialog):
"section" : self.fmtSection.text().strip(),
})
winWidth = self.mainConf.pxInt(self.width())
winHeight = self.mainConf.pxInt(self.height())
winWidth = self.mainConf.rpxInt(self.width())
winHeight = self.mainConf.rpxInt(self.height())
justifyText = self.justifyText.isChecked()
noStyling = self.noStyling.isChecked()
textFont = self.textFont.text()
@@ -924,12 +930,14 @@ class GuiBuildNovelDocView(QTextBrowser):
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
self.theTheme = theParent.theTheme
self.buildTime = 0
self.setMinimumWidth(40*self.theParent.theTheme.textNWidth)
self.setOpenExternalLinks(False)
self.qDocument = self.document()
self.qDocument.setDocumentMargin(self.mainConf.textMargin)
self.qDocument.setDocumentMargin(self.mainConf.getTextMargin())
self.setPlaceholderText(
"This area will show the content of the document to be "
"exported or printed. Press the \"Build Novel Project\" "
@@ -946,12 +954,35 @@ class GuiBuildNovelDocView(QTextBrowser):
docPalette = self.palette()
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)
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.show()
# Age Timer
self.ageTimer = QTimer()
self.ageTimer.setInterval(10000)
self.ageTimer.timeout.connect(self._updateBuildAge)
self.ageTimer.start()
logger.debug("GuiBuildNovelDocView initialisation complete")
@@ -977,15 +1008,20 @@ class GuiBuildNovelDocView(QTextBrowser):
self.setFont(theFont)
return
def setContent(self, theText):
def setContent(self, theText, timeStamp):
"""Set the content, either from text or list of text.
"""
if isinstance(theText, list):
theText = "".join(theText)
self.buildTime = timeStamp
theText = theText.replace("&emsp;", "&nbsp;"*4)
theText = theText.replace("<del>", "<span style='text-decoration: line-through;'>")
theText = theText.replace("</del>", "</span>")
self.setHtml(theText)
self._updateBuildAge()
return
def setStyleSheet(self, theStyles=[]):
@@ -1007,4 +1043,51 @@ class GuiBuildNovelDocView(QTextBrowser):
self.qDocument.setDefaultStyleSheet("")
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
+100 -4
View File
@@ -30,17 +30,17 @@
import logging
import nw
from PyQt5.QtGui import QColor, QPalette, QPainter
from PyQt5.QtGui import QColor, QPalette, QPainter, QFontMetrics
from PyQt5.QtCore import (
Qt, QRect, QPoint, QSize, QRectF, QPropertyAnimation, pyqtProperty
)
from PyQt5.QtWidgets import (
QGridLayout, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QSizePolicy,
QAbstractButton, QDialog, QTabWidget, QTabBar, QStyle,
QStylePainter, QStyleOptionTab
QAbstractButton, QDialog, QTabWidget, QTabBar, QStyle, QDialogButtonBox,
QStylePainter, QStyleOptionTab, QListWidget, QListWidgetItem, QFrame
)
from nw.constants import nwUnicode
from nw.constants import nwUnicode, nwQuotes
logger = logging.getLogger(__name__)
@@ -436,3 +436,99 @@ class VerticalTabBar(QTabBar):
return
# END Class VerticalTabBar
# =============================================================================================== #
# Quotes Dialog
# =============================================================================================== #
class QuotesDialog(QDialog):
def __init__(self, theParent=None, currentQuote="\""):
QDialog.__init__(self, parent=theParent)
self.mainConf = nw.CONFIG
self.outerBox = QVBoxLayout()
self.innerBox = QHBoxLayout()
self.labelBox = QVBoxLayout()
self.selectedQuote = currentQuote
qMetrics = QFontMetrics(self.font())
pxW = 7*qMetrics.boundingRectChar("M").width()
pxH = 7*qMetrics.boundingRectChar("M").height()
pxH = 7*qMetrics.boundingRectChar("M").height()
lblFont = self.font()
lblFont.setPointSizeF(4*lblFont.pointSizeF())
# Preview Label
self.previewLabel = QLabel(currentQuote)
self.previewLabel.setFont(lblFont)
self.previewLabel.setFixedSize(QSize(pxW, pxH))
self.previewLabel.setAlignment(Qt.AlignCenter)
self.previewLabel.setFrameStyle(QFrame.Box | QFrame.Plain)
# Quote Symbols
self.listBox = QListWidget()
self.listBox.itemSelectionChanged.connect(self._selectedSymbol)
minSize = 100
for sKey, sLabel in nwQuotes.SYMBOLS.items():
theText = "[ %s ] %s" % (sKey, sLabel)
minSize = max(minSize, qMetrics.boundingRect(theText).width())
qtItem = QListWidgetItem(theText)
qtItem.setData(Qt.UserRole, sKey)
self.listBox.addItem(qtItem)
if sKey == currentQuote:
self.listBox.setCurrentItem(qtItem)
self.listBox.setMinimumWidth(minSize + self.mainConf.pxInt(40))
self.listBox.setMinimumHeight(self.mainConf.pxInt(150))
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doAccept)
self.buttonBox.rejected.connect(self._doReject)
# Assemble
self.labelBox.addWidget(self.previewLabel, 0, Qt.AlignTop)
self.labelBox.addStretch(1)
self.innerBox.addLayout(self.labelBox)
self.innerBox.addWidget(self.listBox)
self.outerBox.addLayout(self.innerBox)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
return
##
# Slots
##
def _selectedSymbol(self):
"""Update the preview label and the selected quote style.
"""
selItems = self.listBox.selectedItems()
if selItems:
theSymbol = selItems[0].data(Qt.UserRole)
self.previewLabel.setText(theSymbol)
self.selectedQuote = theSymbol
return
def _doAccept(self):
"""Ok button clicked.
"""
self.accept()
return
def _doReject(self):
"""Cancel button clicked.
"""
self.reject()
return
# END Class QuotesDialog
+113 -54
View File
@@ -45,11 +45,12 @@ from PyQt5.QtWidgets import (
QFrame
)
from nw.core import NWDoc
from nw.core import NWDoc, NWSpellSimple, countWords
from nw.gui.dochighlight import GuiDocHighlighter
from nw.core import NWSpellSimple, countWords
from nw.constants import nwUnicode, nwDocAction, nwItemClass
from nw.common import transferCase
from nw.constants import (
nwAlert, nwUnicode, nwDocAction, nwDocInsert, nwInsertSymbols
)
logger = logging.getLogger(__name__)
@@ -76,6 +77,7 @@ class GuiDocEditor(QTextEdit):
self.paraCount = 0
self.lastEdit = 0
self.bigDoc = False
self.doReplace = False
self.nonWord = "\"'"
# Typography
@@ -86,7 +88,6 @@ class GuiDocEditor(QTextEdit):
# Core Elements
self.qDocument = self.document()
self.qDocument.setDocumentMargin(self.mainConf.getTextMargin())
self.qDocument.contentsChange.connect(self._docChange)
# Document Title
@@ -101,8 +102,7 @@ class GuiDocEditor(QTextEdit):
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._openContextMenu)
# Editor State
self.hasSelection = False
# Editor Settings
self.setMinimumWidth(self.mainConf.pxInt(300))
self.setAutoFillBackground(True)
self.setAcceptRichText(False)
@@ -157,8 +157,7 @@ class GuiDocEditor(QTextEdit):
self.paraCount = 0
self.lastEdit = 0
self.bigDoc = False
self.hasSelection = False
self.doReplace = False
self.setDocumentChanged(False)
self.docHeader.setTitleFromHandle(self.theHandle)
@@ -261,7 +260,9 @@ class GuiDocEditor(QTextEdit):
self.hLight.spellCheck = False
bfTime = time()
self._allowAutoReplace(False)
self.setPlainText(theDoc)
self._allowAutoReplace(True)
afTime = time()
logger.debug("Document highlighted in %.3f milliseconds" % (1000*(afTime-bfTime)))
@@ -356,7 +357,11 @@ class GuiDocEditor(QTextEdit):
else:
rH = 0
<<<<<<< HEAD
self.setViewportMargins(tM, max(cM, tH, rH), tM, max(cM, fH))
=======
self.setViewportMargins(tM, max(cM, tH, rH), tM, cM)
>>>>>>> master
return
@@ -392,8 +397,8 @@ class GuiDocEditor(QTextEdit):
"""
if self.mainConf.verQtValue >= 50900:
theText = self.qDocument.toRawText()
theText = theText.replace("\u2028", "\n") # Line separators
theText = theText.replace("\u2029", "\n") # Paragraph separators
theText = theText.replace(nwUnicode.U_LSEP, "\n") # Line separators
theText = theText.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
else:
theText = self.toPlainText()
return theText
@@ -500,6 +505,7 @@ class GuiDocEditor(QTextEdit):
if not self.theParent.hasProject:
logger.error("No project open")
return False
self._allowAutoReplace(False)
if theAction == nwDocAction.UNDO:
self.undo()
elif theAction == nwDocAction.REDO:
@@ -548,9 +554,15 @@ class GuiDocEditor(QTextEdit):
self._formatBlock(nwDocAction.BLOCK_COM)
elif theAction == nwDocAction.BLOCK_TXT:
self._formatBlock(nwDocAction.BLOCK_TXT)
elif theAction == nwDocAction.REPL_SNG:
self._replaceQuotes("'", self.typSQOpen, self.typSQClose)
elif theAction == nwDocAction.REPL_DBL:
self._replaceQuotes("\"", self.typDQOpen, self.typDQClose)
else:
logger.debug("Unknown or unsupported document action %s" % str(theAction))
self._allowAutoReplace(True)
return False
self._allowAutoReplace(True)
return True
def isEmpty(self):
@@ -574,6 +586,21 @@ class GuiDocEditor(QTextEdit):
))
return
def insertText(self, theInsert):
"""Insert a specific type of text at the cursor position.
"""
if isinstance(theInsert, str):
theText = theInsert
elif theInsert in nwInsertSymbols.SYMBOLS:
theText = nwInsertSymbols.SYMBOLS[theInsert]
else:
return False
theCursor = self.textCursor()
theCursor.beginEditBlock()
theCursor.insertText(theText)
theCursor.endEditBlock()
return True
def closeSearch(self):
"""Close the search box.
"""
@@ -586,34 +613,23 @@ class GuiDocEditor(QTextEdit):
def keyPressEvent(self, keyEvent):
"""Intercept key press events.
We need to intercept key presses briefly to record the state of
selection. This is in order to know whether we had a selection
prior to triggering the _docChange slot, as we do not want to
trigger autoreplace on selections. Autoreplace on selections
messes with undo/redo history.
We also need to intercept the Shift key modifier for certain key
combinations that modifies standard keys like enter and space.
However, we don't want to spend a lot of time in this function
as it is triggered on every keypress when typing.
We need to intercept a few key sequences:
* The return key redirects here even if the search box has
focus. Since we need the return key to continue search, we
block any further interaction here while it's in focus.
* The undo sequence bypasses the doAction pathway from the
menu, so we redirect it back from here.
* The default redo sequence is Ctrl+Shift+Z, which we don't
use, so we block it.
"""
self.hasSelection = self.textCursor().hasSelection()
if self.docSearch.searchBox.hasFocus():
# Block the event when the focus is on the search bar.
return
if keyEvent.modifiers() == Qt.ShiftModifier:
theKey = keyEvent.key()
if theKey == Qt.Key_Return:
self._insertHardBreak()
elif theKey == Qt.Key_Enter:
self._insertHardBreak()
elif theKey == Qt.Key_Space:
self._insertNonBreakingSpace()
else:
QTextEdit.keyPressEvent(self, keyEvent)
elif keyEvent == QKeySequence.Redo:
return
elif keyEvent == QKeySequence.Undo:
self.docAction(nwDocAction.UNDO)
else:
QTextEdit.keyPressEvent(self, keyEvent)
return
def mouseReleaseEvent(self, mEvent):
@@ -649,7 +665,7 @@ class GuiDocEditor(QTextEdit):
self.setDocumentChanged(True)
if not self.wcTimer.isActive():
self.wcTimer.start()
if self.mainConf.doReplace and not self.hasSelection:
if self.doReplace and charsAdded == 1:
self._docAutoReplace(self.qDocument.findBlock(thePos))
return
@@ -800,24 +816,6 @@ class GuiDocEditor(QTextEdit):
return True
def _insertHardBreak(self):
"""Inserts a hard line break at the cursor position.
"""
theCursor = self.textCursor()
theCursor.beginEditBlock()
theCursor.insertText(" \n")
theCursor.endEditBlock()
return
def _insertNonBreakingSpace(self):
"""Inserts a non-breaking space at the cursor position.
"""
theCursor = self.textCursor()
theCursor.beginEditBlock()
theCursor.insertText(nwUnicode.U_NBSP)
theCursor.endEditBlock()
return
def _openSpellContext(self):
"""Opens the spell check context menu at the current point of
the cursor.
@@ -879,6 +877,58 @@ class GuiDocEditor(QTextEdit):
return
def _replaceQuotes(self, sQuote, oQuote, cQuote):
"""Replace all straight quotes in the selected text.
"""
theCursor = self.textCursor()
if theCursor.hasSelection():
posS = theCursor.selectionStart()
posE = theCursor.selectionEnd()
closeCheck = (
" ", "\n", nwUnicode.U_LSEP, nwUnicode.U_PSEP
)
self._allowAutoReplace(False)
for posC in range(posS, posE+1):
theCursor.setPosition(posC)
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2)
selText = theCursor.selectedText()
nS = len(selText)
if nS == 2:
pC = selText[0]
cC = selText[1]
elif nS == 1:
pC = " "
cC = selText[0]
else:
continue
if cC != sQuote:
continue
theCursor.clearSelection()
theCursor.setPosition(posC)
if pC in closeCheck:
theCursor.beginEditBlock()
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
theCursor.insertText(oQuote)
theCursor.endEditBlock()
else:
theCursor.beginEditBlock()
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 1)
theCursor.insertText(cQuote)
theCursor.endEditBlock()
self._allowAutoReplace(True)
else:
self.theParent.makeAlert(
"Please selection some text before calling replace quotes.", nwAlert.ERROR
)
return
def _checkDocSize(self, theSize):
"""Check if document size crosses the big document limit set in
config. If so, we will set the big document flag to True.
@@ -1196,10 +1246,10 @@ class GuiDocEditor(QTextEdit):
self._beginSearch()
return
theCursor = self.textCursor()
if not theCursor.hasSelection():
return
theCursor = self.textCursor()
searchFor = self.docSearch.getSearchText()
replWith = self.docSearch.getReplaceText()
selText = theCursor.selectedText()
@@ -1242,6 +1292,15 @@ class GuiDocEditor(QTextEdit):
return
def _allowAutoReplace(self, theState):
"""used to enable/disable the auto-replace feature temporarily.
"""
if theState:
self.doReplace = self.mainConf.doReplace
else:
self.doReplace = False
return
# END Class GuiDocEditor
# =============================================================================================== #
@@ -1770,7 +1829,7 @@ class GuiDocEditHeader(QWidget):
"""Capture a click on the title and ensure that the item is
selected in the project tree.
"""
self.theParent.treeView.setSelectedHandle(self.theHandle)
self.theParent.treeView.setSelectedHandle(self.theHandle, doScroll=True)
return
# END Class GuiDocEditHeader
+1 -1
View File
@@ -132,7 +132,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
# Non-Breaking Spaces
self.hRules.append((
"[%s]+" % nwUnicode.U_NBSP, {
"[%s%s]+" % (nwUnicode.U_NBSP, nwUnicode.U_THNBSP), {
0 : self.hStyles["nobreak"],
}
))
+1 -1
View File
@@ -120,7 +120,7 @@ class GuiDocMerge(QDialog):
self.theParent.treeView.revealTreeItem(nHandle)
theDoc.openDocument(nHandle, False)
theDoc.saveDocument(theText)
self.theParent.openDocument(nHandle)
self.theParent.openDocument(nHandle, doScroll=True)
self.close()
+8 -13
View File
@@ -60,6 +60,7 @@ class GuiDocViewer(QTextBrowser):
self.qDocument = self.document()
self.setMinimumWidth(self.mainConf.pxInt(300))
self.setAutoFillBackground(True)
self.setOpenExternalLinks(False)
self.initViewer()
@@ -104,11 +105,12 @@ class GuiDocViewer(QTextBrowser):
self.setFont(theFont)
docPalette = self.palette()
docPalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack))
docPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack))
docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText))
self.setPalette(docPalette)
self.qDocument.setDocumentMargin(self.mainConf.getTextMargin())
self.qDocument.setDocumentMargin(0)
theOpt = QTextOption()
if self.mainConf.doJustify:
theOpt.setAlignment(Qt.AlignJustify)
@@ -242,19 +244,10 @@ class GuiDocViewer(QTextBrowser):
tH = self.docHeader.height()
fH = self.docFooter.height()
fY = self.height() - fH - tB
tT = cM - tH
bT = cM - fH
self.docHeader.setGeometry(tB, tB, tW, tH)
self.docFooter.setGeometry(tB, fY, tW, fH)
self.setViewportMargins(0, tH, 0, fH)
docFormat = self.qDocument.rootFrame().frameFormat()
docFormat.setTopMargin(max(0, tT))
docFormat.setBottomMargin(max(0, bT))
self.qDocument.blockSignals(True)
self.qDocument.rootFrame().setFrameFormat(docFormat)
self.qDocument.blockSignals(False)
self.setViewportMargins(cM, max(cM, tH), cM, max(cM, fH))
return
@@ -536,6 +529,8 @@ class GuiDocViewHeader(QWidget):
def _refreshDocument(self):
"""Reload the content of the document.
"""
if self.docViewer.theHandle == self.theParent.docEditor.theHandle:
self.theParent.saveDocument()
self.docViewer.reloadText()
return
@@ -547,7 +542,7 @@ class GuiDocViewHeader(QWidget):
"""Capture a click on the title and ensure that the item is
selected in the project tree.
"""
self.theParent.treeView.setSelectedHandle(self.theHandle)
self.theParent.treeView.setSelectedHandle(self.theHandle, doScroll=True)
return
# END Class GuiDocViewHeader
+153 -55
View File
@@ -33,7 +33,7 @@ from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QMenuBar, QAction, QMessageBox
from nw.gui.about import GuiAbout
from nw.constants import nwItemType, nwItemClass, nwDocAction
from nw.constants import nwItemType, nwItemClass, nwDocAction, nwDocInsert
logger = logging.getLogger(__name__)
@@ -50,8 +50,9 @@ class GuiMainMenu(QMenuBar):
self._buildProjectMenu()
self._buildDocumentMenu()
self._buildEditMenu()
self._buildViewMenu()
self._buildInsertMenu()
self._buildFormatMenu()
self._buildViewMenu()
self._buildToolsMenu()
self._buildHelpMenu()
@@ -59,6 +60,7 @@ class GuiMainMenu(QMenuBar):
self._docAction = self.theParent.passDocumentAction
self._moveTreeItem = self.theParent.treeView.moveTreeItem
self._newTreeItem = self.theParent.treeView.newTreeItem
self._docInsert = self.theParent.docEditor.insertText
logger.debug("GuiMainMenu initialisation complete")
@@ -97,6 +99,8 @@ class GuiMainMenu(QMenuBar):
##
def _menuExit(self):
"""Exit novelWriter.
"""
self.theParent.closeMain()
return
@@ -123,19 +127,33 @@ class GuiMainMenu(QMenuBar):
return True
def _showAboutQt(self):
"""Show Qt's own About dialog.
"""
msgBox = QMessageBox()
msgBox.aboutQt(self.theParent,"About Qt")
return True
def _openHelp(self):
"""Open the documentation URL in the system's default browser.
"""
QDesktopServices.openUrl(QUrl(nw.__docurl__))
return True
def _openIssue(self):
"""Open the issue tracker URL in the system's default browser.
"""
QDesktopServices.openUrl(QUrl(nw.__issuesurl__))
return True
def _showDocumentLocation(self):
"""Open the dialog showing the location of the editor document.
"""
self.theParent.docEditor.revealLocation()
return True
def _doBackup(self):
"""Call the backup function for the project.
"""
self.theProject.zipIt(True)
return True
@@ -228,14 +246,14 @@ class GuiMainMenu(QMenuBar):
self.projMenu.addSeparator()
# Project > Edit
self.aEditItem = QAction("&Edit Project Item", self)
self.aEditItem = QAction("Edit Project Item", self)
self.aEditItem.setStatusTip("Change item settings")
self.aEditItem.setShortcuts(["Ctrl+E", "F2"])
self.aEditItem.triggered.connect(self.theParent.editItem)
self.projMenu.addAction(self.aEditItem)
# Project > Delete
self.aDeleteItem = QAction("&Delete Project Item", self)
self.aDeleteItem = QAction("Delete Project Item", self)
self.aDeleteItem.setStatusTip("Delete selected item")
self.aDeleteItem.setShortcut("Ctrl+Del")
self.aDeleteItem.triggered.connect(lambda : self.theParent.treeView.deleteItem(None))
@@ -265,21 +283,21 @@ class GuiMainMenu(QMenuBar):
self.docuMenu = self.addMenu("&Document")
# Document > New
self.aNewDoc = QAction("&New Document", self)
self.aNewDoc = QAction("New Document", self)
self.aNewDoc.setStatusTip("Create new document")
self.aNewDoc.setShortcut("Ctrl+N")
self.aNewDoc.triggered.connect(lambda : self._newTreeItem(nwItemType.FILE, None))
self.docuMenu.addAction(self.aNewDoc)
# Document > Open
self.aOpenDoc = QAction("&Open Document", self)
self.aOpenDoc = QAction("Open Document", self)
self.aOpenDoc.setStatusTip("Open selected document")
self.aOpenDoc.setShortcut("Ctrl+O")
self.aOpenDoc.triggered.connect(self.theParent.openSelectedItem)
self.docuMenu.addAction(self.aOpenDoc)
# Document > Save
self.aSaveDoc = QAction("&Save Document", self)
self.aSaveDoc = QAction("Save Document", self)
self.aSaveDoc.setStatusTip("Save current document")
self.aSaveDoc.setShortcut("Ctrl+S")
self.aSaveDoc.triggered.connect(self.theParent.saveDocument)
@@ -341,53 +359,6 @@ class GuiMainMenu(QMenuBar):
return
def _buildViewMenu(self):
# View
self.viewMenu = self.addMenu("&View")
# View > TreeView
self.aFocusTree = QAction("Focus Project Tree", self)
self.aFocusTree.setStatusTip("Move focus to project tree")
self.aFocusTree.setShortcut("Alt+1")
self.aFocusTree.triggered.connect(lambda : self.theParent.setFocus(1))
self.viewMenu.addAction(self.aFocusTree)
# View > Document Pane 1
self.aFocusEditor = QAction("Focus Document Editor", self)
self.aFocusEditor.setStatusTip("Move focus to left document pane")
self.aFocusEditor.setShortcut("Alt+2")
self.aFocusEditor.triggered.connect(lambda : self.theParent.setFocus(2))
self.viewMenu.addAction(self.aFocusEditor)
# View > Document Pane 2
self.aFocusView = QAction("Focus Document Viewer", self)
self.aFocusView.setStatusTip("Move focus to right document pane")
self.aFocusView.setShortcut("Alt+3")
self.aFocusView.triggered.connect(lambda : self.theParent.setFocus(3))
self.viewMenu.addAction(self.aFocusView)
# View > Separator
self.viewMenu.addSeparator()
# View > Toggle Distraction Free Mode
self.aZenMode = QAction("Zen Mode", self)
self.aZenMode.setStatusTip("Toggles distraction free mode, only showing text editor")
self.aZenMode.setShortcut("F8")
self.aZenMode.setCheckable(True)
self.aZenMode.setChecked(self.theParent.isZenMode)
self.aZenMode.toggled.connect(self.theParent.toggleZenMode)
self.viewMenu.addAction(self.aZenMode)
# View > Toggle Full Screen
self.aFullScreen = QAction("Full Screen Mode", self)
self.aFullScreen.setStatusTip("Maximises the main window")
self.aFullScreen.setShortcut("F11")
self.aFullScreen.triggered.connect(self.theParent.toggleFullScreenMode)
self.viewMenu.addAction(self.aFullScreen)
return
def _buildEditMenu(self):
# Edit
@@ -497,6 +468,65 @@ class GuiMainMenu(QMenuBar):
return
def _buildInsertMenu(self):
# Insert
self.insertMenu = self.addMenu("&Insert")
# Insert > Short Dash
self.aInsENDash = QAction("Short Dash", self)
self.aInsENDash.setStatusTip("Insert short dash")
self.aInsENDash.setShortcut("Ctrl+K, -")
self.aInsENDash.triggered.connect(lambda: self._docInsert(nwDocInsert.SHORT_DASH))
self.insertMenu.addAction(self.aInsENDash)
# Insert > Long Dash
self.aInsEMDash = QAction("Long Dash", self)
self.aInsEMDash.setStatusTip("Insert long dash")
self.aInsEMDash.setShortcut("Ctrl+K, _")
self.aInsEMDash.triggered.connect(lambda: self._docInsert(nwDocInsert.LONG_DASH))
self.insertMenu.addAction(self.aInsEMDash)
# Insert > Ellipsis
self.aInsEllipsis = QAction("Ellipsis", self)
self.aInsEllipsis.setStatusTip("Insert ellipsis")
self.aInsEllipsis.setShortcut("Ctrl+K, .")
self.aInsEllipsis.triggered.connect(lambda: self._docInsert(nwDocInsert.ELLIPSIS))
self.insertMenu.addAction(self.aInsEllipsis)
# Insert > Separator
self.insertMenu.addSeparator()
# Insert > Hard Line Break
self.aInsHardBreak = QAction("Hard Line Break", self)
self.aInsHardBreak.setStatusTip("Insert a hard line break")
self.aInsHardBreak.setShortcut("Ctrl+K, Return")
self.aInsHardBreak.triggered.connect(lambda: self._docInsert(nwDocInsert.HARD_BREAK))
self.insertMenu.addAction(self.aInsHardBreak)
# Insert > Non-Breaking Space
self.aInsNBSpace = QAction("Non-Breaking Space", self)
self.aInsNBSpace.setStatusTip("Insert a non-breaking space")
self.aInsNBSpace.setShortcut("Ctrl+K, Space")
self.aInsNBSpace.triggered.connect(lambda: self._docInsert(nwDocInsert.NB_SPACE))
self.insertMenu.addAction(self.aInsNBSpace)
# Insert > Thin Space
self.aInsThinSpace = QAction("Thin Space", self)
self.aInsThinSpace.setStatusTip("Insert a thin space")
self.aInsThinSpace.setShortcut("Ctrl+K, Shift+Space")
self.aInsThinSpace.triggered.connect(lambda: self._docInsert(nwDocInsert.THIN_SPACE))
self.insertMenu.addAction(self.aInsThinSpace)
# Insert > Thin Non-Breaking Space
self.aInsThinNBSpace = QAction("Thin Non-Breaking Space", self)
self.aInsThinNBSpace.setStatusTip("Insert a thin non-breaking space")
self.aInsThinNBSpace.setShortcut("Ctrl+K, Ctrl+Space")
self.aInsThinNBSpace.triggered.connect(lambda: self._docInsert(nwDocInsert.THIN_NB_SPACE))
self.insertMenu.addAction(self.aInsThinNBSpace)
return
def _buildFormatMenu(self):
# Format
@@ -547,7 +577,7 @@ class GuiMainMenu(QMenuBar):
self.aFmtSQuote.triggered.connect(lambda: self._docAction(nwDocAction.S_QUOTE))
self.fmtMenu.addAction(self.aFmtSQuote)
# Edit > Separator
# Format > Separator
self.fmtMenu.addSeparator()
# Format > Header 1
@@ -592,6 +622,68 @@ class GuiMainMenu(QMenuBar):
self.aFmtNoFormat.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_TXT))
self.fmtMenu.addAction(self.aFmtNoFormat)
# Format > Separator
self.fmtMenu.addSeparator()
# Format > Replace Single Quotes
self.aFmtReplSng = QAction("Replace Single Quotes", self)
self.aFmtReplSng.setStatusTip("Replace all straight single quotes in selected text")
self.aFmtReplSng.triggered.connect(lambda: self._docAction(nwDocAction.REPL_SNG))
self.fmtMenu.addAction(self.aFmtReplSng)
# Format > Replace Double Quotes
self.aFmtReplDbl = QAction("Replace Double Quotes", self)
self.aFmtReplDbl.setStatusTip("Replace all straight double quotes in selected text")
self.aFmtReplDbl.triggered.connect(lambda: self._docAction(nwDocAction.REPL_DBL))
self.fmtMenu.addAction(self.aFmtReplDbl)
return
def _buildViewMenu(self):
# View
self.viewMenu = self.addMenu("&View")
# View > TreeView
self.aFocusTree = QAction("Focus Project Tree", self)
self.aFocusTree.setStatusTip("Move focus to project tree")
self.aFocusTree.setShortcut("Alt+1")
self.aFocusTree.triggered.connect(lambda : self.theParent.setFocus(1))
self.viewMenu.addAction(self.aFocusTree)
# View > Document Pane 1
self.aFocusEditor = QAction("Focus Document Editor", self)
self.aFocusEditor.setStatusTip("Move focus to left document pane")
self.aFocusEditor.setShortcut("Alt+2")
self.aFocusEditor.triggered.connect(lambda : self.theParent.setFocus(2))
self.viewMenu.addAction(self.aFocusEditor)
# View > Document Pane 2
self.aFocusView = QAction("Focus Document Viewer", self)
self.aFocusView.setStatusTip("Move focus to right document pane")
self.aFocusView.setShortcut("Alt+3")
self.aFocusView.triggered.connect(lambda : self.theParent.setFocus(3))
self.viewMenu.addAction(self.aFocusView)
# View > Separator
self.viewMenu.addSeparator()
# View > Toggle Distraction Free Mode
self.aZenMode = QAction("Zen Mode", self)
self.aZenMode.setStatusTip("Toggles distraction free mode, only showing text editor")
self.aZenMode.setShortcut("F8")
self.aZenMode.setCheckable(True)
self.aZenMode.setChecked(self.theParent.isZenMode)
self.aZenMode.toggled.connect(self.theParent.toggleZenMode)
self.viewMenu.addAction(self.aZenMode)
# View > Toggle Full Screen
self.aFullScreen = QAction("Full Screen Mode", self)
self.aFullScreen.setStatusTip("Maximises the main window")
self.aFullScreen.setShortcut("F11")
self.aFullScreen.triggered.connect(self.theParent.toggleFullScreenMode)
self.viewMenu.addAction(self.aFullScreen)
return
def _buildToolsMenu(self):
@@ -703,6 +795,12 @@ class GuiMainMenu(QMenuBar):
self.aHelp.triggered.connect(self._openHelp)
self.helpMenu.addAction(self.aHelp)
# Document > Report Issue
self.aIssue = QAction("Report an Issue", self)
self.aIssue.setStatusTip("View online documentation")
self.aIssue.triggered.connect(self._openIssue)
self.helpMenu.addAction(self.aIssue)
return
# END Class GuiMainMenu
+1 -1
View File
@@ -196,7 +196,7 @@ class GuiOutline(QTreeWidget):
except:
tLine = 1
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
def _itemSelected(self):
+63 -37
View File
@@ -37,9 +37,9 @@ from PyQt5.QtWidgets import (
QDialogButtonBox, QFileDialog, QFontDialog
)
from nw.gui.custom import QSwitch, QConfigLayout, PagedDialog
from nw.gui.custom import QSwitch, QConfigLayout, PagedDialog, QuotesDialog
from nw.core import NWSpellCheck, NWSpellSimple, NWSpellEnchant
from nw.constants import nwAlert, nwQuotes
from nw.constants import nwQuotes
logger = logging.getLogger(__name__)
@@ -816,51 +816,72 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.mainForm.addGroupLabel("Quotation Style")
qWidth = self.mainConf.pxInt(40)
bWidth = int(2.5*self.theTheme.getTextWidth("..."))
## Single Quote Style
self.quoteSingleStyleO = QLineEdit()
self.quoteSingleStyleO.setMaxLength(1)
self.quoteSingleStyleO.setReadOnly(True)
self.quoteSingleStyleO.setFixedWidth(qWidth)
self.quoteSingleStyleO.setAlignment(Qt.AlignCenter)
self.quoteSingleStyleO.setText(self.mainConf.fmtSingleQuotes[0])
self.btnSingleStyleO = QPushButton("...")
self.btnSingleStyleO.setMaximumWidth(bWidth)
self.btnSingleStyleO.clicked.connect(self._getSingleOpen)
self.mainForm.addRow(
"Single quote open style",
self.quoteSingleStyleO,
"Auto-replaces apostrophe before words."
"Auto-replaces apostrophe before words.",
theButton=self.btnSingleStyleO
)
self.quoteSingleStyleC = QLineEdit()
self.quoteSingleStyleC.setMaxLength(1)
self.quoteSingleStyleC.setReadOnly(True)
self.quoteSingleStyleC.setFixedWidth(qWidth)
self.quoteSingleStyleC.setAlignment(Qt.AlignCenter)
self.quoteSingleStyleC.setText(self.mainConf.fmtSingleQuotes[1])
self.btnSingleStyleC = QPushButton("...")
self.btnSingleStyleC.setMaximumWidth(bWidth)
self.btnSingleStyleC.clicked.connect(self._getSingleClose)
self.mainForm.addRow(
"Single quote close style",
self.quoteSingleStyleC,
"Auto-replaces apostrophe after words."
"Auto-replaces apostrophe after words.",
theButton=self.btnSingleStyleC
)
## Double Quote Style
self.quoteDoubleStyleO = QLineEdit()
self.quoteDoubleStyleO.setMaxLength(1)
self.quoteDoubleStyleO.setReadOnly(True)
self.quoteDoubleStyleO.setFixedWidth(qWidth)
self.quoteDoubleStyleO.setAlignment(Qt.AlignCenter)
self.quoteDoubleStyleO.setText(self.mainConf.fmtDoubleQuotes[0])
self.btnDoubleStyleO = QPushButton("...")
self.btnDoubleStyleO.setMaximumWidth(bWidth)
self.btnDoubleStyleO.clicked.connect(self._getDoubleOpen)
self.mainForm.addRow(
"Double quote open style",
self.quoteDoubleStyleO,
"Auto-replaces straight quotes before words."
"Auto-replaces straight quotes before words.",
theButton=self.btnDoubleStyleO
)
self.quoteDoubleStyleC = QLineEdit()
self.quoteDoubleStyleC.setMaxLength(1)
self.quoteDoubleStyleC.setReadOnly(True)
self.quoteDoubleStyleC.setFixedWidth(qWidth)
self.quoteDoubleStyleC.setAlignment(Qt.AlignCenter)
self.quoteDoubleStyleC.setText(self.mainConf.fmtDoubleQuotes[1])
self.btnDoubleStyleC = QPushButton("...")
self.btnDoubleStyleC.setMaximumWidth(bWidth)
self.btnDoubleStyleC.clicked.connect(self._getDoubleClose)
self.mainForm.addRow(
"Double quote close style",
self.quoteDoubleStyleC,
"Auto-replaces straight quotes after words."
"Auto-replaces straight quotes after words.",
theButton=self.btnDoubleStyleC
)
return
@@ -889,37 +910,10 @@ class GuiConfigEditAutoReplaceTab(QWidget):
fmtDoubleQuotesO = self.quoteDoubleStyleO.text()
fmtDoubleQuotesC = self.quoteDoubleStyleC.text()
if self._checkQuoteSymbol(fmtSingleQuotesO):
self.mainConf.fmtSingleQuotes[0] = fmtSingleQuotesO
else:
self.theParent.makeAlert(
"Invalid quote symbol: %s" % fmtSingleQuotesO, nwAlert.ERROR
)
validEntries = False
if self._checkQuoteSymbol(fmtSingleQuotesC):
self.mainConf.fmtSingleQuotes[1] = fmtSingleQuotesC
else:
self.theParent.makeAlert(
"Invalid quote symbol: %s" % fmtSingleQuotesC, nwAlert.ERROR
)
validEntries = False
if self._checkQuoteSymbol(fmtDoubleQuotesO):
self.mainConf.fmtDoubleQuotes[0] = fmtDoubleQuotesO
else:
self.theParent.makeAlert(
"Invalid quote symbol: %s" % fmtDoubleQuotesO, nwAlert.ERROR
)
validEntries = False
if self._checkQuoteSymbol(fmtDoubleQuotesC):
self.mainConf.fmtDoubleQuotes[1] = fmtDoubleQuotesC
else:
self.theParent.makeAlert(
"Invalid quote symbol: %s" % fmtDoubleQuotesC, nwAlert.ERROR
)
validEntries = False
self.mainConf.fmtSingleQuotes[0] = fmtSingleQuotesO
self.mainConf.fmtSingleQuotes[1] = fmtSingleQuotesC
self.mainConf.fmtDoubleQuotes[0] = fmtDoubleQuotesO
self.mainConf.fmtDoubleQuotes[1] = fmtDoubleQuotesC
self.mainConf.confChanged = True
@@ -939,6 +933,38 @@ class GuiConfigEditAutoReplaceTab(QWidget):
self.autoReplaceDots.setEnabled(theState)
return
def _getSingleOpen(self):
"""Dialog for single quote open.
"""
qtBox = QuotesDialog(self, currentQuote=self.quoteSingleStyleO.text())
if qtBox.exec_() == QDialog.Accepted:
self.quoteSingleStyleO.setText(qtBox.selectedQuote)
return
def _getSingleClose(self):
"""Dialog for single quote close.
"""
qtBox = QuotesDialog(self, currentQuote=self.quoteSingleStyleC.text())
if qtBox.exec_() == QDialog.Accepted:
self.quoteSingleStyleC.setText(qtBox.selectedQuote)
return
def _getDoubleOpen(self):
"""Dialog for double quote open.
"""
qtBox = QuotesDialog(self, currentQuote=self.quoteDoubleStyleO.text())
if qtBox.exec_() == QDialog.Accepted:
self.quoteDoubleStyleO.setText(qtBox.selectedQuote)
return
def _getDoubleClose(self):
"""Dialog for double quote close.
"""
qtBox = QuotesDialog(self, currentQuote=self.quoteDoubleStyleC.text())
if qtBox.exec_() == QDialog.Accepted:
self.quoteDoubleStyleC.setText(qtBox.selectedQuote)
return
##
# Internal Functions
##
+39 -11
View File
@@ -31,10 +31,9 @@ import nw
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QLineEdit, QPlainTextEdit,
QLabel, QWidget, QTabWidget, QDialogButtonBox, QListWidget, QPushButton,
QListWidgetItem, QColorDialog, QAbstractItemView, QTreeWidget, QCheckBox,
QTreeWidgetItem
QHBoxLayout, QVBoxLayout, QGridLayout, QLineEdit, QPlainTextEdit, QLabel,
QWidget, QDialogButtonBox, QListWidget, QPushButton, QListWidgetItem,
QColorDialog, QAbstractItemView, QTreeWidget, QTreeWidgetItem
)
from nw.constants import nwAlert
@@ -52,9 +51,17 @@ class GuiProjectSettings(PagedDialog):
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
self.optState = theProject.optState
self.theProject.countStatus()
self.setWindowTitle("Project Settings")
self.setMinimumWidth(self.mainConf.pxInt(570))
self.setMinimumHeight(self.mainConf.pxInt(355))
self.resize(
self.mainConf.pxInt(self.optState.getInt("GuiProjectSettings", "winWidth", 570)),
self.mainConf.pxInt(self.optState.getInt("GuiProjectSettings", "winHeight", 355))
)
self.tabMain = GuiProjectEditMain(self.theParent, self.theProject)
self.tabMeta = GuiProjectEditMeta(self.theParent, self.theProject)
@@ -73,13 +80,17 @@ class GuiProjectSettings(PagedDialog):
self.buttonBox.rejected.connect(self._doClose)
self.addControls(self.buttonBox)
self.show()
logger.debug("GuiProjectSettings initialisation complete")
return
##
# Slots
##
def _doSave(self):
"""Save settings and close dialog.
"""
logger.verbose("GuiProjectSettings save button clicked")
projName = self.tabMain.editName.text()
@@ -103,13 +114,23 @@ class GuiProjectSettings(PagedDialog):
newList = self.tabReplace.getNewList()
self.theProject.setAutoReplace(newList)
self.close()
self._doClose()
return
def _doClose(self):
logger.verbose("GuiProjectSettings close button clicked")
"""Close the dialog.
"""
winWidth = self.mainConf.rpxInt(self.width())
winHeight = self.mainConf.rpxInt(self.height())
replaceColW = self.mainConf.rpxInt(self.tabReplace.listBox.columnWidth(0))
self.optState.setValue("GuiProjectSettings", "winWidth", winWidth)
self.optState.setValue("GuiProjectSettings", "winHeight", winHeight)
self.optState.setValue("GuiProjectSettings", "replaceColW", replaceColW)
self.close()
return
# END Class GuiProjectSettings
@@ -455,16 +476,23 @@ class GuiProjectEditReplace(QWidget):
def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent)
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theProject
self.optState = theProject.optState
self.arChanged = False
self.outerBox = QVBoxLayout()
self.bottomBox = QHBoxLayout()
self.listBox = QTreeWidget()
self.outerBox = QVBoxLayout()
self.bottomBox = QHBoxLayout()
wCol0 = self.mainConf.pxInt(
self.optState.getInt("GuiProjectSettings", "replaceColW", 100)
)
self.listBox = QTreeWidget()
self.listBox.setHeaderLabels(["Keyword","Replace With"])
self.listBox.itemSelectionChanged.connect(self._selectedItem)
self.listBox.setColumnWidth(0, wCol0)
self.listBox.setIndentation(0)
for aKey, aVal in self.theProject.autoReplace.items():
+27 -22
View File
@@ -54,6 +54,7 @@ class GuiProjectTree(QTreeWidget):
QTreeWidget.__init__(self, theParent)
logger.debug("Initialising GuiProjectTree ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
@@ -237,8 +238,8 @@ class GuiProjectTree(QTreeWidget):
"""
if qApp.focusWidget() == self and self.theParent.hasProject:
tHandle = self.getSelectedHandle()
tItem = self._getTreeItem(tHandle)
pItem = tItem.parent()
tItem = self._getTreeItem(tHandle)
pItem = tItem.parent()
if pItem is None:
tIndex = self.indexOfTopLevelItem(tItem)
nChild = self.topLevelItemCount()
@@ -367,6 +368,7 @@ class GuiProjectTree(QTreeWidget):
if nwItemS is None:
return False
wCount = int(trItemS.text(self.C_COUNT))
if nwItemS.itemType == nwItemType.FILE:
logger.debug("User requested file %s moved to trash" % tHandle)
trItemP = trItemS.parent()
@@ -393,7 +395,8 @@ class GuiProjectTree(QTreeWidget):
if doPermanent:
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)
if self.theParent.docEditor.theHandle == tHandle:
@@ -422,10 +425,12 @@ class GuiProjectTree(QTreeWidget):
if pHandle is None:
logger.warning("File has no parent item")
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC)
nwItemS.setParent(self.theProject.projTree.trashRoot())
self.propagateCount(tHandle, wCount)
self._setTreeChanged(True)
self.theParent.theIndex.deleteHandle(tHandle)
@@ -504,7 +509,7 @@ class GuiProjectTree(QTreeWidget):
"""
tItem = self._getTreeItem(tHandle)
if tItem is not None:
tItem.setText(self.C_COUNT,str(theCount))
tItem.setText(self.C_COUNT, str(theCount))
pItem = tItem.parent()
if pItem is not None:
pCount = 0
@@ -520,7 +525,7 @@ class GuiProjectTree(QTreeWidget):
relevant values in the project and on the status bar. This call
is a fast way of getting this number, and depends on the
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
for n in range(self.topLevelItemCount()):
@@ -569,14 +574,14 @@ class GuiProjectTree(QTreeWidget):
selHandles.append(selItems[n].data(self.C_NAME, Qt.UserRole))
return selHandles
def setSelectedHandle(self, tHandle):
def setSelectedHandle(self, tHandle, doScroll=False):
"""Set a specific handle as the selected item.
"""
if tHandle in self.theMap:
self.clearSelection()
self.theMap[tHandle].setSelected(True)
selItems = self.selectedIndexes()
if selItems:
if selItems and doScroll:
self.scrollTo(
selItems[0], QAbstractItemView.PositionAtCenter
)
@@ -629,23 +634,26 @@ class GuiProjectTree(QTreeWidget):
logger.error("Invalid drop index")
return
dItem = self.itemFromIndex(dIndex)
sItem = self._getTreeItem(sHandle)
dItem = self.itemFromIndex(dIndex)
dHandle = dItem.data(self.C_NAME, Qt.UserRole)
snItem = self.theProject.projTree[sHandle]
dnItem = self.theProject.projTree[dHandle]
snItem = self.theProject.projTree[sHandle]
dnItem = self.theProject.projTree[dHandle]
if dnItem is None:
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
return
isSame = snItem.itemClass == dnItem.itemClass
isNone = snItem.itemClass == nwItemClass.NO_CLASS
isNote = snItem.itemLayout == nwItemLayout.NOTE
onFile = dnItem.itemType == nwItemType.FILE
isRoot = snItem.itemType == nwItemType.ROOT
onRoot = dnItem.itemType == nwItemType.ROOT
wCount = int(sItem.text(self.C_COUNT))
isSame = snItem.itemClass == dnItem.itemClass
isNone = snItem.itemClass == nwItemClass.NO_CLASS
isNote = snItem.itemLayout == nwItemLayout.NOTE
onFile = dnItem.itemType == nwItemType.FILE
isRoot = snItem.itemType == nwItemType.ROOT
onRoot = dnItem.itemType == nwItemType.ROOT
isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem
if (isSame or isNone or isNote) and not (onFile and isOnTop) and not isRoot:
logger.debug("Drag'n'drop of item %s accepted" % sHandle)
self.propagateCount(sHandle, 0)
QTreeWidget.dropEvent(self, theEvent)
if isNone:
self._moveOrphanedItem(sHandle, dHandle)
@@ -660,6 +668,7 @@ class GuiProjectTree(QTreeWidget):
))
snItem.setClass(dnItem.itemClass)
self.setTreeItemValues(sHandle)
self.propagateCount(sHandle, wCount)
else:
logger.debug("Drag'n'drop of item %s not accepted" % sHandle)
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
@@ -794,8 +803,7 @@ class GuiProjectTree(QTreeWidget):
def _updateItemParent(self, tHandle):
"""Update the parent handle of an item so that the information
in the project is consistent with the treeView. Also move the
word count over to the new parent tree.
in the project is consistent with the treeView.
"""
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.projTree[tHandle]
@@ -805,10 +813,7 @@ class GuiProjectTree(QTreeWidget):
return False
pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
wC = int(trItemS.text(self.C_COUNT))
self.propagateCount(tHandle, -wC)
nwItemS.setParent(pHandle)
self.propagateCount(tHandle, wC)
self.setTreeItemValues(tHandle)
self._setTreeChanged(True)
@@ -929,7 +934,7 @@ class GuiProjectTreeMenu(QMenu):
"""Forward the open document call to the main GUI window.
"""
if self.theItem is not None:
self.theTree.theParent.openDocument(self.theItem.itemHandle)
self.theTree.theParent.openDocument(self.theItem.itemHandle, doScroll=False)
return
def _doViewItem(self):
+407 -110
View File
@@ -26,78 +26,105 @@
"""
import logging
import json
import nw
from os import path
from datetime import datetime
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
from PyQt5.QtGui import QFont, QPixmap
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QDialogButtonBox,
QGridLayout, QLabel, QGroupBox, QCheckBox
qApp, QDialog, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QGridLayout,
QLabel, QGroupBox, QMenu, QAction, QFileDialog
)
from nw.constants import nwConst, nwFiles, nwAlert
from nw.gui.custom import QSwitch
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):
QDialog.__init__(self, theParent)
logger.debug("Initialising SessionLogView ...")
logger.debug("Initialising GuiSessionLog ...")
self.mainConf = nw.CONFIG
self.theProject = theProject
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.timeTotal = 0.0
self.outerBox = QGridLayout()
self.bottomBox = QHBoxLayout()
self.wordOffset = 0
self.setWindowTitle("Session Log")
self.setMinimumWidth(self.mainConf.pxInt(420))
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))
wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol1", 80))
wCol2 = self.mainConf.pxInt(self.optState.getInt("GuiSession", "widthCol2", 80))
# List Box
wCol0 = self.mainConf.pxInt(
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.setHeaderLabels(["Session Start","Length","Words",""])
self.listBox.setHeaderLabels(["Session Start","Length","Words","Histogram"])
self.listBox.setIndentation(0)
self.listBox.setColumnWidth(0, wCol0)
self.listBox.setColumnWidth(1, wCol1)
self.listBox.setColumnWidth(2, wCol2)
self.listBox.setColumnWidth(3, 0)
self.listBox.setColumnWidth(self.C_TIME, wCol0)
self.listBox.setColumnWidth(self.C_LENGTH, wCol1)
self.listBox.setColumnWidth(self.C_COUNT, wCol2)
hHeader = self.listBox.headerItem()
hHeader.setTextAlignment(1,Qt.AlignRight)
hHeader.setTextAlignment(2,Qt.AlignRight)
hHeader.setTextAlignment(self.C_LENGTH, 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)
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(
self.optState.getInt("GuiSession", "sortOrder", Qt.DescendingOrder),
self.optState.getInt("GuiSessionLog", "sortOrder", Qt.DescendingOrder),
sortValid, Qt.DescendingOrder
)
self.listBox.sortByColumn(sortCol, sortOrder)
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
self.infoBox = QGroupBox("Sum Total Time", self)
self.infoBoxForm = QGridLayout(self)
self.infoBox.setLayout(self.infoBoxForm)
self.infoBox = QGroupBox("Sum Totals", self)
self.infoForm = QGridLayout(self)
self.infoBox.setLayout(self.infoForm)
self.labelTotal = QLabel(self._formatTime(0))
self.labelTotal.setFont(self.monoFont)
@@ -107,105 +134,296 @@ class GuiSessionLogView(QDialog):
self.labelFilter.setFont(self.monoFont)
self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.infoBoxForm.addWidget(QLabel("All:"), 0, 0)
self.infoBoxForm.addWidget(self.labelTotal, 0, 1)
self.infoBoxForm.addWidget(QLabel("Filtered:"), 1, 0)
self.infoBoxForm.addWidget(self.labelFilter, 1, 1)
self.novelWords = QLabel("0")
self.novelWords.setFont(self.monoFont)
self.novelWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
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
self.filterBox = QGroupBox("Filters", self)
self.filterBoxForm = QGridLayout(self)
self.filterBox.setLayout(self.filterBoxForm)
sPx = self.theTheme.baseIconSize
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.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.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.filterBoxForm.addWidget(self.hideNegative, 1, 0)
self.groupByDay = QSwitch(width=2*sPx, height=sPx)
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
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
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
self.outerBox = QGridLayout()
self.outerBox.addWidget(self.listBox, 0, 0, 1, 2)
self.outerBox.addWidget(self.infoBox, 1, 0)
self.outerBox.addWidget(self.filterBox, 1, 1)
self.outerBox.addWidget(self.buttonBox, 2, 0, 1, 2)
self.outerBox.setRowStretch(0, 1)
self.setLayout(self.outerBox)
self.show()
logger.debug("GuiSessionLog initialisation complete")
logger.debug("SessionLogView initialisation complete")
self._loadSessionLog()
qApp.processEvents()
self._loadLogFile()
self._updateListBox()
return
def _loadSessionLog(self):
##
# Slots
##
logFile = path.join(self.theProject.projMeta, nwFiles.SESS_INFO)
if not path.isfile(logFile):
logger.warning("No session log file found for this project.")
return False
self.listBox.clear()
self.timeFilter = 0.0
self.timeTotal = 0.0
def _doClose(self):
"""Save the state of the window, clear cache, end close.
"""
self.logData = []
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()
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:
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:
inData = inLine.split()
if len(inData) != 8:
if inLine.startswith("#"):
if inLine.startswith("# Offset"):
self.wordOffset = int(inLine[9:].strip())
logger.verbose(
"Initial word count when log was started is %d" % self.wordOffset
)
continue
inData = inLine.split()
if len(inData) != 6:
continue
dStart = datetime.strptime(
"%s %s" % (inData[1],inData[2]), nwConst.tStampFmt
"%s %s" % (inData[0], inData[1]), nwConst.tStampFmt
)
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
sDiff = tDiff.total_seconds()
ttTime += sDiff
self.timeTotal += sDiff
if abs(nWords) > 0:
self.timeFilter += sDiff
wcNovel = int(inData[4])
wcNotes = int(inData[5])
ttNovel = wcNovel
ttNotes = wcNotes
if hideZeros and nWords == 0:
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)
self.logData.append((dStart, sDiff, wcNovel, wcNotes))
except Exception as e:
self.theParent.makeAlert(
@@ -213,47 +431,126 @@ class GuiSessionLogView(QDialog):
)
return False
self.labelFilter.setText(self._formatTime(self.timeFilter))
self.labelTotal.setText(self._formatTime(self.timeTotal))
self.labelTotal.setText(self._formatTime(ttTime))
self.novelWords.setText("{:n}".format(ttNovel))
self.notesWords.setText("{:n}".format(ttNotes))
self.totalWords.setText("{:n}".format(ttNovel + ttNotes))
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))
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()
hideNegative = self.hideNegative.isChecked()
groupByDay = self.groupByDay.isChecked()
self.optState.setValue("GuiSession", "widthCol0", widthCol0)
self.optState.setValue("GuiSession", "widthCol1", widthCol1)
self.optState.setValue("GuiSession", "widthCol2", widthCol2)
self.optState.setValue("GuiSession", "sortCol", sortCol)
self.optState.setValue("GuiSession", "sortOrder", sortOrder)
self.optState.setValue("GuiSession", "hideZeros", hideZeros)
self.optState.setValue("GuiSession", "hideNegative", hideNegative)
# Group the data
if groupByDay:
tempData = []
sessDate = None
sessTime = 0
lstNovel = 0
lstNotes = 0
self.optState.saveSettings()
self.close()
for n, (dStart, sDiff, wcNovel, wcNotes) in enumerate(self.logData):
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):
self._loadSessionLog()
return
else:
tempData = self.logData
def _doHideNegative(self, newState):
self._loadSessionLog()
return
# Calculate Word Diff
self.filterData = []
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):
"""Format the time spent in 00:00:00 format.
"""
tM = int(tS/60)
tH = int(tM/60)
tM = tM - tH*60
tS = tS - tM*60 - tH*3600
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)
self.guiFont = qApp.font()
self.guiFontFixed = QFontDatabase.systemFont(QFontDatabase.FixedFont)
qMetric = QFontMetrics(self.guiFont)
self.fontPointSize = self.guiFont.pointSizeF()
self.fontPixelSize = int(round(qMetric.height()))
+55 -22
View File
@@ -43,7 +43,7 @@ from nw.gui import (
GuiBuildNovel, GuiDocEditor, GuiDocMerge, GuiDocSplit, GuiDocViewDetails,
GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus,
GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectLoad, GuiTheme,
GuiProjectSettings, GuiProjectTree, GuiSessionLogView
GuiProjectSettings, GuiProjectTree, GuiSessionLog
)
from nw.core import NWProject, NWDoc, NWIndex
from nw.constants import nwFiles, nwItemType, nwAlert
@@ -419,7 +419,7 @@ class GuiMain(QMainWindow):
# Restore previously open documents, if any
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:
self.viewDocument(self.theProject.lastViewed)
@@ -463,7 +463,7 @@ class GuiMain(QMainWindow):
self.docEditor.clearEditor()
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.
"""
if self.hasProject:
@@ -473,7 +473,7 @@ class GuiMain(QMainWindow):
if changeFocus:
self.docEditor.setFocus()
self.theProject.setLastEdited(tHandle)
self.treeView.setSelectedHandle(tHandle)
self.treeView.setSelectedHandle(tHandle, doScroll=doScroll)
else:
return False
return True
@@ -501,10 +501,10 @@ class GuiMain(QMainWindow):
break
if nHandle is not None:
self.openDocument(nHandle, tLine=0)
self.openDocument(nHandle, tLine=0, doScroll=True)
return True
elif wrapAround:
self.openDocument(fHandle, tLine=0)
self.openDocument(fHandle, tLine=0, doScroll=True)
return False
return False
@@ -520,20 +520,30 @@ class GuiMain(QMainWindow):
"""Load a document for viewing in the view panel.
"""
if tHandle is None:
tHandle = self.treeView.getSelectedHandle()
if tHandle is None:
logger.debug("No document selected, trying editor document")
tHandle = self.docEditor.theHandle
if tHandle is None:
logger.debug("No document selected, trying last viewed")
tHandle = self.theProject.lastViewed
if tHandle is None:
logger.debug("No document selected, giving up")
return False
logger.debug("Viewing document, but no handle provided")
if self.docEditor.hasFocus():
logger.verbose("Trying editor document")
tHandle = self.docEditor.theHandle
if tHandle is not None:
self.saveDocument()
else:
logger.verbose("Trying selected document")
tHandle = self.treeView.getSelectedHandle()
if tHandle is None:
logger.verbose("Trying last viewed document")
tHandle = self.theProject.lastViewed
if tHandle is None:
logger.verbose("No document to view, giving up")
return False
# Make sure main tab is in Editor view
self.tabWidget.setCurrentWidget(self.splitDocs)
logger.debug("Viewing document with handle %s" % tHandle)
if self.docViewer.loadText(tHandle):
if not self.splitView.isVisible():
bPos = self.splitMain.sizes()
@@ -648,7 +658,7 @@ class GuiMain(QMainWindow):
nwItem = self.theProject.projTree[tHandle]
if nwItem.itemType == nwItemType.FILE:
logger.verbose("Requested item %s is a file" % tHandle)
self.openDocument(tHandle)
self.openDocument(tHandle, doScroll=False)
else:
logger.verbose("Requested item %s is not a file" % tHandle)
@@ -802,7 +812,7 @@ class GuiMain(QMainWindow):
"""Open the session log dialog.
"""
if self.hasProject:
dlgTLine = GuiSessionLogView(self, self.theProject)
dlgTLine = GuiSessionLog(self, self.theProject)
dlgTLine.exec_()
return True
@@ -968,12 +978,15 @@ class GuiMain(QMainWindow):
"""Connect to the main window all menu actions that need to be
available also when the main menu is hidden.
"""
# Project
self.addAction(self.mainMenu.aSaveProject)
self.addAction(self.mainMenu.aExitNW)
# Document
self.addAction(self.mainMenu.aSaveDoc)
self.addAction(self.mainMenu.aFileDetails)
self.addAction(self.mainMenu.aZenMode)
self.addAction(self.mainMenu.aFullScreen)
# Edit
self.addAction(self.mainMenu.aEditUndo)
self.addAction(self.mainMenu.aEditRedo)
self.addAction(self.mainMenu.aEditCut)
@@ -981,6 +994,17 @@ class GuiMain(QMainWindow):
self.addAction(self.mainMenu.aEditPaste)
self.addAction(self.mainMenu.aSelectAll)
self.addAction(self.mainMenu.aSelectPar)
# Insert
self.addAction(self.mainMenu.aInsENDash)
self.addAction(self.mainMenu.aInsEMDash)
self.addAction(self.mainMenu.aInsEllipsis)
self.addAction(self.mainMenu.aInsHardBreak)
self.addAction(self.mainMenu.aInsNBSpace)
self.addAction(self.mainMenu.aInsThinSpace)
self.addAction(self.mainMenu.aInsThinNBSpace)
# Format
self.addAction(self.mainMenu.aFmtItalic)
self.addAction(self.mainMenu.aFmtBold)
self.addAction(self.mainMenu.aFmtBoldIt)
@@ -992,10 +1016,19 @@ class GuiMain(QMainWindow):
self.addAction(self.mainMenu.aFmtHead4)
self.addAction(self.mainMenu.aFmtComment)
self.addAction(self.mainMenu.aFmtNoFormat)
# View
self.addAction(self.mainMenu.aZenMode)
self.addAction(self.mainMenu.aFullScreen)
# Tools
self.addAction(self.mainMenu.aSpellCheck)
self.addAction(self.mainMenu.aReRunSpell)
self.addAction(self.mainMenu.aPreferences)
# Help
self.addAction(self.mainMenu.aHelp)
return True
def _setWindowTitle(self, projName=None):
@@ -1070,7 +1103,7 @@ class GuiMain(QMainWindow):
if nwItem is not None:
if nwItem.itemType == nwItemType.FILE:
logger.verbose("Requested item %s is a file" % tHandle)
self.openDocument(tHandle, changeFocus=False)
self.openDocument(tHandle, changeFocus=False, doScroll=False)
else:
logger.verbose("Requested item %s is a folder" % tHandle)
return
@@ -1086,7 +1119,7 @@ class GuiMain(QMainWindow):
if nwItem is not None:
if nwItem.itemType == nwItemType.FILE:
logger.verbose("Requested item %s is a file" % tHandle)
self.openDocument(tHandle, changeFocus=False)
self.openDocument(tHandle, changeFocus=False, doScroll=False)
else:
logger.verbose("Requested item %s is a folder" % tHandle)
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.
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
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'?>
<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>
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
<author>Jay Doh</author>
<saveCount>332</saveCount>
<autoCount>56</autoCount>
<editTime>10692</editTime>
<saveCount>593</saveCount>
<autoCount>103</autoCount>
<editTime>24563</editTime>
</project>
<settings>
<doBackup>False</doBackup>
<spellCheck>True</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>636b6aa9b697b</lastEdited>
<lastViewed>636b6aa9b697b</lastViewed>
<lastWordCount>927</lastWordCount>
<lastViewed>b3e74dbc1f584</lastViewed>
<lastWordCount>982</lastWordCount>
<novelWordCount>606</novelWordCount>
<notesWordCount>376</notesWordCount>
<autoReplace>
<A>B</A>
<B>E</B>
<C>D</C>
<entry key="A">B</entry>
<entry key="B">E</entry>
<entry key="C">D</entry>
</autoReplace>
<titleFormat>
<title>%title%</title>
@@ -114,10 +116,10 @@
<status>1st Draft</status>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>1240</charCount>
<wordCount>223</wordCount>
<paraCount>7</paraCount>
<cursorPos>403</cursorPos>
<charCount>1564</charCount>
<wordCount>278</wordCount>
<paraCount>8</paraCount>
<cursorPos>1633</cursorPos>
</item>
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
<name>Another Scene</name>
+1 -1
View File
@@ -6,7 +6,7 @@ with open("README.md", "r") as inFile:
setuptools.setup(
name = "novelWriter",
version = "0.9rc1",
version = "0.10.0",
author = "Veronica Berglyd Olsen",
author_email = "code@vkbo.net",
description = "A markdown-like document editor for writing novels",
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>31489056e0916</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>86</lastWordCount>
<novelWordCount>59</novelWordCount>
<notesWordCount>27</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+4 -2
View File
@@ -1,5 +1,5 @@
<?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>
<name>Project Name</name>
<title>Project Title</title>
@@ -16,8 +16,10 @@
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace>
<This>With This Stuff </This>
<entry key="This">With This Stuff </entry>
</autoReplace>
<titleFormat>
<title>%title%</title>
+8 -6
View File
@@ -1,5 +1,5 @@
<?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>
<name>New Project</name>
<title></title>
@@ -11,9 +11,11 @@
<doBackup>True</doBackup>
<spellCheck>False</spellCheck>
<autoOutline>True</autoOutline>
<lastEdited>None</lastEdited>
<lastEdited>31489056e0916</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<lastWordCount>59</lastWordCount>
<novelWordCount>59</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
@@ -57,9 +59,9 @@
<status>Note</status>
<exported>False</exported>
<layout>PAGE</layout>
<charCount>0</charCount>
<wordCount>0</wordCount>
<paraCount>0</paraCount>
<charCount>331</charCount>
<wordCount>59</wordCount>
<paraCount>2</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="44cb730c42048" order="1" parent="None">
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+2
View File
@@ -14,6 +14,8 @@
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastWordCount>0</lastWordCount>
<novelWordCount>0</novelWordCount>
<notesWordCount>0</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
+259 -4
View File
@@ -2,7 +2,7 @@
"""novelWriter Main GUI Class Tester
"""
import nw, pytest, sys
import nw, pytest, sys, json
from nwtools import *
from os import path, unlink
@@ -10,7 +10,7 @@ from PyQt5.QtCore import Qt
from nw.gui import (
GuiProjectSettings, GuiItemEditor, GuiAbout, GuiBuildNovel,
GuiDocMerge, GuiDocSplit
GuiDocMerge, GuiDocSplit, GuiSessionLog
)
from nw.constants import *
@@ -127,6 +127,11 @@ def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI.treeView.newTreeItem(nwItemType.FILE, None)
assert nwGUI.openSelectedItem()
# Add Some Text
nwGUI.docEditor.replaceText("Hello World!")
assert nwGUI.docEditor.getText() == "Hello World!"
nwGUI.docEditor.replaceText("")
# Type something into the document
nwGUI.setFocus(2)
for c in "# Main Location":
@@ -273,6 +278,7 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI.mainConf.backupPath = nwTempGUI
projEdit = GuiProjectSettings(nwGUI, nwGUI.theProject)
projEdit.show()
qtbot.addWidget(projEdit)
projEdit.tabMain.editName.setText("")
@@ -358,6 +364,7 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
# Create new, save, open project
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject(nwTempGUI, True)
assert nwGUI.openDocument("31489056e0916")
itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916")
qtbot.addWidget(itemEdit)
@@ -374,7 +381,6 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
itemEdit.editExport.setChecked(False)
assert not itemEdit.editExport.isChecked()
itemEdit._doSave()
itemEdit = GuiItemEditor(nwGUI, nwGUI.theProject, "31489056e0916")
@@ -382,9 +388,16 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
assert itemEdit.editName.text() == "Just a Page"
assert itemEdit.editStatus.currentData() == "Note"
assert itemEdit.editLayout.currentData() == nwItemLayout.PAGE
itemEdit._doClose()
# Check that the header is updated
nwGUI.docEditor.updateDocTitle("31489056e0916")
assert nwGUI.docEditor.docHeader.theTitle.text() == "Novel New Chapter Just a Page"
assert not nwGUI.docEditor.setCursorLine("where?")
assert nwGUI.docEditor.setCursorLine(2)
qtbot.wait(stepDelay)
assert nwGUI.docEditor.getCursorPosition() == 9
qtbot.wait(stepDelay)
assert nwGUI.saveProject()
qtbot.wait(stepDelay)
@@ -396,6 +409,115 @@ def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI.closeMain()
# 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
def testAboutBox(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
@@ -610,3 +732,136 @@ def testSplitTool(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp):
# qtbot.stopForInteraction()
nwGUI.closeMain()
@pytest.mark.gui
def testDocAction(qtbot, nwTempGUI, nwLipsum, nwRef, nwTemp):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.waitForWindowShown(nwGUI)
qtbot.wait(stepDelay)
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.openProject(nwLipsum)
qtbot.wait(stepDelay)
# CUT = 3
# COPY = 4
# PASTE = 5
# SEL_ALL = 12
# SEL_PARA = 13
# FIND = 14
# REPLACE = 15
# GO_NEXT = 16
# GO_PREV = 17
# REPL_NEXT = 18
# Split By Chapter
assert nwGUI.openDocument("4c4f28287af27")
assert nwGUI.docEditor.setCursorPosition(30)
cleanText = nwGUI.docEditor.getText()[27:74]
# Bold
assert nwGUI.passDocumentAction(nwDocAction.BOLD)
assert nwGUI.docEditor.getText()[27:78] == "**Pellentesque** nec erat ut nulla posuere commodo."
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.BOLD)
assert nwGUI.docEditor.getText()[27:74] == cleanText
qtbot.wait(stepDelay)
# Italic
assert nwGUI.passDocumentAction(nwDocAction.ITALIC)
assert nwGUI.docEditor.getText()[27:76] == "*Pellentesque* nec erat ut nulla posuere commodo."
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.ITALIC)
assert nwGUI.docEditor.getText()[27:74] == cleanText
qtbot.wait(stepDelay)
# Bold-Italic
assert nwGUI.passDocumentAction(nwDocAction.BOLDITALIC)
assert nwGUI.docEditor.getText()[27:80] == "***Pellentesque*** nec erat ut nulla posuere commodo."
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.BOLDITALIC)
assert nwGUI.docEditor.getText()[27:74] == cleanText
qtbot.wait(stepDelay)
# Strikethrough
assert nwGUI.passDocumentAction(nwDocAction.STRIKE)
assert nwGUI.docEditor.getText()[27:78] == "~~Pellentesque~~ nec erat ut nulla posuere commodo."
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.STRIKE)
assert nwGUI.docEditor.getText()[27:74] == cleanText
qtbot.wait(stepDelay)
# Should get us back to plain
assert nwGUI.passDocumentAction(nwDocAction.BOLD)
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.ITALIC)
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.ITALIC)
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.BOLD)
assert nwGUI.docEditor.getText()[27:74] == cleanText
qtbot.wait(stepDelay)
# Equivalent of the above
assert nwGUI.passDocumentAction(nwDocAction.BOLDITALIC)
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.ITALIC)
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.BOLD)
assert nwGUI.docEditor.getText()[27:74] == cleanText
qtbot.wait(stepDelay)
# Double Quotes
assert nwGUI.passDocumentAction(nwDocAction.D_QUOTE)
assert nwGUI.docEditor.getText()[27:76] == "“Pellentesque” nec erat ut nulla posuere commodo."
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.UNDO)
assert nwGUI.docEditor.getText()[27:74] == cleanText
qtbot.wait(stepDelay)
# Single Quotes
assert nwGUI.passDocumentAction(nwDocAction.S_QUOTE)
assert nwGUI.docEditor.getText()[27:76] == "Pellentesque nec erat ut nulla posuere commodo."
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.UNDO)
assert nwGUI.docEditor.getText()[27:74] == cleanText
qtbot.wait(stepDelay)
# Block Formats
assert nwGUI.docEditor.setCursorPosition(30)
assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H1)
assert nwGUI.docEditor.getText()[27:76] == "# Pellentesque nec erat ut nulla posuere commodo."
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H2)
assert nwGUI.docEditor.getText()[27:77] == "## Pellentesque nec erat ut nulla posuere commodo."
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H3)
assert nwGUI.docEditor.getText()[27:78] == "### Pellentesque nec erat ut nulla posuere commodo."
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.BLOCK_H4)
assert nwGUI.docEditor.getText()[27:79] == "#### Pellentesque nec erat ut nulla posuere commodo."
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.BLOCK_TXT)
assert nwGUI.docEditor.getText()[27:74] == cleanText
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.BLOCK_COM)
assert nwGUI.docEditor.getText()[27:76] == "% Pellentesque nec erat ut nulla posuere commodo."
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.BLOCK_TXT)
assert nwGUI.docEditor.getText()[27:74] == cleanText
qtbot.wait(stepDelay)
# Undo/Redo
assert nwGUI.passDocumentAction(nwDocAction.UNDO)
assert nwGUI.docEditor.getText()[27:76] == "% Pellentesque nec erat ut nulla posuere commodo."
qtbot.wait(stepDelay)
assert nwGUI.passDocumentAction(nwDocAction.REDO)
assert nwGUI.docEditor.getText()[27:74] == cleanText
qtbot.wait(stepDelay)
# qtbot.stopForInteraction()
nwGUI.closeMain()