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