Fix merge conflicts

This commit is contained in:
Veronica K. B. Olsen
2020-02-27 00:19:18 +01:00
52 changed files with 1835 additions and 584 deletions
+4 -3
View File
@@ -1,10 +1,9 @@
os: linux os: linux
dist: xenial dist: bionic
services: services:
- xvfb - xvfb
language: python language: python
cache: bundler cache: bundler
sudo: required
addons: addons:
apt: apt:
packages: packages:
@@ -12,9 +11,11 @@ addons:
- python3-pyqt5 - python3-pyqt5
- python3-pyqt5.qtsvg - python3-pyqt5.qtsvg
python: python:
- "3.5" - "3.6"
- "3.7" - "3.7"
- "3.8"
install: install:
- pip install --upgrade pip
- pip install -r requirements.txt - pip install -r requirements.txt
# - pip install pytest-faulthandler # - pip install pytest-faulthandler
- pip install pytest-xvfb - pip install pytest-xvfb
+41
View File
@@ -1,5 +1,46 @@
# novelWriter ChangeLog # novelWriter ChangeLog
## Version 0.4.5 [2020-02-17]
**Features**
* A project can now be opened from the command line by providing the project path to the launching script. PRs #164 and #166.
**User Interface**
* Added functionality to split a document into a folder of multiple documents, and also to merge a folder of documents into a single document. PRs #159 and #163.
* It is now possible to permanently delete files from the Trash folder. This can be done file-by-file or by using the Empty Trash option in the menu. PRs #159 and #163.
* When running the spell checker, a wait cursor is displayed. This will alert the user that novelWriter is working on something when, for instance, a very large document is opened and initial spell checking is running. PR #158.
**Bug Fixes**
* Fixed a few keyboard shortcuts that were not working in distraction free mode. PR #157.
* Added a check to ensure the user does not drag and drop an item into the Orphaned Items folder. Since this folder is not an actual project item, novelWriter would crash when trying to change the dropped item's parent item to the Orphaned Items folder. Now, instead, the drop event is cancelled if the target folder is Orphaned Items. PR #163.
**Code Improvements**
* The way project files are saved has been altered slightly. When a project file or document file is saved, the data is first streamed to a temp file. Then the old storage file is renamed to .bak, and and the temp file is renamed to the correct storage file name. This ensures that the storage file is only replaced after a complete and successful write. PR #165.
* The cache folder has been removed. It was used to store the 10 most recent versions of the project file. Instead, the previous project file is renamed to .bak, and can be restored if opening from the latest project file fails. Any additional restore capabilities should be ensured by backup solutions, either the internal simple zip backup, or other third party tools. PR #165.
* The dependency on the Python package `appdirs` has been dropped. It was used only for extracting the path to the user's config folder, a feature which is also provided by Qt. PR #169.
## Version 0.4.4 [2020-02-17]
* Botched release. Replaced with 0.4.5
## Version 0.4.3 [2019-11-24]
**User Interface**
* Added keyboard shortcuts and menu entries for formatting headers, comments, and removing block formats. PR #155.
* Disable re-highlighting of open file when resizing window. This is potentially a slow process if the spell checker is on and the file is large. There is no need to do this just for reflowing text, so it is now disabled on resize events. PR #153 fixing issue #150.
* Improved the speed of the syntax highlighter by about 40% by not using regular expressions for highlighting block formats and by skipping empty lines entirely. PR #154.
**Bug Fixes**
* Fixed an issue when closing the import file dialog without selecting a file, the import would procede, but fail on file not found. The import is now cancelled when there is no file selected. PR #149.
* Fixed an issue with markdown export did not take into account hard line breaks. PR #152 fixing issue #151.
* Fixed a crash when running file status check when the project contains orphaned files. PR #152.
## Version 0.4.2 [2019-11-17] ## Version 0.4.2 [2019-11-17]
**User Interface** **User Interface**
+1 -3
View File
@@ -70,14 +70,13 @@ For the apt package manager on Debian systems, the following Python3 packages ar
* `python3-pyqt5` for the GUI * `python3-pyqt5` for the GUI
* `python3-pyqt5.qtsvg` may need to be installed separately * `python3-pyqt5.qtsvg` may need to be installed separately
* `python3-appdirs` for locating the system's config folder
* `python3-lxml` for writing project files * `python3-lxml` for writing project files
These are optional, but recommended: These are optional, but recommended:
* `python3-enchant` for better spell checking * `python3-enchant` for better spell checking
* `python3-latexcodec` for escaping unicode characters in LaTeX export * `python3-latexcodec` for escaping unicode characters in LaTeX export
* `python3-pandoc` for additional exports to Word, Open Office, eBooks, etc. * `python3-pypandoc` for additional exports to Word, Open Office, eBooks, etc.
Alternatively, the packages can be installed with `pip` by running Alternatively, the packages can be installed with `pip` by running
``` ```
@@ -88,7 +87,6 @@ in the application folder.
You can also do them one at a time, skipping the ones you don't need: You can also do them one at a time, skipping the ones you don't need:
``` ```
python3 -m pip install pyqt5 python3 -m pip install pyqt5
python3 -m pip install appdirs
python3 -m pip install lxml python3 -m pip install lxml
python3 -m pip install pyenchant python3 -m pip install pyenchant
python3 -m pip install latexcodec python3 -m pip install latexcodec
+3 -3
View File
@@ -20,13 +20,13 @@
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = "novelWriter" project = "novelWriter"
copyright = "2018-2019, Veronica Berglyd Olsen" 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.4.2" version = "0.4.5"
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = "0.4.2" release = "0.4.5"
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
+3 -3
View File
@@ -37,9 +37,9 @@ The editor also has a minimal set of keywords used for setting tags and referenc
"``## Title``", "Heading level two. The space after the # is mandatory." "``## Title``", "Heading level two. The space after the # is mandatory."
"``### Title``", "Heading level three. The space after the # is mandatory." "``### Title``", "Heading level three. The space after the # is mandatory."
"``#### Title``", "Heading level four. The space after the # is mandatory." "``#### Title``", "Heading level four. The space after the # is mandatory."
"``**text**``", "The text is renderred as bold text." "``**text**``", "The text is rendered as bold text."
"``_text_``", "The text is renderred as italics text." "``_text_``", "The text is rendered as italicized text."
"``__text__``", "The text is renderred as underlined text." "``__text__``", "The text is rendered as underlined text."
"``% text...``", "A comment. The text is not exported, seen in viewer, or counted towards word counts." "``% text...``", "A comment. The text is not exported, seen in viewer, or counted towards word counts."
"``@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."
+2 -2
View File
@@ -22,7 +22,7 @@ These other root folder types are intended for your notes on the various element
Using these is of course entirely optional. Using these is of course entirely optional.
A new project will not have all of the root folders present, but you can add the ones you want from :menuselection:`Project --> Create Root Folder`. A new project will not have all of the root folders present, but you can add the ones you want from :menuselection:`Project --> Create Root Folder`.
The root folders are intended for the follwing use, but aside from the Novel folder, not restrictions apply. The root folders are intended for the following use, but aside from the Novel folder, no restrictions apply.
* **Novel:** The root folder of all text that goes into the final novel. * **Novel:** The root folder of all text that goes into the final novel.
This class of files have other rules and features than other files in the project. This class of files have other rules and features than other files in the project.
@@ -44,7 +44,7 @@ Orphaned Documents
------------------ ------------------
In the event the editor crashes or otherwise exits without saving the project state, files that have been added to the project tree and are saved to disk will appear in a special "Orphaned Items" root folder next time the application is started. In the event the editor crashes or otherwise exits without saving the project state, files that have been added to the project tree and are saved to disk will appear in a special "Orphaned Items" root folder next time the application is started.
These orphaned files will not have any meta data associated with them, so the label and other information has to be set again, and the files moved back to the correct location in the project. These orphaned files will not have any meta data associated with them, so the label and other information will have to be set again, and the files moved back to the correct location in the project.
Using Folders in the Project Tree Using Folders in the Project Tree
--------------------------------- ---------------------------------
+2 -3
View File
@@ -6,8 +6,8 @@ You can download novelWriter from https://github.com/vkbo/novelWriter/releases
Latest version is |version|: Latest version is |version|:
* ZIP file: https://github.com/vkbo/novelWriter/archive/v0.3.2.zip * ZIP file: https://github.com/vkbo/novelWriter/archive/v0.4.5.zip
* TAR file: https://github.com/vkbo/novelWriter/archive/v0.3.2.tar.gz * TAR file: https://github.com/vkbo/novelWriter/archive/v0.4.5.tar.gz
Extract the archive to a location of your choice. Extract the archive to a location of your choice.
@@ -26,7 +26,6 @@ On some operating systems you need to use ``python3`` instead of ``python``.
The following Python packages are required to run novelWriter: The following Python packages are required to run novelWriter:
* ``pyqt5`` for the GUI * ``pyqt5`` for the GUI
* ``appdirs`` for locating the system's config folder
* ``lxml`` for writing project files * ``lxml`` for writing project files
.. note:: .. note::
+1 -1
View File
@@ -28,7 +28,7 @@ The project XML file is suitable for diff tools and version control, although a
Project Documents Project Documents
----------------- -----------------
The project documents are saved in folders staring with ``data_``. The project documents are saved in folders starting with ``data_``.
Each document has a file handle taken from the first 13 characters of a SHA256 hash of the system time when the file was first created. Each document has a file handle taken from the first 13 characters of a SHA256 hash of the system time when the file was first created.
The documents are saved with a folder and filename derived from this hash. The documents are saved with a folder and filename derived from this hash.
If you wish to find the physical location of a file in the project, you can either look it up in the project XML file, or select :menuselection:`Document --> Show File Details` in the menu when having the document open. If you wish to find the physical location of a file in the project, you can either look it up in the project XML file, or select :menuselection:`Document --> Show File Details` in the menu when having the document open.
+2 -2
View File
@@ -63,8 +63,8 @@ else:
instOpt = [ instOpt = [
"--name=novelWriter", "--name=novelWriter",
"--onefile", "--onefile",
"--add-data=%s%s%s" % (os.path.join("nw", "themes"), dotDot,"themes"), "--add-data=%s%s%s" % (os.path.join("nw", "assets", "themes"), dotDot,"themes"),
"--add-data=%s%s%s" % (os.path.join("nw", "graphics"),dotDot,"graphics"), "--add-data=%s%s%s" % (os.path.join("nw", "assets", "graphics"), dotDot,"graphics"),
"--icon=%s" % os.path.join("nw", "assets", "icons", "novelWriter.ico"), "--icon=%s" % os.path.join("nw", "assets", "icons", "novelWriter.ico"),
] ]
if buildWindowed: if buildWindowed:
+2 -8
View File
@@ -3,8 +3,8 @@
import sys import sys
if sys.hexversion < 0x030500F0: if sys.hexversion < 0x030600F0:
print("ERROR: At least Python 3.5 is required") print("ERROR: At least Python 3.6 is required")
sys.exit(1) sys.exit(1)
try: try:
@@ -27,12 +27,6 @@ except:
print("ERROR: Failed to load dependency python3-lxml") print("ERROR: Failed to load dependency python3-lxml")
sys.exit(1) sys.exit(1)
try:
import appdirs
except:
print("ERROR: Failed to load dependency python3-appdirs")
sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":
import nw import nw
nw.main(sys.argv[1:]) nw.main(sys.argv[1:])
+19 -14
View File
@@ -23,10 +23,10 @@ from nw.config import Config
__package__ = "novelWriter" __package__ = "novelWriter"
__author__ = "Veronica Berglyd Olsen" __author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 20182019, Veronica Berglyd Olsen" __copyright__ = "Copyright 20182020, Veronica Berglyd Olsen"
__license__ = "GPLv3" __license__ = "GPLv3"
__version__ = "0.4.2" __version__ = "0.4.5"
__date__ = "2019-11-17" __date__ = "2020-02-17"
__maintainer__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net" __email__ = "code@vkbo.net"
__status__ = "Development" __status__ = "Development"
@@ -77,17 +77,17 @@ def main(sysArgs=None):
sysArgs = sys.argv[1:] sysArgs = sys.argv[1:]
# Valid Input Options # Valid Input Options
shortOpt = "hdiqtl:v" shortOpt = "hdiql:v"
longOpt = [ longOpt = [
"help", "help",
"debug", "debug",
"info", "info",
"verbose", "verbose",
"quiet", "quiet",
"time",
"logfile=", "logfile=",
"version", "version",
"config=", "config=",
"data=",
"testmode", "testmode",
"style=", "style=",
] ]
@@ -103,10 +103,10 @@ def main(sysArgs=None):
" -d, --debug Print debug output.\n" " -d, --debug Print debug output.\n"
" --verbose Increase verbosity of debug output.\n" " --verbose Increase verbosity of debug output.\n"
" -q, --quiet Disable output to command line. Does not affect log file.\n" " -q, --quiet Disable output to command line. Does not affect log file.\n"
" -t, --time Shows time stamp in logging output.\n"
" -l, --logfile= Specify log file.\n" " -l, --logfile= Specify log file.\n"
" --style= Set Qt5 style flag. Defaults to 'Fusion'.\n" " --style= Set Qt5 style flag. Defaults to 'Fusion'.\n"
" --config= Alternative config file.\n" " --config= Alternative config file.\n"
" --data= Alternative user data path.\n"
" --headless Do not display GUI. Useful for testing scripts.\n" " --headless Do not display GUI. Useful for testing scripts.\n"
).format( ).format(
appname = __package__, appname = __package__,
@@ -118,22 +118,25 @@ def main(sysArgs=None):
# Defaults # Defaults
debugLevel = logging.WARN debugLevel = logging.WARN
debugStr = "{levelname:8} {message:}" debugStr = "{levelname:8} {message:}"
timeStr = "[{asctime:}] "
logFile = "" logFile = ""
toFile = False toFile = False
toStd = True toStd = True
showTime = False
confPath = None confPath = None
dataPath = None
testMode = False testMode = False
qtStyle = "Fusion" qtStyle = "Fusion"
cmdOpen = None
# Parse Options # Parse Options
try: try:
inOpts, inArgs = getopt.getopt(sysArgs,shortOpt,longOpt) inOpts, inRemain = getopt.getopt(sysArgs,shortOpt,longOpt)
except getopt.GetoptError: except getopt.GetoptError:
print(helpMsg) print(helpMsg)
sys.exit(2) sys.exit(2)
if len(inRemain) > 0:
cmdOpen = inRemain[0]
for inOpt, inArg in inOpts: for inOpt, inArg in inOpts:
if inOpt in ("-h","--help"): if inOpt in ("-h","--help"):
print(helpMsg) print(helpMsg)
@@ -145,7 +148,7 @@ def main(sysArgs=None):
debugLevel = logging.INFO debugLevel = logging.INFO
elif inOpt in ("-d", "--debug"): elif inOpt in ("-d", "--debug"):
debugLevel = logging.DEBUG debugLevel = logging.DEBUG
debugStr = "{name:>30}:{lineno:<4d} {levelname:8} {message:}" debugStr = "[{asctime:}] {name:>30}:{lineno:<4d} {levelname:8} {message:}"
elif inOpt in ("-l","--logfile"): elif inOpt in ("-l","--logfile"):
logFile = inArg logFile = inArg
toFile = True toFile = True
@@ -153,21 +156,21 @@ def main(sysArgs=None):
toStd = False toStd = False
elif inOpt in ("--verbose"): elif inOpt in ("--verbose"):
debugLevel = VERBOSE debugLevel = VERBOSE
elif inOpt in ("-t","--time"):
showTime = True
elif inOpt in ("--style"): elif inOpt in ("--style"):
qtStyle = inArg qtStyle = inArg
elif inOpt in ("--config"): elif inOpt in ("--config"):
confPath = inArg confPath = inArg
elif inOpt in ("--data"):
dataPath = inArg
elif inOpt in ("--testmode"): elif inOpt in ("--testmode"):
testMode = True testMode = True
# Set Config Options # Set Config Options
CONFIG.showGUI = not testMode CONFIG.showGUI = not testMode
CONFIG.debugInfo = debugLevel < logging.INFO CONFIG.debugInfo = debugLevel < logging.INFO
CONFIG.cmdOpen = cmdOpen
# Set Logging # Set Logging
if showTime: debugStr = timeStr+debugStr
logFmt = logging.Formatter(fmt=debugStr,datefmt="%Y-%m-%d %H:%M:%S",style="{") logFmt = logging.Formatter(fmt=debugStr,datefmt="%Y-%m-%d %H:%M:%S",style="{")
if not logFile == "" and toFile: if not logFile == "" and toFile:
@@ -189,13 +192,15 @@ def main(sysArgs=None):
logger.setLevel(debugLevel) logger.setLevel(debugLevel)
CONFIG.initConfig(confPath) CONFIG.initConfig(confPath, dataPath)
if testMode: if testMode:
nwGUI = GuiMain() nwGUI = GuiMain()
return nwGUI return nwGUI
else: else:
nwApp = QApplication([__package__,("-style=%s" % qtStyle)]) nwApp = QApplication([__package__,("-style=%s" % qtStyle)])
nwApp.setApplicationName(__package__)
nwApp.setApplicationVersion(__version__)
nwGUI = GuiMain() nwGUI = GuiMain()
sys.exit(nwApp.exec_()) sys.exit(nwApp.exec_())
@@ -1,3 +1,8 @@
FROM ICON SET: Typicons FROM ICON SET: Typicons
LICENSE: Creative Commons (Attribution-Share Alike 3.0 Unported) LICENSE: Creative Commons (Attribution-Share Alike 3.0 Unported)
https://creativecommons.org/licenses/by-sa/3.0/ https://creativecommons.org/licenses/by-sa/3.0/
Apllies to:
export.svg
merge.svg
split.svg
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg4"
viewBox="0 0 20 20"
height="20"
width="20"
version="1.2">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<path
style="fill:#000000;fill-opacity:0.35294118;stroke:none;stroke-opacity:1"
id="path2"
d="m 16,5.816 v 1.851 c 0,1.93 -1.57,3.5 -3.5,3.5 -0.827,0 -1.5,0.673 -1.5,1.5 v 1.517 c 1.161,0.415 2,1.514 2,2.816 0,1.654 -1.346,3 -3,3 C 8.346,20 7,18.654 7,17 7,15.698 7.839,14.599 9,14.185 V 12.667 C 9,11.84 8.327,11.167 7.5,11.167 5.57,11.167 4,9.597 4,7.667 V 5.816 C 2.839,5.401 2,4.302 2,3 2,1.346 3.346,0 5,0 6.654,0 8,1.346 8,3 8,4.302 7.161,5.401 6,5.816 v 1.851 c 0,0.827 0.673,1.5 1.5,1.5 0.979,0 1.864,0.407 2.5,1.058 0.636,-0.651 1.521,-1.058 2.5,-1.058 0.827,0 1.5,-0.673 1.5,-1.5 V 5.816 C 12.839,5.401 12,4.302 12,3 c 0,-1.654 1.346,-3 3,-3 1.654,0 3,1.346 3,3 0,1.302 -0.839,2.401 -2,2.816 z M 5,2 C 4.448,2 4,2.449 4,3 4,3.551 4.448,4 5,4 5.552,4 6,3.551 6,3 6,2.449 5.552,2 5,2 Z m 5,16 c 0.552,0 1,-0.449 1,-1 0,-0.551 -0.448,-1 -1,-1 -0.552,0 -1,0.449 -1,1 0,0.551 0.448,1 1,1 z M 15,2 c -0.552,0 -1,0.449 -1,1 0,0.551 0.448,1 1,1 0.552,0 1,-0.449 1,-1 0,-0.551 -0.448,-1 -1,-1 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg4"
viewBox="0 0 20 20"
height="20"
width="20"
version="1.2">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<path
style="fill:#000000;fill-opacity:0.35294118;stroke:none;stroke-opacity:1"
id="path2"
d="m 16,14.184 v -1.851 c 0,-1.93 -1.57,-3.5 -3.5,-3.5 -0.827,0 -1.5,-0.673 -1.5,-1.5 V 5.816 C 12.161,5.401 13,4.302 13,3 13,1.346 11.654,0 10,0 8.346,0 7,1.346 7,3 7,4.302 7.839,5.401 9,5.815 v 1.518 c 0,0.827 -0.673,1.5 -1.5,1.5 -1.93,0 -3.5,1.57 -3.5,3.5 v 1.851 C 2.839,14.599 2,15.698 2,17 c 0,1.654 1.346,3 3,3 1.654,0 3,-1.346 3,-3 0,-1.302 -0.839,-2.401 -2,-2.816 v -1.851 c 0,-0.827 0.673,-1.5 1.5,-1.5 0.979,0 1.864,-0.407 2.5,-1.058 0.636,0.651 1.521,1.058 2.5,1.058 0.827,0 1.5,0.673 1.5,1.5 v 1.851 c -1.161,0.415 -2,1.514 -2,2.816 0,1.654 1.346,3 3,3 1.654,0 3,-1.346 3,-3 0,-1.302 -0.839,-2.401 -2,-2.816 z M 5,18 c -0.552,0 -1,-0.449 -1,-1 0,-0.551 0.448,-1 1,-1 0.552,0 1,0.449 1,1 0,0.551 -0.448,1 -1,1 z M 10,2 c 0.552,0 1,0.449 1,1 0,0.551 -0.448,1 -1,1 C 9.448,4 9,3.551 9,3 9,2.449 9.448,2 10,2 Z m 5,16 c -0.552,0 -1,-0.449 -1,-1 0,-0.551 0.448,-1 1,-1 0.552,0 1,0.449 1,1 0,0.551 -0.448,1 -1,1 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+19
View File
@@ -88,6 +88,25 @@ def colRange(rgbStart, rgbEnd, nStep):
return retCol return retCol
def formatInt(theInt):
"""Formats an integer with k, M, G etc.
"""
postFix = ["k","M","G","T","P","E"]
theVal = float(theInt)
if theVal > 1000.0:
for pF in postFix:
theVal /= 1000.0
if theVal < 1000.0:
if theVal < 10.0:
return "%4.2f%s" % (theVal,pF)
elif theVal < 100.0:
return "%4.1f%s" % (theVal,pF)
else:
return "%3.0f%s" % (theVal,pF)
return "%d" % theInt
def splitVersionNumber(vString): def splitVersionNumber(vString):
""" Splits a version string on the form aa.bb.cc into major, minor """ Splits a version string on the form aa.bb.cc into major, minor
and patch, and computes an integer value aabbcc. and patch, and computes an integer value aabbcc.
+172 -41
View File
@@ -12,15 +12,15 @@
import logging import logging
import configparser import configparser
import json
import sys import sys
import nw import nw
from os import path, mkdir, makedirs from os import path, mkdir, unlink, rename
from appdirs import user_config_dir
from datetime import datetime from datetime import datetime
from PyQt5.Qt import PYQT_VERSION_STR from PyQt5.Qt import PYQT_VERSION_STR
from PyQt5.QtCore import QT_VERSION_STR from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths, QSysInfo
from nw.constants import nwFiles, nwUnicode from nw.constants import nwFiles, nwUnicode
from nw.common import splitVersionNumber from nw.common import splitVersionNumber
@@ -41,10 +41,16 @@ class Config:
self.appHandle = nw.__package__.lower() self.appHandle = nw.__package__.lower()
self.showGUI = True self.showGUI = True
self.debugInfo = False self.debugInfo = False
self.cmdOpen = None
# Config Error Handling
self.hasError = False
self.errData = []
# Set Paths # Set Paths
self.confPath = None self.confPath = None
self.confFile = None self.confFile = None
self.dataPath = None
self.homePath = None self.homePath = None
self.lastPath = None self.lastPath = None
self.appPath = None self.appPath = None
@@ -57,7 +63,7 @@ class Config:
self.iconPath = None self.iconPath = None
# Set default values # Set default values
self.confChanged = False self.confChanged = False
## General ## General
self.guiTheme = "default" self.guiTheme = "default"
@@ -111,9 +117,6 @@ class Config:
self.showRefPanel = True self.showRefPanel = True
self.viewComments = True self.viewComments = True
## Path
self.recentList = [""]*10
# Check Qt5 Versions # Check Qt5 Versions
verQt = splitVersionNumber(QT_VERSION_STR) verQt = splitVersionNumber(QT_VERSION_STR)
self.verQtString = QT_VERSION_STR self.verQtString = QT_VERSION_STR
@@ -153,24 +156,48 @@ class Config:
else: else:
self.osUnknown = True self.osUnknown = True
# Other System Info
self.hostName = QSysInfo.machineHostName()
self.kernelVer = QSysInfo.kernelVersion()
# Packages # Packages
self.hasEnchant = False self.hasEnchant = False
self.hasSymSpell = False self.hasSymSpell = False
# Recent Cache
self.recentProj = {}
return return
## ##
# Actions # Actions
## ##
def initConfig(self, confPath=None): def initConfig(self, confPath=None, dataPath=None):
"""Initialise the config class. The manual setting of confPath
and dataPath is mainly intended for the test suite.
"""
if confPath is None: if confPath is None:
self.confPath = user_config_dir(self.appHandle) confRoot = QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)
self.confPath = path.join(path.abspath(confRoot), self.appHandle)
else: else:
logger.info("Setting config from alternative path: %s" % confPath) logger.info("Setting config from alternative path: %s" % confPath)
self.confPath = confPath self.confPath = confPath
if dataPath is None:
if self.verQtValue >= 50400:
dataRoot = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
else:
dataRoot = QStandardPaths.writableLocation(QStandardPaths.DataLocation)
self.dataPath = path.join(path.abspath(dataRoot), self.appHandle)
else:
logger.info("Setting data path from alternative path: %s" % dataPath)
self.dataPath = dataPath
logger.verbose("Config path: %s" % self.confPath)
logger.verbose("Data path: %s" % self.dataPath)
self.confFile = self.appHandle+".conf" self.confFile = self.appHandle+".conf"
self.homePath = path.expanduser("~") self.homePath = path.expanduser("~")
self.lastPath = self.homePath self.lastPath = self.homePath
@@ -185,20 +212,42 @@ class Config:
# If config folder does not exist, make it. # If config folder does not exist, make it.
# This assumes that the os config folder itself exists. # This assumes that the os config folder itself exists.
if self.osWindows: if not path.isdir(self.confPath):
if not path.isdir(self.confPath): try:
makedirs(self.confPath)
else:
if not path.isdir(self.confPath):
mkdir(self.confPath) mkdir(self.confPath)
except Exception as e:
logger.error("Could not create folder: %s" % self.confPath)
logger.error(str(e))
self.hasError = True
self.errData.append("Could not create folder: %s" % self.confPath)
self.errData.append(str(e))
self.confPath = None
# Check if config file exists # Check if config file exists
if path.isfile(path.join(self.confPath,self.confFile)): if self.confPath is not None:
# If it exists, load it if path.isfile(path.join(self.confPath,self.confFile)):
self.loadConfig() # If it exists, load it
else: self.loadConfig()
# If it does not exist, save a copy of the default values else:
self.saveConfig() # If it does not exist, save a copy of the default values
self.saveConfig()
# If data folder does not exist, make it.
# This assumes that the os data folder itself exists.
if self.dataPath is not None:
if not path.isdir(self.dataPath):
try:
mkdir(self.dataPath)
except Exception as e:
logger.error("Could not create folder: %s" % self.dataPath)
logger.error(str(e))
self.hasError = True
self.errData.append("Could not create folder: %s" % self.dataPath)
self.errData.append(str(e))
self.dataPath = None
# Load recent projects cache
self.loadRecentCache()
# Check the availability of optional packages # Check the availability of optional packages
self._checkOptionalPackages() self._checkOptionalPackages()
@@ -220,6 +269,10 @@ class Config:
) )
except Exception as e: except Exception as e:
logger.error("Could not load config file") logger.error("Could not load config file")
logger.error(str(e))
self.hasError = True
self.errData.append("Could not load config file")
self.errData.append(str(e))
return False return False
## Main ## Main
@@ -353,10 +406,6 @@ class Config:
self.lastPath = self._parseLine( self.lastPath = self._parseLine(
cnfParse, cnfSec, "lastpath", self.CNF_STR, self.lastPath cnfParse, cnfSec, "lastpath", self.CNF_STR, self.lastPath
) )
for i in range(10):
self.recentList[i] = self._parseLine(
cnfParse, cnfSec, "recent%d" % i,self.CNF_STR, self.recentList[i]
)
# Check Certain Values for None # Check Certain Values for None
self.spellLanguage = self._checkNone(self.spellLanguage) self.spellLanguage = self._checkNone(self.spellLanguage)
@@ -435,8 +484,6 @@ class Config:
cnfSec = "Path" cnfSec = "Path"
cnfParse.add_section(cnfSec) cnfParse.add_section(cnfSec)
cnfParse.set(cnfSec,"lastpath", str(self.lastPath)) cnfParse.set(cnfSec,"lastpath", str(self.lastPath))
for i in range(10):
cnfParse.set(cnfSec,"recent%d" % i, str(self.recentList[i]))
# Write config file # Write config file
try: try:
@@ -444,24 +491,93 @@ class Config:
self.confChanged = False self.confChanged = False
except Exception as e: except Exception as e:
logger.error("Could not save config file") logger.error("Could not save config file")
logger.error(str(e))
self.hasError = True
self.errData.append("Could not save config file")
self.errData.append(str(e))
return False return False
return True return True
## def loadRecentCache(self):
# Setters """Load the cache file for recent projects.
## """
def setRecent(self, recentPath): if self.dataPath is None:
if recentPath == "": return return False
if recentPath in self.recentList[0:10]:
self.recentList.remove(recentPath)
self.recentList.insert(0,recentPath)
return
def clearRecent(self): cacheFile = path.join(self.dataPath, nwFiles.RECENT_FILE)
self.recentList = [""]*10 self.recentProj = {}
return
if path.isfile(cacheFile):
try:
with open(cacheFile, mode="r", encoding="utf8") as inFile:
theJson = inFile.read()
theData = json.loads(theJson)
for projPath in theData.keys():
theEntry = theData[projPath]
theTitle = ""
lastTime = 0
wordCount = 0
if "title" in theEntry.keys():
theTitle = theEntry["title"]
if "time" in theEntry.keys():
lastTime = int(theEntry["time"])
if "words" in theEntry.keys():
wordCount = int(theEntry["words"])
self.recentProj[projPath] = {
"title" : theTitle,
"time" : lastTime,
"words" : wordCount,
}
except Exception as e:
self.hasError = True
self.errData.append("Could not load recent project cache")
self.errData.append(str(e))
return False
return True
def saveRecentCache(self):
"""Save the cache dictionary of recent projects.
"""
if self.dataPath is None:
return False
cacheFile = path.join(self.dataPath, nwFiles.RECENT_FILE)
cacheTemp = path.join(self.dataPath, nwFiles.RECENT_FILE+"~")
try:
with open(cacheTemp, mode="w+", encoding="utf8") as outFile:
outFile.write(json.dumps(self.recentProj, indent=2))
except Exception as e:
self.hasError = True
self.errData.append("Could not save recent project cache")
self.errData.append(str(e))
return False
if path.isfile(cacheFile):
unlink(cacheFile)
rename(cacheTemp, cacheFile)
return True
def updateRecentCache(self, projPath, projTitle, wordCount, saveTime):
"""Add or update recent cache information o9n a given project.
"""
self.recentProj[path.abspath(projPath)] = {
"title" : projTitle,
"time" : int(saveTime),
"words" : int(wordCount),
}
return True
##
# Setters and Getters
##
def setConfPath(self, newPath): def setConfPath(self, newPath):
if newPath is None: if newPath is None:
@@ -473,6 +589,15 @@ class Config:
self.confFile = path.basename(newPath) self.confFile = path.basename(newPath)
return True return True
def setDataPath(self, newPath):
if newPath is None:
return True
if not path.isdir(newPath):
logger.error("Config: Path not found. Using default data path instead.")
return False
self.dataPath = path.abspath(newPath)
return True
def setLastPath(self, lastPath): def setLastPath(self, lastPath):
if lastPath is None or lastPath == "": if lastPath is None or lastPath == "":
self.lastPath = "" self.lastPath = ""
@@ -507,12 +632,18 @@ class Config:
def setShowRefPanel(self, checkState): def setShowRefPanel(self, checkState):
self.showRefPanel = checkState self.showRefPanel = checkState
self.confChanged = True self.confChanged = True
return return self.showRefPanel
def setViewComments(self, checkState): def setViewComments(self, checkState):
self.viewComments = checkState self.viewComments = checkState
self.confChanged = True self.confChanged = True
return return self.viewComments
def getErrData(self):
errMessage = "<br>".join(self.errData)
self.hasError = False
self.errData = []
return errMessage
## ##
# Internal Functions # Internal Functions
@@ -550,7 +681,7 @@ class Config:
if checkVal is None: if checkVal is None:
return None return None
if isinstance(checkVal, str): if isinstance(checkVal, str):
if checkVal.lower == "none": if checkVal.lower() == "none":
return None return None
return checkVal return checkVal
+3 -8
View File
@@ -22,13 +22,12 @@ class nwFiles():
APP_ICON = "novelWriter.svg" APP_ICON = "novelWriter.svg"
PROJ_FILE = "nwProject.nwx" PROJ_FILE = "nwProject.nwx"
PROJ_COUNT = "projCount.txt"
PROJ_DICT = "wordlist.txt" PROJ_DICT = "wordlist.txt"
PROJ_LOCK = "nwProject.lock"
SESS_INFO = "sessionInfo.log" SESS_INFO = "sessionInfo.log"
INDEX_FILE = "tagsIndex.json" INDEX_FILE = "tagsIndex.json"
EXPORT_OPT = "exportOptions.json" OPTS_FILE = "guiOptions.json"
OUTLINE_OPT = "outlineOptions.json" RECENT_FILE = "recentProjects.json"
SLOG_OPT = "sessionLogOptions.json"
# END Class nwFiles # END Class nwFiles
@@ -116,10 +115,6 @@ class nwDependencies():
"site" : "", "site" : "",
"docs" : "", "docs" : "",
}, },
"appdirs" : {
"site" : "",
"docs" : "",
},
"lxml" : { "lxml" : {
"site" : "", "site" : "",
"docs" : "", "docs" : "",
+6
View File
@@ -8,9 +8,12 @@ from nw.gui.theme import GuiTheme
# Dialogs # Dialogs
from nw.gui.dialogs.configeditor import GuiConfigEditor from nw.gui.dialogs.configeditor import GuiConfigEditor
from nw.gui.dialogs.docmerge import GuiDocMerge
from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.export import GuiExport from nw.gui.dialogs.export import GuiExport
from nw.gui.dialogs.itemeditor import GuiItemEditor from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor from nw.gui.dialogs.projecteditor import GuiProjectEditor
from nw.gui.dialogs.projectload import GuiProjectLoad
from nw.gui.dialogs.sessionlog import GuiSessionLogView from nw.gui.dialogs.sessionlog import GuiSessionLogView
# GUI Elements # GUI Elements
@@ -33,9 +36,12 @@ __all__ = [
"GuiMainStatus", "GuiMainStatus",
"GuiTheme", "GuiTheme",
"GuiConfigEditor", "GuiConfigEditor",
"GuiDocMerge",
"GuiDocSplit",
"GuiExport", "GuiExport",
"GuiItemEditor", "GuiItemEditor",
"GuiProjectEditor", "GuiProjectEditor",
"GuiProjectLoad",
"GuiSessionLogView", "GuiSessionLogView",
"GuiDocDetails", "GuiDocDetails",
"GuiDocEditor", "GuiDocEditor",
+6
View File
@@ -1,15 +1,21 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from nw.gui.dialogs.configeditor import GuiConfigEditor from nw.gui.dialogs.configeditor import GuiConfigEditor
from nw.gui.dialogs.docmerge import GuiDocMerge
from nw.gui.dialogs.docsplit import GuiDocSplit
from nw.gui.dialogs.export import GuiExport from nw.gui.dialogs.export import GuiExport
from nw.gui.dialogs.itemeditor import GuiItemEditor from nw.gui.dialogs.itemeditor import GuiItemEditor
from nw.gui.dialogs.projecteditor import GuiProjectEditor from nw.gui.dialogs.projecteditor import GuiProjectEditor
from nw.gui.dialogs.projectload import GuiProjectLoad
from nw.gui.dialogs.sessionlog import GuiSessionLogView from nw.gui.dialogs.sessionlog import GuiSessionLogView
__all__ = [ __all__ = [
"GuiConfigEditor", "GuiConfigEditor",
"GuiDocMerge",
"GuiDocSplit",
"GuiExport", "GuiExport",
"GuiItemEditor", "GuiItemEditor",
"GuiProjectEditor", "GuiProjectEditor",
"GuiProjectLoad",
"GuiSessionLogView", "GuiSessionLogView",
] ]
+157
View File
@@ -0,0 +1,157 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Doc Merge
novelWriter GUI Doc Merge
=============================
Tool for merging multiple documents to one
File History:
Created: 2020-01-23 [0.4.3]
"""
import logging
import nw
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton,
QListWidget, QAbstractItemView, QListWidgetItem
)
from nw.constants import nwAlert, nwItemType
from nw.project import NWDoc
logger = logging.getLogger(__name__)
class GuiDocMerge(QDialog):
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiDocMerge ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
self.sourceItem = None
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Merge Documents")
self.setLayout(self.outerBox)
self.guiDeco = self.theParent.theTheme.loadDecoration("merge",(64,64))
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.doMergeForm = QGridLayout()
self.doMergeForm.setContentsMargins(0,0,0,0)
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
self.mergeButton = QPushButton("Merge")
self.mergeButton.clicked.connect(self._doMerge)
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
self.doMergeForm.addWidget(self.listBox, 0, 0, 1, 3)
self.doMergeForm.addWidget(self.mergeButton, 1, 1)
self.doMergeForm.addWidget(self.closeButton, 1, 2)
self.innerBox.addLayout(self.doMergeForm)
self.rejected.connect(self._doClose)
self.show()
self._populateList()
logger.debug("GuiDocMerge initialisation complete")
return
##
# Buttons
##
def _doMerge(self):
"""Perform the merge of the files in the selected folder, and
create a new file in the same parent folder. The old files are
not removed in the merge process, and must be deleted manually.
"""
logger.verbose("GuiDocMerge merge button clicked")
finalOrder = []
for i in range(self.listBox.count()):
finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
theDoc = NWDoc(self.theProject, self.theParent)
theText = ""
for tHandle in finalOrder:
theText += theDoc.openDocument(tHandle, False).rstrip()
theText += "\n\n"
if self.sourceItem is None:
self.theParent.makeAlert((
"Cannot parse source item."
), nwAlert.ERROR)
return
srcItem = self.theProject.getItem(self.sourceItem)
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.parHandle)
self.theParent.treeView.revealTreeItem(nHandle)
theDoc.openDocument(nHandle, False)
theDoc.saveDocument(theText)
self.theParent.openDocument(nHandle)
self.close()
return
def _doClose(self):
"""Close the dialog window without doing anything.
"""
logger.verbose("GuiDocMerge close button clicked")
self.close()
return
##
# Internal Functions
##
def _populateList(self):
"""Get the item selected in the tree, check that it is a folder,
and try to find all files associated with it. The valid files
are then added to the list view in order. The list itself can be
reordered by the user.
"""
tHandle = self.theParent.treeView.getSelectedHandle()
self.sourceItem = tHandle
if tHandle is None:
return
nwItem = self.theProject.getItem(tHandle)
if nwItem is None:
return
if nwItem.itemType is not nwItemType.FOLDER:
self.theParent.makeAlert((
"Element selected in the project tree must be a folder."
), nwAlert.ERROR)
return
for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle):
newItem = QListWidgetItem()
nwItem = self.theProject.getItem(sHandle)
if nwItem.itemType is not nwItemType.FILE:
continue
newItem.setText(nwItem.itemName)
newItem.setData(Qt.UserRole, sHandle)
self.listBox.addItem(newItem)
return
# END Class GuiDocMerge
+241
View File
@@ -0,0 +1,241 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Doc Split
novelWriter GUI Doc Split
=============================
Tool for splitting a single document into multiple documents
File History:
Created: 2020-02-01 [0.4.3]
"""
import logging
import nw
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QComboBox,
QListWidget, QAbstractItemView, QListWidgetItem
)
from nw.constants import nwAlert, nwItemType, nwItemClass, nwItemLayout
from nw.project import NWDoc
logger = logging.getLogger(__name__)
class GuiDocSplit(QDialog):
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiDocSplit ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
self.optState = self.theProject.optState
self.sourceItem = None
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Split Document")
self.setLayout(self.outerBox)
self.guiDeco = self.theParent.theTheme.loadDecoration("split",(64,64))
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.doMergeForm = QGridLayout()
self.doMergeForm.setContentsMargins(0,0,0,0)
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
self.splitLevel = QComboBox(self)
self.splitLevel.addItem("Split on Header Level 1 (Title)", 1)
self.splitLevel.addItem("Split up to Header Level 2 (Chapter)", 2)
self.splitLevel.addItem("Split up to Header Level 3 (Scene)", 3)
self.splitLevel.addItem("Split up to Header Level 4 (Section)", 4)
spIndex = self.splitLevel.findData(
self.optState.getInt("GuiDocSplit", "spLevel", 3)
)
if spIndex != -1:
self.splitLevel.setCurrentIndex(spIndex)
self.splitLevel.currentIndexChanged.connect(self._populateList)
self.splitButton = QPushButton("Split")
self.splitButton.clicked.connect(self._doSplit)
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
self.doMergeForm.addWidget(self.listBox, 0, 0, 1, 3)
self.doMergeForm.addWidget(self.splitLevel, 1, 0, 1, 3)
self.doMergeForm.addWidget(self.splitButton, 2, 1)
self.doMergeForm.addWidget(self.closeButton, 2, 2)
self.innerBox.addLayout(self.doMergeForm)
self.rejected.connect(self._doClose)
self.show()
self._populateList()
logger.debug("GuiDocSplit initialisation complete")
return
##
# Buttons
##
def _doSplit(self):
"""Perform the split of the file, create a new folder in the
same parent folder, and multiple files depending on split level
settings. The old file is not removed in the merge process, and
must be deleted manually.
"""
logger.verbose("GuiDocSplit split button clicked")
if self.sourceItem is None:
self.theParent.makeAlert((
"No source document selected. Nothing to do."
), nwAlert.ERROR)
return
srcItem = self.theProject.getItem(self.sourceItem)
if srcItem is None:
self.theParent.makeAlert((
"Could not parse source document."
), nwAlert.ERROR)
return
theDoc = NWDoc(self.theProject, self.theParent)
theText = theDoc.openDocument(self.sourceItem, False)
theLines = theText.splitlines()
nLines = len(theLines)
theLines.insert(0, "%Split Doc")
logger.debug(
"Splitting document %s with %d lines" % (self.sourceItem,nLines)
)
finalOrder = []
for i in range(self.listBox.count()):
listItem = self.listBox.item(i)
wTitle = listItem.text()
lineNo = listItem.data(Qt.UserRole)
finalOrder.append([wTitle, lineNo, nLines])
if i > 0:
finalOrder[i-1][2] = lineNo
if len(finalOrder) == 0:
self.theParent.makeAlert((
"No headers found. Nothing to do."
), nwAlert.ERROR)
return
fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemClass, srcItem.parHandle)
self.theParent.treeView.revealTreeItem(fHandle)
logger.verbose("Creating folder %s" % fHandle)
for wTitle, iStart, iEnd in finalOrder:
itemLayout = nwItemLayout.NOTE
if srcItem.itemClass == nwItemClass.NOVEL:
if wTitle.startswith("# "):
itemLayout = nwItemLayout.PARTITION
elif wTitle.startswith("## "):
itemLayout = nwItemLayout.CHAPTER
elif wTitle.startswith("### "):
itemLayout = nwItemLayout.SCENE
elif wTitle.startswith("#### "):
itemLayout = nwItemLayout.PAGE
wTitle = wTitle.lstrip("#")
wTitle = wTitle.strip()
nHandle = self.theProject.newFile(wTitle, srcItem.itemClass, fHandle)
newItem = self.theProject.getItem(nHandle)
newItem.setLayout(itemLayout)
logger.verbose(
"Creating new document %s with text from line %d to %d" % (nHandle, iStart, iEnd-1)
)
theText = "\n".join(theLines[iStart:iEnd])
theDoc.openDocument(nHandle, False)
theDoc.saveDocument(theText)
theDoc.clearDocument()
self.theParent.treeView.revealTreeItem(nHandle)
self.close()
return
def _doClose(self):
"""Close the dialog window without doing anything.
"""
logger.verbose("GuiDocSplit close button clicked")
self.optState.saveSettings()
self.close()
return
##
# Internal Functions
##
def _populateList(self):
"""Get the item selected in the tree, check that it is a folder,
and try to find all files associated with it. The valid files
are then added to the list view in order. The list itself can be
reordered by the user.
"""
if self.sourceItem is None:
self.sourceItem = self.theParent.treeView.getSelectedHandle()
if self.sourceItem is None:
return
nwItem = self.theProject.getItem(self.sourceItem)
if nwItem is None:
return
if nwItem.itemType is not nwItemType.FILE:
self.theParent.makeAlert((
"Element selected in the project tree must be a file."
), nwAlert.ERROR)
return
self.listBox.clear()
theDoc = NWDoc(self.theProject, self.theParent)
theText = theDoc.openDocument(self.sourceItem, False)
spLevel = self.splitLevel.currentData()
self.optState.setValue("GuiDocSplit", "spLevel", spLevel)
logger.debug("Scanning document %s for headings level <= %d" % (self.sourceItem, spLevel))
lineNo = 0
for aLine in theText.splitlines():
lineNo += 1
onLine = 0
if aLine.startswith("# ") and spLevel >= 1:
onLine = lineNo
elif aLine.startswith("## ") and spLevel >= 2:
onLine = lineNo
elif aLine.startswith("### ") and spLevel >= 3:
onLine = lineNo
elif aLine.startswith("#### ") and spLevel >= 4:
onLine = lineNo
if onLine > 0:
newItem = QListWidgetItem()
newItem.setText(aLine.strip())
newItem.setData(Qt.UserRole, onLine)
self.listBox.addItem(newItem)
return
# END Class GuiDocSplit
+63 -65
View File
@@ -24,7 +24,6 @@ from PyQt5.QtWidgets import (
) )
from nw.convert import TextFile, HtmlFile, MarkdownFile, LaTeXFile, ConcatFile from nw.convert import TextFile, HtmlFile, MarkdownFile, LaTeXFile, ConcatFile
from nw.tools import OptLastState
from nw.common import packageRefURL from nw.common import packageRefURL
from nw.constants import nwFiles, nwItemType, nwAlert from nw.constants import nwFiles, nwItemType, nwAlert
@@ -40,8 +39,7 @@ class GuiExport(QDialog):
self.mainConf = nw.CONFIG self.mainConf = nw.CONFIG
self.theParent = theParent self.theParent = theParent
self.theProject = theProject self.theProject = theProject
self.optState = ExportLastState(self.theProject,nwFiles.EXPORT_OPT) self.optState = self.theProject.optState
self.optState.loadSettings()
self.outerBox = QHBoxLayout() self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout() self.innerBox = QVBoxLayout()
@@ -50,8 +48,8 @@ class GuiExport(QDialog):
self.guiDeco = self.theParent.theTheme.loadDecoration("export",(64,64)) self.guiDeco = self.theParent.theTheme.loadDecoration("export",(64,64))
self.tabMain = GuiExportMain(self.theParent, self.theProject, self.optState) self.tabMain = GuiExportMain(self.theParent, self.theProject)
self.tabPandoc = GuiExportPandoc(self.theParent, self.theProject, self.optState) self.tabPandoc = GuiExportPandoc(self.theParent, self.theProject)
self.tabWidget = QTabWidget() self.tabWidget = QTabWidget()
self.tabWidget.addTab(self.tabMain, "Settings") self.tabWidget.addTab(self.tabMain, "Settings")
@@ -290,24 +288,24 @@ class GuiExport(QDialog):
if saveTo.startswith("~"): if saveTo.startswith("~"):
saveTo = path.expanduser(saveTo) saveTo = path.expanduser(saveTo)
self.optState.setSetting("wNovel", wNovel) self.optState.setValue("GuiExport", "wNovel", wNovel)
self.optState.setSetting("wNotes", wNotes) self.optState.setValue("GuiExport", "wNotes", wNotes)
self.optState.setSetting("eFormat", eFormat) self.optState.setValue("GuiExport", "eFormat", eFormat)
self.optState.setSetting("fixWidth", fixWidth) self.optState.setValue("GuiExport", "fixWidth", fixWidth)
self.optState.setSetting("wComments",wComments) self.optState.setValue("GuiExport", "wComments", wComments)
self.optState.setSetting("wKeywords",wKeywords) self.optState.setValue("GuiExport", "wKeywords", wKeywords)
self.optState.setSetting("chFormat", chFormat) self.optState.setValue("GuiExport", "chFormat", chFormat)
self.optState.setSetting("unFormat", unFormat) self.optState.setValue("GuiExport", "unFormat", unFormat)
self.optState.setSetting("scFormat", scFormat) self.optState.setValue("GuiExport", "scFormat", scFormat)
self.optState.setSetting("seFormat", seFormat) self.optState.setValue("GuiExport", "seFormat", seFormat)
self.optState.setSetting("saveTo", saveTo) self.optState.setValue("GuiExport", "saveTo", saveTo)
self.optState.setSetting("hScene", hScene) self.optState.setValue("GuiExport", "hScene", hScene)
self.optState.setSetting("hSection", hSection) self.optState.setValue("GuiExport", "hSection", hSection)
# Pandoc Settings # Pandoc Settings
pFormat = self.tabPandoc.outputFormat.currentData() pFormat = self.tabPandoc.outputFormat.currentData()
self.optState.setSetting("pFormat", pFormat) self.optState.setValue("GuiExport", "pFormat", pFormat)
self.optState.saveSettings() self.optState.saveSettings()
self.close() self.close()
@@ -362,14 +360,14 @@ class GuiExportMain(QWidget):
), ),
} }
def __init__(self, theParent, theProject, optState): def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent) QWidget.__init__(self, theParent)
self.theParent = theParent self.theParent = theParent
self.theProject = theProject self.theProject = theProject
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
self.outerBox = QGridLayout() self.outerBox = QGridLayout()
self.optState = optState self.optState = self.theProject.optState
self.currFormat = self.FMT_TXT self.currFormat = self.FMT_TXT
# Select Files # Select Files
@@ -378,19 +376,27 @@ class GuiExportMain(QWidget):
self.guiFiles.setLayout(self.guiFilesForm) self.guiFiles.setLayout(self.guiFilesForm)
self.expNovel = QCheckBox("Novel files",self) self.expNovel = QCheckBox("Novel files",self)
self.expNovel.setChecked(self.optState.getSetting("wNovel")) self.expNovel.setChecked(
self.optState.getBool("GuiExport", "wNovel", True)
)
self.expNovel.setToolTip("Include all novel files in the exported document") self.expNovel.setToolTip("Include all novel files in the exported document")
self.expNotes = QCheckBox("Note files",self) self.expNotes = QCheckBox("Note files",self)
self.expNotes.setChecked(self.optState.getSetting("wNotes")) self.expNotes.setChecked(
self.optState.getBool("GuiExport", "wNotes", False)
)
self.expNotes.setToolTip("Include all note files in the exported document") self.expNotes.setToolTip("Include all note files in the exported document")
self.expComments = QCheckBox("Comments",self) self.expComments = QCheckBox("Comments",self)
self.expComments.setChecked(self.optState.getSetting("wComments")) self.expComments.setChecked(
self.optState.getBool("GuiExport", "wComments", False)
)
self.expComments.setToolTip("Export comments from all files") self.expComments.setToolTip("Export comments from all files")
self.expKeywords = QCheckBox("Keywords",self) self.expKeywords = QCheckBox("Keywords",self)
self.expKeywords.setChecked(self.optState.getSetting("wKeywords")) self.expKeywords.setChecked(
self.optState.getBool("GuiExport", "wKeywords", False)
)
self.expKeywords.setToolTip("Export @keywords from all files") self.expKeywords.setToolTip("Export @keywords from all files")
self.guiFilesForm.addWidget(self.expNovel, 0, 1) self.guiFilesForm.addWidget(self.expNovel, 0, 1)
@@ -406,13 +412,17 @@ class GuiExportMain(QWidget):
self.chapterFormat = QLineEdit() self.chapterFormat = QLineEdit()
self.chapterFormat.setMaxLength(200) self.chapterFormat.setMaxLength(200)
self.chapterFormat.setText(self.optState.getSetting("chFormat")) self.chapterFormat.setText(
self.optState.getString("GuiExport", "chFormat", "Chapter %numword%")
)
self.chapterFormat.setToolTip("Available formats: %num%, %numword%, %title%") self.chapterFormat.setToolTip("Available formats: %num%, %numword%, %title%")
self.chapterFormat.setMinimumWidth(250) self.chapterFormat.setMinimumWidth(250)
self.unnumFormat = QLineEdit() self.unnumFormat = QLineEdit()
self.unnumFormat.setMaxLength(200) self.unnumFormat.setMaxLength(200)
self.unnumFormat.setText(self.optState.getSetting("unFormat")) self.unnumFormat.setText(
self.optState.getString("GuiExport", "unFormat", "%title%")
)
self.unnumFormat.setToolTip("Available formats: %title%") self.unnumFormat.setToolTip("Available formats: %title%")
self.unnumFormat.setMinimumWidth(250) self.unnumFormat.setMinimumWidth(250)
@@ -428,22 +438,30 @@ class GuiExportMain(QWidget):
self.sceneFormat = QLineEdit() self.sceneFormat = QLineEdit()
self.sceneFormat.setMaxLength(200) self.sceneFormat.setMaxLength(200)
self.sceneFormat.setText(self.optState.getSetting("scFormat")) self.sceneFormat.setText(
self.optState.getString("GuiExport", "scFormat", "* * *")
)
self.sceneFormat.setToolTip("Available formats: %title%") self.sceneFormat.setToolTip("Available formats: %title%")
self.sceneFormat.setMinimumWidth(100) self.sceneFormat.setMinimumWidth(100)
self.sectionFormat = QLineEdit() self.sectionFormat = QLineEdit()
self.sectionFormat.setMaxLength(200) self.sectionFormat.setMaxLength(200)
self.sectionFormat.setText(self.optState.getSetting("seFormat")) self.sectionFormat.setText(
self.optState.getString("GuiExport", "seFormat", "")
)
self.sectionFormat.setToolTip("Available formats: %title%") self.sectionFormat.setToolTip("Available formats: %title%")
self.sectionFormat.setMinimumWidth(100) self.sectionFormat.setMinimumWidth(100)
self.hideScene = QCheckBox("Skip",self) self.hideScene = QCheckBox("Skip",self)
self.hideScene.setChecked(self.optState.getSetting("hScene")) self.hideScene.setChecked(
self.optState.getBool("GuiExport", "hScene", False)
)
self.hideScene.setToolTip("Skip scene titles in export") self.hideScene.setToolTip("Skip scene titles in export")
self.hideSection = QCheckBox("Skip",self) self.hideSection = QCheckBox("Skip",self)
self.hideSection.setChecked(self.optState.getSetting("hSection")) self.hideSection.setChecked(
self.optState.getBool("GuiExport", "hSection", False)
)
self.hideSection.setToolTip("Skip section titles in export") self.hideSection.setToolTip("Skip section titles in export")
self.guiScenesForm.addWidget(QLabel("Scenes"), 0, 0) self.guiScenesForm.addWidget(QLabel("Scenes"), 0, 0)
@@ -458,8 +476,9 @@ class GuiExportMain(QWidget):
self.exportToForm = QGridLayout(self) self.exportToForm = QGridLayout(self)
self.exportTo.setLayout(self.exportToForm) self.exportTo.setLayout(self.exportToForm)
self.exportPath = QLineEdit(self.optState.getSetting("saveTo")) self.exportPath = QLineEdit(
self.optState.getString("GuiExport", "saveTo", "")
)
self.exportGetPath = QPushButton(self.theTheme.getIcon("folder"),"") self.exportGetPath = QPushButton(self.theTheme.getIcon("folder"),"")
self.exportGetPath.clicked.connect(self._exportFolder) self.exportGetPath.clicked.connect(self._exportFolder)
@@ -486,7 +505,9 @@ class GuiExportMain(QWidget):
self.outputFormat.addItem("Pandoc via Markdown or HTML", self.FMT_PDOC) self.outputFormat.addItem("Pandoc via Markdown or HTML", self.FMT_PDOC)
self.outputFormat.currentIndexChanged.connect(self._updateFormat) self.outputFormat.currentIndexChanged.connect(self._updateFormat)
optIdx = self.outputFormat.findData(self.optState.getSetting("eFormat")) optIdx = self.outputFormat.findData(
self.optState.getInt("GuiExport", "eFormat", 1)
)
if optIdx == -1: if optIdx == -1:
self.outputFormat.setCurrentIndex(1) self.outputFormat.setCurrentIndex(1)
self._updateFormat(1) self._updateFormat(1)
@@ -508,7 +529,9 @@ class GuiExportMain(QWidget):
self.fixedWidth.setMinimum(0) self.fixedWidth.setMinimum(0)
self.fixedWidth.setMaximum(999) self.fixedWidth.setMaximum(999)
self.fixedWidth.setSingleStep(1) self.fixedWidth.setSingleStep(1)
self.fixedWidth.setValue(self.optState.getSetting("fixWidth")) self.fixedWidth.setValue(
self.optState.getInt("GuiExport", "fixWidth", 80)
)
self.fixedWidth.setToolTip( self.fixedWidth.setToolTip(
"Applies to .txt and .md files. A value of '0' disables the feature." "Applies to .txt and .md files. A value of '0' disables the feature."
) )
@@ -609,13 +632,13 @@ class GuiExportPandoc(QWidget):
FMT_ZIM : "markdown", FMT_ZIM : "markdown",
} }
def __init__(self, theParent, theProject, optState): def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent) QWidget.__init__(self, theParent)
self.theParent = theParent self.theParent = theParent
self.theProject = theProject self.theProject = theProject
self.outerBox = QGridLayout() self.outerBox = QGridLayout()
self.optState = optState self.optState = self.theProject.optState
try: try:
import pypandoc import pypandoc
@@ -659,7 +682,9 @@ class GuiExportPandoc(QWidget):
self.outputFormat.addItem("ePUB eBook v3 (.epub3)", self.FMT_EPUB3) self.outputFormat.addItem("ePUB eBook v3 (.epub3)", self.FMT_EPUB3)
self.outputFormat.addItem("Zim Wiki (.txt)", self.FMT_ZIM) self.outputFormat.addItem("Zim Wiki (.txt)", self.FMT_ZIM)
optIdx = self.outputFormat.findData(self.optState.getSetting("pFormat")) optIdx = self.outputFormat.findData(
self.optState.getInt("GuiExport", "pFormat", 1)
)
if optIdx == -1: if optIdx == -1:
self.outputFormat.setCurrentIndex(1) self.outputFormat.setCurrentIndex(1)
else: else:
@@ -678,30 +703,3 @@ class GuiExportPandoc(QWidget):
return return
# END Class GuiExportPandoc # END Class GuiExportPandoc
class ExportLastState(OptLastState):
def __init__(self, theProject, theFile):
OptLastState.__init__(self, theProject, theFile)
self.theState = {
"wNovel" : True,
"wNotes" : False,
"eFormat" : 1,
"pFormat" : 1,
"fixWidth" : 80,
"wComments" : False,
"wKeywords" : False,
"chFormat" : "Chapter %numword%",
"unFormat" : "%title%",
"scFormat" : "* * *",
"seFormat" : "",
"saveTo" : "",
"hScene" : False,
"hSection" : False,
}
self.stringOpt = ("chFormat","unFormat","scFormat","seFormat","saveTo")
self.boolOpt = ("wNovel","wNotes","wComments","wKeywords","hScene","hSection")
self.intOpt = ("eFormat","pFormat","fixWidth")
return
# END Class ExportLastState
-2
View File
@@ -13,8 +13,6 @@
import logging import logging
import nw import nw
from os import path
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGroupBox, QFormLayout, QLineEdit, QDialog, QHBoxLayout, QVBoxLayout, QGroupBox, QFormLayout, QLineEdit,
-2
View File
@@ -13,8 +13,6 @@
import logging import logging
import nw import nw
from os import path
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 (
+169
View File
@@ -0,0 +1,169 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Open Project
novelWriter GUI Open Project
================================
New and open project dialog
File History:
Created: 2020-02-26 [0.4.5]
"""
import logging
import nw
from datetime import datetime
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QTreeWidget,
QAbstractItemView, QTreeWidgetItem
)
from nw.common import formatInt
logger = logging.getLogger(__name__)
class GuiProjectLoad(QDialog):
def __init__(self, theParent):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiProjectLoad ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.sourceItem = None
self.openPath = None
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Open Project")
self.setLayout(self.outerBox)
self.guiDeco = self.theParent.theTheme.loadDecoration("nwicon", (128, 128))
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.projectForm = QGridLayout()
self.projectForm.setContentsMargins(0, 0, 0, 0)
self.listBox = QTreeWidget()
self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
self.listBox.setColumnCount(4)
self.listBox.setHeaderLabels(["Working Title","Words","Accessed","Path"])
self.listBox.setRootIsDecorated(False)
treeHead = self.listBox.headerItem()
treeHead.setTextAlignment(1, Qt.AlignRight)
self.recentButton = QPushButton("Open")
self.recentButton.clicked.connect(self._doOpenRecent)
self.browseButton = QPushButton("Browse")
self.browseButton.clicked.connect(self._doBrowse)
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
self.projectForm.addWidget(self.listBox, 0, 0, 1, 4)
self.projectForm.addWidget(self.recentButton, 1, 1)
self.projectForm.addWidget(self.browseButton, 1, 2)
self.projectForm.addWidget(self.closeButton, 1, 3)
self.projectForm.setColumnStretch(0, 1)
self.innerBox.addLayout(self.projectForm)
self.rejected.connect(self._doClose)
self.setModal(True)
self.setMinimumWidth(750)
self.setMinimumHeight(450)
self.show()
self._populateList()
logger.debug("GuiProjectLoad initialisation complete")
return
##
# Buttons
##
def _doOpenRecent(self):
"""Close the dialog window with a recent project selected.
"""
logger.verbose("GuiProjectLoad open button clicked")
selItems = self.listBox.selectedItems()
if selItems:
self.openPath = selItems[0].text(3)
self.accept()
else:
self.openPath = None
return
def _doBrowse(self):
"""Close the dialog window with no selected path, triggering the
project browser dialog.
"""
logger.verbose("GuiProjectLoad browse button clicked")
self.openPath = None
self.accept()
return
def _doClose(self):
"""Close the dialog window without doing anything.
"""
logger.verbose("GuiProjectLoad close button clicked")
self.close()
return
##
# Internal Functions
##
def _populateList(self):
"""Populate the list box with recent project data.
"""
listOrder = []
listData = {}
for projPath in self.mainConf.recentProj.keys():
theEntry = self.mainConf.recentProj[projPath]
theTitle = ""
theTime = 0
theWords = 0
if "title" in theEntry.keys():
theTitle = theEntry["title"]
if "time" in theEntry.keys():
theTime = theEntry["time"]
if "words" in theEntry.keys():
theWords = theEntry["words"]
if theTime > 0:
listOrder.append(theTime)
listData[theTime] = [theTitle, theWords, projPath]
self.listBox.clear()
hasSelection = False
for timeStamp in sorted(listOrder, reverse=True):
newItem = QTreeWidgetItem([""]*4)
newItem.setText(0, listData[timeStamp][0])
newItem.setText(1, formatInt(listData[timeStamp][1]))
newItem.setText(2, datetime.fromtimestamp(timeStamp).strftime("%x %X"))
newItem.setText(3, listData[timeStamp][2])
newItem.setTextAlignment(1, Qt.AlignRight)
self.listBox.addTopLevelItem(newItem)
if not hasSelection:
newItem.setSelected(True)
hasSelection = True
self.listBox.resizeColumnToContents(0)
self.listBox.resizeColumnToContents(1)
self.listBox.resizeColumnToContents(2)
return
# END Class GuiProjectLoad
+20 -37
View File
@@ -24,7 +24,6 @@ from PyQt5.QtWidgets import (
) )
from nw.constants import nwConst, nwFiles, nwAlert from nw.constants import nwConst, nwFiles, nwAlert
from nw.tools import OptLastState
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -38,8 +37,7 @@ class GuiSessionLogView(QDialog):
self.mainConf = nw.CONFIG self.mainConf = nw.CONFIG
self.theProject = theProject self.theProject = theProject
self.theParent = theParent self.theParent = theParent
self.optState = SessionLogLastState(self.theProject,nwFiles.SLOG_OPT) self.optState = self.theProject.optState
self.optState.loadSettings()
self.timeFilter = 0.0 self.timeFilter = 0.0
self.timeTotal = 0.0 self.timeTotal = 0.0
@@ -52,13 +50,13 @@ class GuiSessionLogView(QDialog):
self.setMinimumHeight(400) self.setMinimumHeight(400)
widthCol0 = self.optState.validIntRange( widthCol0 = self.optState.validIntRange(
self.optState.getSetting("widthCol0"), 30, 999, 180 self.optState.getInt("GuiSession", "widthCol0", 180), 30, 999, 180
) )
widthCol1 = self.optState.validIntRange( widthCol1 = self.optState.validIntRange(
self.optState.getSetting("widthCol1"), 30, 999, 80 self.optState.getInt("GuiSession", "widthCol1", 80), 30, 999, 80
) )
widthCol2 = self.optState.validIntRange( widthCol2 = self.optState.validIntRange(
self.optState.getSetting("widthCol2"), 30, 999, 80 self.optState.getInt("GuiSession", "widthCol2", 80), 30, 999, 80
) )
self.listBox = QTreeWidget() self.listBox = QTreeWidget()
@@ -77,10 +75,11 @@ class GuiSessionLogView(QDialog):
sortValid = (Qt.AscendingOrder, Qt.DescendingOrder) sortValid = (Qt.AscendingOrder, Qt.DescendingOrder)
sortCol = self.optState.validIntRange( sortCol = self.optState.validIntRange(
self.optState.getSetting("sortCol"), 0, 2, 0 self.optState.getInt("GuiSession", "sortCol", 0), 0, 2, 0
) )
sortOrder = self.optState.validIntTuple( sortOrder = self.optState.validIntTuple(
self.optState.getSetting("sortOrder"), sortValid, Qt.DescendingOrder self.optState.getInt("GuiSession", "sortOrder", Qt.DescendingOrder),
sortValid, Qt.DescendingOrder
) )
self.listBox.sortByColumn(sortCol, sortOrder) self.listBox.sortByColumn(sortCol, sortOrder)
@@ -110,11 +109,15 @@ class GuiSessionLogView(QDialog):
self.filterBox.setLayout(self.filterBoxForm) self.filterBox.setLayout(self.filterBoxForm)
self.hideZeros = QCheckBox("Hide zero word count", self) self.hideZeros = QCheckBox("Hide zero word count", self)
self.hideZeros.setChecked(self.optState.getSetting("hideZeros")) self.hideZeros.setChecked(
self.optState.getBool("GuiSession", "hideZeros", True)
)
self.hideZeros.stateChanged.connect(self._doHideZeros) self.hideZeros.stateChanged.connect(self._doHideZeros)
self.hideNegative = QCheckBox("Hide negative word count", self) self.hideNegative = QCheckBox("Hide negative word count", self)
self.hideNegative.setChecked(self.optState.getSetting("hideNegative")) self.hideNegative.setChecked(
self.optState.getBool("GuiSession", "hideNegative", False)
)
self.hideNegative.stateChanged.connect(self._doHideNegative) self.hideNegative.stateChanged.connect(self._doHideNegative)
self.filterBoxForm.addWidget(self.hideZeros, 0, 0) self.filterBoxForm.addWidget(self.hideZeros, 0, 0)
@@ -208,13 +211,13 @@ class GuiSessionLogView(QDialog):
hideZeros = self.hideZeros.isChecked() hideZeros = self.hideZeros.isChecked()
hideNegative = self.hideNegative.isChecked() hideNegative = self.hideNegative.isChecked()
self.optState.setSetting("widthCol0", widthCol0) self.optState.setValue("GuiSession", "widthCol0", widthCol0)
self.optState.setSetting("widthCol1", widthCol1) self.optState.setValue("GuiSession", "widthCol1", widthCol1)
self.optState.setSetting("widthCol2", widthCol2) self.optState.setValue("GuiSession", "widthCol2", widthCol2)
self.optState.setSetting("sortCol", sortCol) self.optState.setValue("GuiSession", "sortCol", sortCol)
self.optState.setSetting("sortOrder", sortOrder) self.optState.setValue("GuiSession", "sortOrder", sortOrder)
self.optState.setSetting("hideZeros", hideZeros) self.optState.setValue("GuiSession", "hideZeros", hideZeros)
self.optState.setSetting("hideNegative",hideNegative) self.optState.setValue("GuiSession", "hideNegative", hideNegative)
self.optState.saveSettings() self.optState.saveSettings()
self.close() self.close()
@@ -237,23 +240,3 @@ class GuiSessionLogView(QDialog):
return "%02d:%02d:%02d" % (tH,tM,tS) return "%02d:%02d:%02d" % (tH,tM,tS)
# END Class GuiSessionLogView # END Class GuiSessionLogView
class SessionLogLastState(OptLastState):
def __init__(self, theProject, theFile):
OptLastState.__init__(self, theProject, theFile)
self.theState = {
"widthCol0" : 180,
"widthCol1" : 80,
"widthCol2" : 80,
"sortCol" : 0,
"sortOrder" : Qt.DescendingOrder,
"hideZeros" : True,
"hideNegative" : False,
}
self.stringOpt = ()
self.boolOpt = ("hideZeros","hideNegative")
self.intOpt = ("widthCol0","widthCol1","widthCol2","sortCol","sortOrder")
return
# END Class SessionLogLastState
-5
View File
@@ -23,11 +23,6 @@ logger = logging.getLogger(__name__)
class GuiDocDetails(QFrame): class GuiDocDetails(QFrame):
C_NAME = 0
C_COUNT = 1
C_FLAGS = 2
C_HANDLE = 3
def __init__(self, theParent, theProject): def __init__(self, theParent, theProject):
QFrame.__init__(self, theParent) QFrame.__init__(self, theParent)
+23 -14
View File
@@ -21,13 +21,13 @@ from PyQt5.QtWidgets import (
) )
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette, QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette,
QTextDocument QTextDocument, QCursor
) )
from nw.project import NWDoc from nw.project import NWDoc
from nw.gui.tools import GuiDocHighlighter, WordCounter from nw.gui.tools import GuiDocHighlighter, WordCounter
from nw.tools import NWSpellCheck, NWSpellSimple from nw.tools import NWSpellSimple
from nw.constants import nwFiles, nwUnicode, nwDocAction, nwAlert from nw.constants import nwUnicode, nwDocAction
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -209,7 +209,7 @@ class GuiDocEditor(QTextEdit):
risk overwriting the file if it exists. This can for instance risk overwriting the file if it exists. This can for instance
happen of the file contains binary elements or an encoding that happen of the file contains binary elements or an encoding that
novelWriter does not support. If load is successful, or the novelWriter does not support. If load is successful, or the
document is new (empty string) we set up the editor for editing document is new (empty string), we set up the editor for editing
the file. the file.
""" """
@@ -219,6 +219,7 @@ class GuiDocEditor(QTextEdit):
self.clearEditor() self.clearEditor()
return False return False
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self.hLight.setHandle(tHandle) self.hLight.setHandle(tHandle)
# Check that the document is not too big for full, initial spell # Check that the document is not too big for full, initial spell
@@ -246,6 +247,7 @@ class GuiDocEditor(QTextEdit):
self.theParent.noticeBar.showNote("This document is read only.") self.theParent.noticeBar.showNote("This document is read only.")
self.hLight.spellCheck = spTemp self.hLight.spellCheck = spTemp
qApp.restoreOverrideCursor()
return True return True
@@ -277,8 +279,9 @@ class GuiDocEditor(QTextEdit):
return True return True
def updateDocMargins(self): def updateDocMargins(self):
"""Automatically adjust the margins so the text is centred, but """Automatically adjust the margins so the text is centred if
only if Config.textFixedW is enabled or we're in Zen mode. Config.textFixedW is enabled or we're in Zen mode. Otherwise,
just ensure the margins are set correctly.
""" """
if self.mainConf.textFixedW or self.theParent.isZenMode: if self.mainConf.textFixedW or self.theParent.isZenMode:
@@ -335,7 +338,7 @@ class GuiDocEditor(QTextEdit):
return theText return theText
def setCursorPosition(self, thePosition): def setCursorPosition(self, thePosition):
if thePosition > 0: if thePosition >= 0:
theCursor = self.textCursor() theCursor = self.textCursor()
theCursor.setPosition(thePosition) theCursor.setPosition(thePosition)
self.setTextCursor(theCursor) self.setTextCursor(theCursor)
@@ -361,8 +364,8 @@ class GuiDocEditor(QTextEdit):
def setSpellCheck(self, theMode): def setSpellCheck(self, theMode):
"""This is the master spell check setting function, and this one """This is the master spell check setting function, and this one
should call all other setSpellCheck functions in other classes. should call all other setSpellCheck functions in other classes.
If the spell check mode is not defined, then toggle the current If the spell check mode (theMode) is not defined (None), then
status saved in the class. toggle the current status saved in this class.
""" """
if theMode is None: if theMode is None:
@@ -375,23 +378,29 @@ class GuiDocEditor(QTextEdit):
self.theParent.mainMenu.setSpellCheck(theMode) self.theParent.mainMenu.setSpellCheck(theMode)
self.theProject.setSpellCheck(theMode) self.theProject.setSpellCheck(theMode)
self.hLight.setSpellCheck(theMode) self.hLight.setSpellCheck(theMode)
self.reHighlightDocument() if not self.bigDoc:
self.spellCheckDocument()
logger.verbose("Spell check is set to %s" % str(theMode)) logger.verbose("Spell check is set to %s" % str(theMode))
return True return True
def reHighlightDocument(self): def spellCheckDocument(self):
"""Rerun the highlighter to update spell checking status of the """Rerun the highlighter to update spell checking status of the
currently loaded text. The fastest way to do this, at least as currently loaded text. The fastest way to do this, at least as
of Qt 5.13, is to clear the text and put it back. of Qt 5.13, is to clear the text and put it back.
""" """
logger.verbose("Running spell checker")
if self.spellCheck: if self.spellCheck:
theText = self.getText()
self.clear()
bfTime = time() bfTime = time()
self.setPlainText(theText) qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
if self.bigDoc:
theText = self.getText()
self.setPlainText(theText)
else:
self.hLight.rehighlight()
qApp.restoreOverrideCursor()
afTime = time() afTime = time()
logger.debug("Document re-highlighted in %.3f milliseconds" % (1000*(afTime-bfTime))) logger.debug("Document re-highlighted in %.3f milliseconds" % (1000*(afTime-bfTime)))
+120 -19
View File
@@ -16,10 +16,10 @@ import nw
from PyQt5.QtCore import Qt, QSize from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QFont, QColor from PyQt5.QtGui import QFont, QColor
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QApplication QTreeWidget, QTreeWidgetItem, QAbstractItemView, QApplication, QMessageBox
) )
from nw.project import NWItem from nw.project import NWItem, NWDoc
from nw.constants import ( from nw.constants import (
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert
) )
@@ -171,14 +171,21 @@ class GuiDocTree(QTreeWidget):
return False return False
# Add the new item to the tree # Add the new item to the tree
nwItem = self.theProject.getItem(tHandle) self.revealTreeItem(tHandle)
trItem = self._addTreeItem(nwItem) self.theParent.editItem()
return True
def revealTreeItem(self, tHandle):
"""Reveal a newly added project item in the project tree.
"""
nwItem = self.theProject.getItem(tHandle)
trItem = self._addTreeItem(nwItem)
pHandle = nwItem.parHandle
if pHandle is not None and pHandle in self.theMap.keys(): if pHandle is not None and pHandle in self.theMap.keys():
self.theMap[pHandle].setExpanded(True) self.theMap[pHandle].setExpanded(True)
self.clearSelection() self.clearSelection()
trItem.setSelected(True) trItem.setSelected(True)
self.theParent.editItem()
return True return True
def moveTreeItem(self, nStep): def moveTreeItem(self, nStep):
@@ -221,6 +228,16 @@ class GuiDocTree(QTreeWidget):
self.theProject.setTreeOrder(theList) self.theProject.setTreeOrder(theList)
return True return True
def getTreeFromHandle(self, tHandle):
"""Recursively return all the children items starting from a
given item handle.
"""
theList = []
theItem = self._getTreeItem(tHandle)
if theItem is not None:
theList = self._scanChildren(theList, theItem, 0)
return theList
def getColumnSizes(self): def getColumnSizes(self):
retVals = [ retVals = [
self.columnWidth(0), self.columnWidth(0),
@@ -229,7 +246,44 @@ class GuiDocTree(QTreeWidget):
] ]
return retVals return retVals
def deleteItem(self, tHandle=None): def emptyTrash(self):
"""Permanently delete all documents in the Trash folder. This
function only asks for confirmation once, and calls the regular
deleteItem function for each document in the Trash folder.
"""
logger.debug("Emptying Trash folder")
if self.theProject.trashRoot is None:
self.makeAlert("There is no Trash folder.", nwAlert.INFO)
return False
theTrash = self.getTreeFromHandle(self.theProject.trashRoot)
if self.theProject.trashRoot in theTrash:
theTrash.remove(self.theProject.trashRoot)
nTrash = len(theTrash)
if nTrash == 0:
self.makeAlert("The Trash folder is empty.", nwAlert.INFO)
return False
msgBox = QMessageBox()
msgRes = msgBox.question(
self, "Empty Trash", "Permanently delete %d file%s from Trash?" % (
nTrash, "s"*int(nTrash > 1)
)
)
if msgRes != QMessageBox.Yes:
return False
logger.verbose("Deleting %d files from Trash" % nTrash)
for tHandle in self.getTreeFromHandle(self.theProject.trashRoot):
if tHandle == self.theProject.trashRoot:
continue
self.deleteItem(tHandle, True)
return True
def deleteItem(self, tHandle=None, alreadyAsked=False):
"""Delete items from the tree. Note that this does not delete """Delete items from the tree. Note that this does not delete
the item from the item tree in the project object. However, the item from the item tree in the project object. However,
since this is only meta data, there isn't really a need to do since this is only meta data, there isn't really a need to do
@@ -246,21 +300,61 @@ class GuiDocTree(QTreeWidget):
trItemS = self._getTreeItem(tHandle) trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.getItem(tHandle) nwItemS = self.theProject.getItem(tHandle)
if nwItemS is None:
return False
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()
trItemT = self._addTrashRoot() trItemT = self._addTrashRoot()
if trItemP is None or trItemT is None: if trItemP is None or trItemT is None:
logger.error("Could not move item to trash") logger.error("Could not delete item")
return False return False
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex) pHandle = nwItemS.parHandle
trItemT.addChild(trItemC) if pHandle is not None and pHandle == self.theProject.trashRoot:
nwItemS.setParent(self.theProject.trashRoot) # If the file is in the trash folder already, as the
self.clearSelection() # user if they want to permanently delete the file.
trItemP.setSelected(True)
self.theProject.setProjectChanged(True) doPermanent = False
self.theParent.theIndex.deleteHandle(tHandle) if self.mainConf.showGUI and not alreadyAsked:
msgBox = QMessageBox()
msgRes = msgBox.question(
self, "Delete File", "Permanently delete file '%s'?" % nwItemS.itemName
)
if msgRes == QMessageBox.Yes:
doPermanent = True
else:
doPermanent = True
if doPermanent:
logger.debug("Permanently deleting file with handle %s" % tHandle)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
if self.theParent.docEditor.theHandle == tHandle:
self.theParent.closeDocument()
theDoc = NWDoc(self.theProject, self.theParent)
theDoc.deleteDocument(tHandle)
self.theProject.deleteItem(tHandle)
self.theParent.theIndex.deleteHandle(tHandle)
else:
# The file is not already in the trash folder, so we
# move it there.
if pHandle is None:
logger.warning("File has no parent item")
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC)
nwItemS.setParent(self.theProject.trashRoot)
self.theProject.setProjectChanged(True)
self.theParent.theIndex.deleteHandle(tHandle)
elif nwItemS.itemType == nwItemType.FOLDER: elif nwItemS.itemType == nwItemType.FOLDER:
logger.debug("User requested folder %s deleted" % tHandle) logger.debug("User requested folder %s deleted" % tHandle)
@@ -271,8 +365,6 @@ class GuiDocTree(QTreeWidget):
tIndex = trItemP.indexOfChild(trItemS) tIndex = trItemP.indexOfChild(trItemS)
if trItemS.childCount() == 0: if trItemS.childCount() == 0:
trItemP.takeChild(tIndex) trItemP.takeChild(tIndex)
self.clearSelection()
trItemP.setSelected(True)
self.theProject.deleteItem(tHandle) self.theProject.deleteItem(tHandle)
else: else:
self.makeAlert(["Cannot delete folder.","It is not empty."], nwAlert.ERROR) self.makeAlert(["Cannot delete folder.","It is not empty."], nwAlert.ERROR)
@@ -428,6 +520,9 @@ class GuiDocTree(QTreeWidget):
return newItem return newItem
def _addTrashRoot(self): def _addTrashRoot(self):
"""Adds the trash root folder if it doesn't already exist in the
project tree.
"""
if self.theProject.trashRoot is None: if self.theProject.trashRoot is None:
self.theProject.addTrash() self.theProject.addTrash()
trItem = self._addTreeItem( trItem = self._addTreeItem(
@@ -518,16 +613,22 @@ class GuiDocTree(QTreeWidget):
""" """
sHandle = self.getSelectedHandle() sHandle = self.getSelectedHandle()
if sHandle is None: if sHandle is None:
logger.error("No handle selected")
return return
dIndex = self.indexAt(theEvent.pos()) dIndex = self.indexAt(theEvent.pos())
if not dIndex.isValid(): if not dIndex.isValid():
logger.error("Invalid drop index")
return return
dItem = self.itemFromIndex(dIndex) dItem = self.itemFromIndex(dIndex)
dHandle = dItem.text(self.C_HANDLE) dHandle = dItem.text(self.C_HANDLE)
snItem = self.theProject.getItem(sHandle) snItem = self.theProject.getItem(sHandle)
dnItem = self.theProject.getItem(dHandle) dnItem = self.theProject.getItem(dHandle)
if dnItem is None:
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
return
isSame = snItem.itemClass == dnItem.itemClass isSame = snItem.itemClass == dnItem.itemClass
isNone = snItem.itemClass == nwItemClass.NO_CLASS isNone = snItem.itemClass == nwItemClass.NO_CLASS
isNote = snItem.itemLayout == nwItemLayout.NOTE isNote = snItem.itemLayout == nwItemLayout.NOTE
+4 -17
View File
@@ -21,7 +21,6 @@ from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem
) )
from nw.tools import OptLastState
from nw.constants import nwItemLayout, nwKeyWords, nwLabels, nwFiles from nw.constants import nwItemLayout, nwKeyWords, nwLabels, nwFiles
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -75,7 +74,7 @@ class GuiProjectOutline(QWidget):
self.theParent = theParent self.theParent = theParent
self.theProject = theProject self.theProject = theProject
self.theIndex = self.theParent.theIndex self.theIndex = self.theParent.theIndex
self.optState = OutlineLastState(self.theProject,nwFiles.OUTLINE_OPT) self.optState = self.theProject.optState
self.showWords = True self.showWords = True
self.showSynopsis = True self.showSynopsis = True
@@ -111,13 +110,13 @@ class GuiProjectOutline(QWidget):
colW.append(self.mainTree.columnWidth(iCol)) colW.append(self.mainTree.columnWidth(iCol))
self.treeCols["width"] = colW self.treeCols["width"] = colW
self.optState.setSetting("headState", self.treeCols) self.optState.setValue("GuiProjectOutline", "headState", self.treeCols)
self.optState.saveSettings() self.optState.saveSettings()
return return
def loadHeaderState(self): def loadHeaderState(self):
self.optState.loadSettings()
treeCols = self.optState.getSetting("headState") treeCols = self.optState.getValue("GuiProjectOutline", "headState", {})
if "order" not in treeCols.keys(): return if "order" not in treeCols.keys(): return
if not isinstance(treeCols["order"], list): return if not isinstance(treeCols["order"], list): return
@@ -253,15 +252,3 @@ class GuiProjectOutline(QWidget):
return return
# END Class GuiProjectOutline # END Class GuiProjectOutline
class OutlineLastState(OptLastState):
def __init__(self, theProject, theFile):
OptLastState.__init__(self, theProject, theFile)
self.theState = {
"headState" : {},
}
self.dictOpt = ("headState")
return
# END Class OutlineLastState
+2 -2
View File
@@ -86,13 +86,13 @@ class GuiSearchBar(QFrame):
if not self.isVisible(): if not self.isVisible():
self.setVisible(True) self.setVisible(True)
self.searchBox.setText(theText) self.searchBox.setText(theText)
self.searchBox.setFocus(True) self.searchBox.setFocus()
logger.verbose("Setting search text to '%s'" % theText) logger.verbose("Setting search text to '%s'" % theText)
return True return True
def setReplaceText(self, theText): def setReplaceText(self, theText):
self._replaceVisible(True) self._replaceVisible(True)
self.replaceBox.setFocus(True) self.replaceBox.setFocus()
self.replaceBox.setText(theText) self.replaceBox.setText(theText)
return True return True
-2
View File
@@ -18,8 +18,6 @@ from PyQt5.QtWidgets import (
QWidget, QLabel, QScrollArea, QFrame, QToolButton, QCheckBox, QGridLayout QWidget, QLabel, QScrollArea, QFrame, QToolButton, QCheckBox, QGridLayout
) )
from nw.constants import nwLabels
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GuiDocViewDetails(QWidget): class GuiDocViewDetails(QWidget):
+27 -3
View File
@@ -45,8 +45,11 @@ class GuiIcons:
} }
DECO_MAP = { DECO_MAP = {
"export" : "export.svg", "nwicon" : ["icons", "novelWriter.svg"],
"settings" : "gear.svg", "export" : ["graphics", "export.svg"],
"merge" : ["graphics", "merge.svg"],
"settings" : ["graphics", "gear.svg"],
"split" : ["graphics", "split.svg"],
} }
def __init__(self, theParent): def __init__(self, theParent):
@@ -65,6 +68,9 @@ class GuiIcons:
return return
def initIcons(self, priPath): def initIcons(self, priPath):
"""Load all icons listed in the icon map. Can be overridden by
the selected theme.
"""
self.priPath = priPath self.priPath = priPath
self.secPath = self.mainConf.iconPath self.secPath = self.mainConf.iconPath
@@ -76,12 +82,19 @@ class GuiIcons:
return return
def loadDecoration(self, decoKey, decoSize=None): def loadDecoration(self, decoKey, decoSize=None):
"""Load graphical decoration element based on the decoration
map. This function always returns a QSwgWidget.
"""
if decoKey not in self.DECO_MAP: if decoKey not in self.DECO_MAP:
logger.error("Decoration with name '%s' does not exist" % decoKey) logger.error("Decoration with name '%s' does not exist" % decoKey)
return QSvgWidget() return QSvgWidget()
svgPath = path.join(self.mainConf.graphPath, self.DECO_MAP[decoKey]) svgPath = path.join(
self.mainConf.assetPath,
self.DECO_MAP[decoKey][0],
self.DECO_MAP[decoKey][1]
)
if not path.isfile(svgPath): if not path.isfile(svgPath):
logger.error("Decoration file '%s' not in assets folder" % self.DECO_MAP[decoKey]) logger.error("Decoration file '%s' not in assets folder" % self.DECO_MAP[decoKey])
return QSvgWidget() return QSvgWidget()
@@ -93,11 +106,17 @@ class GuiIcons:
return svgDeco return svgDeco
def getIcon(self, iconKey, iconSize=None): def getIcon(self, iconKey, iconSize=None):
"""Return an icon from the icon buffer. If it doesn't exist,
return an empty icon.
"""
if iconKey in self.qIcons: if iconKey in self.qIcons:
return self.qIcons[iconKey] return self.qIcons[iconKey]
return QIcon() return QIcon()
def getPixmap(self, iconKey, iconSize): def getPixmap(self, iconKey, iconSize):
"""Return an icon from the icon buffer as a QPixmap. If it
doesn't exist, return an empty QPixmap.
"""
if iconKey in self.qIcons: if iconKey in self.qIcons:
return self.qIcons[iconKey].pixmap(iconSize[0], iconSize[1], QIcon.Normal) return self.qIcons[iconKey].pixmap(iconSize[0], iconSize[1], QIcon.Normal)
return QPixmap() return QPixmap()
@@ -107,6 +126,11 @@ class GuiIcons:
## ##
def _loadIcon(self, iconKey): def _loadIcon(self, iconKey):
"""Load an icon from the assets or theme folder, with a
preference for dark/light icons depending on theme type, if such
an icon exists. Prefer svg files over png files. Always returns
a QIcon.
"""
if iconKey not in self.ICON_MAP: if iconKey not in self.ICON_MAP:
logger.error("Icon with name '%s' does not exist" % iconKey) logger.error("Icon with name '%s' does not exist" % iconKey)
+21 -41
View File
@@ -48,11 +48,6 @@ class GuiMainMenu(QMenuBar):
return return
def openRecentProject(self, menuItem, recentItem):
logger.verbose("User requested opening recent project #%d" % recentItem)
self.theParent.openProject(self.mainConf.recentList[recentItem])
return True
def setAvailableRoot(self): def setAvailableRoot(self):
for itemClass in nwItemClass: for itemClass in nwItemClass:
if itemClass == nwItemClass.NO_CLASS: continue if itemClass == nwItemClass.NO_CLASS: continue
@@ -66,30 +61,6 @@ class GuiMainMenu(QMenuBar):
# Update Menu on Settings Changed # Update Menu on Settings Changed
## ##
def updateMenu(self):
self.updateRecentProjects()
return
def updateRecentProjects(self):
self.recentMenu.clear()
for n in range(len(self.mainConf.recentList)):
recentProject = self.mainConf.recentList[n]
if recentProject == "": continue
menuItem = QAction("%s" % recentProject, self.projMenu)
menuItem.triggered.connect(
lambda menuItem, n=n : self.openRecentProject(menuItem, n)
)
self.recentMenu.addAction(menuItem)
self.recentMenu.addSeparator()
menuItem = QAction("Clear Recent Projects", self)
menuItem.setStatusTip("Clear the list of recent projects")
menuItem.triggered.connect(self._clearRecentProjects)
self.recentMenu.addAction(menuItem)
return
def setSpellCheck(self, theMode): def setSpellCheck(self, theMode):
"""Set the spell check check box to theMode. This is controlled """Set the spell check check box to theMode. This is controlled
by the document editor class, which holds the master spell check by the document editor class, which holds the master spell check
@@ -166,11 +137,6 @@ class GuiMainMenu(QMenuBar):
QDesktopServices.openUrl(QUrl(nw.__docurl__)) QDesktopServices.openUrl(QUrl(nw.__docurl__))
return True return True
def _clearRecentProjects(self):
self.mainConf.clearRecent()
self.updateRecentProjects()
return True
def _showDocumentLocation(self): def _showDocumentLocation(self):
self.theParent.docEditor.revealLocation() self.theParent.docEditor.revealLocation()
return True return True
@@ -194,7 +160,7 @@ class GuiMainMenu(QMenuBar):
self.aOpenProject = QAction("Open Project", self) self.aOpenProject = QAction("Open Project", self)
self.aOpenProject.setStatusTip("Open project") self.aOpenProject.setStatusTip("Open project")
self.aOpenProject.setShortcut("Ctrl+Shift+O") self.aOpenProject.setShortcut("Ctrl+Shift+O")
self.aOpenProject.triggered.connect(lambda : self.theParent.openProject(None)) self.aOpenProject.triggered.connect(self.theParent.manageProjects)
self.projMenu.addAction(self.aOpenProject) self.projMenu.addAction(self.aOpenProject)
# Project > Save Project # Project > Save Project
@@ -211,10 +177,6 @@ class GuiMainMenu(QMenuBar):
self.aCloseProject.triggered.connect(lambda : self.theParent.closeProject(False)) self.aCloseProject.triggered.connect(lambda : self.theParent.closeProject(False))
self.projMenu.addAction(self.aCloseProject) self.projMenu.addAction(self.aCloseProject)
# Project > Recent Projects
self.recentMenu = self.projMenu.addMenu("Recent Projects")
self.updateRecentProjects()
# Project > Project Settings # Project > Project Settings
self.aProjectSettings = QAction("Project Settings", self) self.aProjectSettings = QAction("Project Settings", self)
self.aProjectSettings.setStatusTip("Project settings") self.aProjectSettings.setStatusTip("Project settings")
@@ -255,7 +217,7 @@ class GuiMainMenu(QMenuBar):
self.rootItems[itemClass].triggered.connect( self.rootItems[itemClass].triggered.connect(
lambda nCount, itemClass=itemClass : self._newTreeItem(nwItemType.ROOT, itemClass) lambda nCount, itemClass=itemClass : self._newTreeItem(nwItemType.ROOT, itemClass)
) )
self.rootMenu.addActions(self.rootItems.values()) self.rootMenu.addAction(self.rootItems[itemClass])
# Project > New Folder # Project > New Folder
self.aCreateFolder = QAction("Create Folder", self) self.aCreateFolder = QAction("Create Folder", self)
@@ -281,6 +243,12 @@ class GuiMainMenu(QMenuBar):
self.aDeleteItem.triggered.connect(lambda : self.theParent.treeView.deleteItem(None)) self.aDeleteItem.triggered.connect(lambda : self.theParent.treeView.deleteItem(None))
self.projMenu.addAction(self.aDeleteItem) self.projMenu.addAction(self.aDeleteItem)
# Project > Empty Trash
self.aEmptyTrash = QAction("Empty Trash", self)
self.aEmptyTrash.setStatusTip("Permanently delete all files in the Trash folder")
self.aEmptyTrash.triggered.connect(self.theParent.treeView.emptyTrash)
self.projMenu.addAction(self.aEmptyTrash)
# Project > Separator # Project > Separator
self.projMenu.addSeparator() self.projMenu.addSeparator()
@@ -369,6 +337,18 @@ class GuiMainMenu(QMenuBar):
self.aImportFile.triggered.connect(self.theParent.importDocument) self.aImportFile.triggered.connect(self.theParent.importDocument)
self.docuMenu.addAction(self.aImportFile) self.docuMenu.addAction(self.aImportFile)
# Document > Merge Documents
self.aMergeDocs = QAction("Merge Folder to Document", self)
self.aMergeDocs.setStatusTip("Merge a folder of documents to a single document")
self.aMergeDocs.triggered.connect(self.theParent.mergeDocuments)
self.docuMenu.addAction(self.aMergeDocs)
# Document > Split Document
self.aSplitDoc = QAction("Split Document to Folder", self)
self.aSplitDoc.setStatusTip("Split a document into a folder of multiple documents")
self.aSplitDoc.triggered.connect(self.theParent.splitDocument)
self.docuMenu.addAction(self.aSplitDoc)
return return
def _buildViewMenu(self): def _buildViewMenu(self):
@@ -653,7 +633,7 @@ class GuiMainMenu(QMenuBar):
self.aReRunSpell = QAction("Re-Run Spell Check", self) self.aReRunSpell = QAction("Re-Run Spell Check", self)
self.aReRunSpell.setStatusTip("Run the spell checker on current document") self.aReRunSpell.setStatusTip("Run the spell checker on current document")
self.aReRunSpell.setShortcut("F7") self.aReRunSpell.setShortcut("F7")
self.aReRunSpell.triggered.connect(self.theParent.docEditor.reHighlightDocument) self.aReRunSpell.triggered.connect(self.theParent.docEditor.spellCheckDocument)
self.toolsMenu.addAction(self.aReRunSpell) self.toolsMenu.addAction(self.aReRunSpell)
# Tools > Separator # Tools > Separator
+123 -35
View File
@@ -15,19 +15,20 @@ import time
import nw import nw
from os import path from os import path
from datetime import datetime
from PyQt5.QtCore import Qt, QTimer from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtGui import QIcon, QPixmap, QColor from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
qApp, QMainWindow, QVBoxLayout, QFrame, QSplitter, QFileDialog, QShortcut, qApp, QMainWindow, QVBoxLayout, QFrame, QSplitter, QFileDialog, QShortcut,
QMessageBox, QProgressDialog, QDialog, QTabWidget QMessageBox, QProgressDialog, QDialog, QTabWidget
) )
from nw.gui import ( from nw.gui import (
GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiExport,
GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails,
GuiDocViewDetails, GuiConfigEditor, GuiProjectEditor, GuiExport, GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiProjectOutline,
GuiItemEditor, GuiSessionLogView, GuiProjectOutline GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad
) )
from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup
from nw.tools import countWords from nw.tools import countWords
@@ -49,9 +50,9 @@ class GuiMain(QMainWindow):
self.hasProject = False self.hasProject = False
self.isZenMode = False self.isZenMode = False
logger.info("OS: %s" % ( logger.info("OS: %s" % self.mainConf.osType)
self.mainConf.osType) logger.info("Kernel: %s" % self.mainConf.kernelVer)
) logger.info("Host: %s" % self.mainConf.hostName)
logger.info("Qt5 Version: %s (%d)" % ( logger.info("Qt5 Version: %s (%d)" % (
self.mainConf.verQtString, self.mainConf.verQtValue) self.mainConf.verQtString, self.mainConf.verQtValue)
) )
@@ -64,7 +65,7 @@ class GuiMain(QMainWindow):
self.resize(*self.mainConf.winGeometry) self.resize(*self.mainConf.winGeometry)
self._setWindowTitle() self._setWindowTitle()
self.setWindowIcon(QIcon(path.join(self.mainConf.appIcon))) self.setWindowIcon(QIcon(self.mainConf.appIcon))
# Main GUI Elements # Main GUI Elements
self.statusBar = GuiMainStatus(self) self.statusBar = GuiMainStatus(self)
@@ -84,7 +85,7 @@ class GuiMain(QMainWindow):
# Assemble Main Window # Assemble Main Window
self.treePane = QFrame() self.treePane = QFrame()
self.treeBox = QVBoxLayout() self.treeBox = QVBoxLayout()
self.treeBox.setContentsMargins(0,0,0,0) self.treeBox.setContentsMargins(0,0,0,0)
self.treeBox.addWidget(self.treeView) self.treeBox.addWidget(self.treeView)
self.treeBox.addWidget(self.treeMeta) self.treeBox.addWidget(self.treeMeta)
@@ -163,17 +164,12 @@ class GuiMain(QMainWindow):
# Shortcuts and Actions # Shortcuts and Actions
self._connectMenuActions() self._connectMenuActions()
QShortcut( keyReturn = QShortcut(self.treeView)
Qt.Key_Return, keyReturn.setKey(QKeySequence(Qt.Key_Return))
self.treeView, keyReturn.activated.connect(self._treeKeyPressReturn)
context=Qt.WidgetShortcut, keyEscape = QShortcut(self)
activated=self._treeKeyPressReturn keyEscape.setKey(QKeySequence(Qt.Key_Escape))
) keyEscape.activated.connect(self._keyPressEscape)
QShortcut(
Qt.Key_Escape,
self,
activated=self._keyPressEscape
)
# Forward Functions # Forward Functions
self.setStatus = self.statusBar.setStatus self.setStatus = self.statusBar.setStatus
@@ -182,6 +178,9 @@ class GuiMain(QMainWindow):
if self.mainConf.showGUI: if self.mainConf.showGUI:
self.show() self.show()
# Check that config loaded fine
self.reportConfErr()
self.initMain() self.initMain()
self.asProjTimer.start() self.asProjTimer.start()
self.asDocTimer.start() self.asDocTimer.start()
@@ -193,6 +192,12 @@ class GuiMain(QMainWindow):
logger.debug("GUI initialisation complete") logger.debug("GUI initialisation complete")
if self.mainConf.cmdOpen is not None:
logger.debug("Opening project from additional command line option")
self.openProject(self.mainConf.cmdOpen)
else:
self.manageProjects()
return return
def clearGUI(self): def clearGUI(self):
@@ -213,13 +218,30 @@ class GuiMain(QMainWindow):
# Project Actions # Project Actions
## ##
def manageProjects(self):
"""Opens the projects dialog for selecting either existing
projects from a cache of recently opened projects, or provide a
browse button for projects not yet cached.
"""
if not self.mainConf.showGUI:
return False
dlgProj = GuiProjectLoad(self)
dlgProj.exec_()
if dlgProj.result() == QDialog.Accepted:
self.openProject(dlgProj.openPath)
return True
def newProject(self, projPath=None, forceNew=False): def newProject(self, projPath=None, forceNew=False):
"""Create new project with a few default files and folders.
"""
if self.hasProject: if self.hasProject:
msgBox = QMessageBox() msgBox = QMessageBox()
msgRes = msgBox.warning( msgRes = msgBox.warning(
self, "New Project", self, "New Project",
"Please close the current project<br>before making a new one." "Please close the current project before making a new one."
) )
return False return False
@@ -232,7 +254,7 @@ class GuiMain(QMainWindow):
msgBox = QMessageBox() msgBox = QMessageBox()
msgRes = msgBox.critical( msgRes = msgBox.critical(
self, "New Project", self, "New Project",
"A project already exists in that location.<br>Please choose another folder." "A project already exists in that location. Please choose another folder."
) )
return False return False
@@ -247,9 +269,9 @@ class GuiMain(QMainWindow):
return True return True
def closeProject(self, isYes=False): def closeProject(self, isYes=False):
"""Closes the project if one is open. """Closes the project if one is open. isYes is passed on from
isYes is passed on from the close application event so the user the close application event so the user doesn't get prompted
doesn't get prompted twice. twice.
""" """
if not self.hasProject: if not self.hasProject:
# There is no project loaded, everything OK # There is no project loaded, everything OK
@@ -309,7 +331,42 @@ class GuiMain(QMainWindow):
# Try to open the project # Try to open the project
if not self.theProject.openProject(projFile): if not self.theProject.openProject(projFile):
return False if self.theProject.lockedBy is not None:
if self.mainConf.showGUI:
try:
lockDetails = (
"<br><br>The project was locked by the computer "
"'%s' (%s %s), last active on %s"
) % (
self.theProject.lockedBy[0],
self.theProject.lockedBy[1],
self.theProject.lockedBy[2],
datetime.fromtimestamp(
int(self.theProject.lockedBy[3])
).strftime("%x %X")
)
except:
lockDetails = ""
msgBox = QMessageBox()
msgRes = msgBox.warning(
self, "Project Locked", (
"The project is already open by another instance of %s, and is "
"therefore locked. Override lock and continue anyway?<br><br>"
"Note: If the program or the computer previously crashed, the lock "
"can safely be overridden. If, however, another instance of %s has "
"the project open, overriding the lock may corrupt the project, and "
"is not recommended.%s"
) % (nw.__package__, nw.__package__, lockDetails),
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if msgRes == QMessageBox.Yes:
if not self.theProject.openProject(projFile, overrideLock=True):
return False
else:
return False
else:
return False
# project is loaded # project is loaded
self.hasProject = True self.hasProject = True
@@ -323,7 +380,6 @@ class GuiMain(QMainWindow):
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.docEditor.setSpellCheck(self.theProject.spellCheck) self.docEditor.setSpellCheck(self.theProject.spellCheck)
self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setRefTime(self.theProject.projOpened)
self.mainMenu.updateMenu()
# 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:
@@ -337,7 +393,7 @@ class GuiMain(QMainWindow):
return True return True
def saveProject(self, isAuto=False): def saveProject(self):
"""Save the current project. """Save the current project.
""" """
if not self.hasProject: if not self.hasProject:
@@ -351,9 +407,8 @@ class GuiMain(QMainWindow):
return False return False
self.treeView.saveTreeOrder() self.treeView.saveTreeOrder()
self.theProject.saveProject(isAuto) self.theProject.saveProject()
self.theIndex.saveIndex() self.theIndex.saveIndex()
self.mainMenu.updateRecentProjects()
return True return True
@@ -469,6 +524,22 @@ class GuiMain(QMainWindow):
return True return True
def mergeDocuments(self):
"""Merge multiple documents to one single new document.
"""
if self.mainConf.showGUI:
dlgMerge = GuiDocMerge(self, self.theProject)
dlgMerge.exec_()
return True
def splitDocument(self):
"""Split a single document into multiple documents.
"""
if self.mainConf.showGUI:
dlgSplit = GuiDocSplit(self, self.theProject)
dlgSplit.exec_()
return True
def passDocumentAction(self, theAction): def passDocumentAction(self, theAction):
"""Pass on document action theAction to whatever document has """Pass on document action theAction to whatever document has
the focus. If no document has focus, the action is discarded. the focus. If no document has focus, the action is discarded.
@@ -676,6 +747,16 @@ class GuiMain(QMainWindow):
return return
def reportConfErr(self):
"""Checks if the Config module has any errors to report, and let
the user know if this is the case. The Config module caches
errors since it is initialised before the GUI itself.
"""
if self.mainConf.hasError:
self.makeAlert(self.mainConf.getErrData(), nwAlert.ERROR)
return True
return False
## ##
# Main Window Actions # Main Window Actions
## ##
@@ -691,9 +772,9 @@ class GuiMain(QMainWindow):
return False return False
logger.info("Exiting %s" % nw.__package__) logger.info("Exiting %s" % nw.__package__)
self.projView.saveHeaderState() if self.hasProject:
self.projView.saveHeaderState()
self.closeProject(True) self.closeProject(True)
self.mainConf.setTreeColWidths(self.treeView.getColumnSizes()) self.mainConf.setTreeColWidths(self.treeView.getColumnSizes())
if not self.mainConf.isFullScreen: if not self.mainConf.isFullScreen:
@@ -702,6 +783,7 @@ class GuiMain(QMainWindow):
self.mainConf.setMainPanePos(self.splitMain.sizes()) self.mainConf.setMainPanePos(self.splitMain.sizes())
self.mainConf.setDocPanePos(self.splitView.sizes()) self.mainConf.setDocPanePos(self.splitView.sizes())
self.mainConf.saveConfig() self.mainConf.saveConfig()
self.reportConfErr()
qApp.quit() qApp.quit()
@@ -803,6 +885,12 @@ class GuiMain(QMainWindow):
self.addAction(self.mainMenu.aFmtULine) self.addAction(self.mainMenu.aFmtULine)
self.addAction(self.mainMenu.aFmtDQuote) self.addAction(self.mainMenu.aFmtDQuote)
self.addAction(self.mainMenu.aFmtSQuote) self.addAction(self.mainMenu.aFmtSQuote)
self.addAction(self.mainMenu.aFmtHead1)
self.addAction(self.mainMenu.aFmtHead2)
self.addAction(self.mainMenu.aFmtHead3)
self.addAction(self.mainMenu.aFmtHead4)
self.addAction(self.mainMenu.aFmtComment)
self.addAction(self.mainMenu.aFmtNoFormat)
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)
@@ -820,7 +908,7 @@ class GuiMain(QMainWindow):
if (self.hasProject and self.theProject.projChanged and if (self.hasProject and self.theProject.projChanged and
self.theProject.projPath is not None): self.theProject.projPath is not None):
logger.debug("Autosaving project") logger.debug("Autosaving project")
self.saveProject(isAuto=True) self.saveProject()
return return
def _autoSaveDocument(self): def _autoSaveDocument(self):
+2
View File
@@ -57,7 +57,9 @@ class NWBackup():
baseName = path.join(self.mainConf.backupPath, archName) baseName = path.join(self.mainConf.backupPath, archName)
try: try:
self.theProject._clearLockFile()
make_archive(baseName, "zip", self.theProject.projPath, ".") make_archive(baseName, "zip", self.theProject.projPath, ".")
self.theProject._writeLockFile()
except Exception as e: except Exception as e:
self.theParent.makeAlert( self.theParent.makeAlert(
["Could not write backup archive.",str(e)], ["Could not write backup archive.",str(e)],
+37 -22
View File
@@ -59,7 +59,7 @@ class NWDoc():
if self.theItem.parHandle == self.theProject.trashRoot: if self.theItem.parHandle == self.theProject.trashRoot:
self.docEditable = False self.docEditable = False
docDir, docFile = self._assemblePath(self.FILE_MN) docDir, docFile = self.assemblePath(self.docHandle, self.FILE_MN)
self.fileLoc = path.join(docDir,docFile) self.fileLoc = path.join(docDir,docFile)
logger.debug("Opening document %s" % self.fileLoc) logger.debug("Opening document %s" % self.fileLoc)
dataDir = path.join(self.theProject.projPath, docDir) dataDir = path.join(self.theProject.projPath, docDir)
@@ -92,7 +92,7 @@ class NWDoc():
if self.docHandle is None or not self.docEditable: if self.docHandle is None or not self.docEditable:
return False return False
docDir, docFile = self._assemblePath(self.FILE_MN) docDir, docFile = self.assemblePath(self.docHandle, self.FILE_MN)
logger.debug("Saving document %s" % path.join(docDir,docFile)) logger.debug("Saving document %s" % path.join(docDir,docFile))
dataPath = path.join(self.theProject.projPath, docDir) dataPath = path.join(self.theProject.projPath, docDir)
docPath = path.join(dataPath, docFile) docPath = path.join(dataPath, docFile)
@@ -100,39 +100,54 @@ class NWDoc():
mkdir(dataPath) mkdir(dataPath)
logger.debug("Created folder %s" % dataPath) logger.debug("Created folder %s" % dataPath)
docTemp = path.join(dataPath,docFile[:-3]+"tmp") docTemp = path.join(dataPath, docFile+"~")
docBack = path.join(dataPath,docFile[:-3]+"bak") docBack = path.join(dataPath, docFile[:-3]+"bak")
if path.isfile(docTemp):
unlink(docTemp)
if path.isfile(docBack):
rename(docBack,docTemp)
if path.isfile(docPath):
rename(docPath,docBack)
try: try:
with open(docPath,mode="w",encoding="utf8") as outFile: with open(docTemp,mode="w",encoding="utf8") as outFile:
outFile.write(docText) outFile.write(docText)
except Exception as e: except Exception as e:
self.makeAlert(["Could not save document.",str(e)], nwAlert.ERROR) self.makeAlert(["Could not save document.",str(e)], nwAlert.ERROR)
return False return False
if path.isfile(docTemp): # If we're here, the file was successfully saved,
unlink(docTemp) # so let's sort out the temps and backups
if path.isfile(docBack):
unlink(docBack)
if path.isfile(docPath):
rename(docPath, docBack)
rename(docTemp, docPath)
self.theParent.statusBar.setStatus("Saved Document: %s" % self.theItem.itemName) self.theParent.statusBar.setStatus("Saved Document: %s" % self.theItem.itemName)
return True return True
## def deleteDocument(self, tHandle):
# Internal Functions """Permanently delete a document source file and its backups
## from the project data folder.
"""
docDir, docFile = self.assemblePath(tHandle, self.FILE_MN)
dataPath = path.join(self.theProject.projPath, docDir)
chkList = []
chkList.append(path.join(dataPath, docFile))
chkList.append(path.join(dataPath, docFile+"~"))
chkList.append(path.join(dataPath, docFile[:-3]+"bak"))
for chkFile in chkList:
if path.isfile(chkFile):
try:
unlink(chkFile)
logger.debug("Deleted: %s" % chkFile)
except Exception as e:
self.makeAlert(["Could not delete document file.",str(e)], nwAlert.ERROR)
return False
return True
def _assemblePath(self, docExt): @staticmethod
if self.docHandle is None: def assemblePath(tHandle, docExt):
return None if tHandle is None:
docDir = "data_"+self.docHandle[0] return None, None
docFile = self.docHandle[1:13]+"_"+docExt docDir = "data_"+tHandle[0]
docFile = tHandle[1:13]+"_"+docExt
return docDir, docFile return docDir, docFile
# END Class NWDoc # END Class NWDoc
-3
View File
@@ -14,7 +14,6 @@ import logging
import nw import nw
from lxml import etree from lxml import etree
from datetime import datetime
from nw.common import checkInt from nw.common import checkInt
from nw.constants import nwItemType, nwItemClass, nwItemLayout from nw.constants import nwItemType, nwItemClass, nwItemLayout
@@ -23,8 +22,6 @@ logger = logging.getLogger(__name__)
class NWItem(): class NWItem():
MAX_DEPTH = 8
def __init__(self, theProject): def __init__(self, theProject):
self.theProject = theProject self.theProject = theProject
+170 -72
View File
@@ -13,8 +13,7 @@
import logging import logging
import nw import nw
from os import path, mkdir, listdir from os import path, mkdir, listdir, unlink, rename
from shutil import copyfile
from lxml import etree from lxml import etree
from hashlib import sha256 from hashlib import sha256
from datetime import datetime from datetime import datetime
@@ -22,6 +21,7 @@ from time import time
from nw.project.status import NWStatus from nw.project.status import NWStatus
from nw.project.item import NWItem from nw.project.item import NWItem
from nw.tools import projectMaintenance, OptionState
from nw.common import checkString, checkBool, checkInt from nw.common import checkString, checkBool, checkInt
from nw.constants import ( from nw.constants import (
nwFiles, nwConst, nwItemType, nwItemClass, nwItemLayout, nwAlert nwFiles, nwConst, nwItemType, nwItemClass, nwItemLayout, nwAlert
@@ -36,9 +36,11 @@ class NWProject():
# Internal # Internal
self.theParent = theParent self.theParent = theParent
self.mainConf = self.theParent.mainConf self.mainConf = self.theParent.mainConf
self.optState = OptionState(self)
self.projOpened = None # The time stamp of when the project file was opened self.projOpened = None # The time stamp of when the project file was opened
self.projChanged = None # The project has unsaved changes self.projChanged = None # The project has unsaved changes
self.projAltered = None # The project has been altered this session self.projAltered = None # The project has been altered this session
self.lockedBy = None # Data on which computer has the project open
# Debug # Debug
self.handleSeed = None self.handleSeed = None
@@ -50,7 +52,6 @@ class NWProject():
self.trashRoot = None # The handle of the trash root folder self.trashRoot = None # The handle of the trash root folder
self.projPath = None # The full path to where the currently open project is saved self.projPath = None # The full path to where the currently open project is saved
self.projMeta = None # The full path to the project's meta data folder self.projMeta = None # The full path to the project's meta data folder
self.projCache = None # The full path to the project's cache folder
self.projDict = None # The spell check dictionary self.projDict = None # The spell check dictionary
self.projFile = None # The file name of the project main xml file self.projFile = None # The file name of the project main xml file
@@ -154,7 +155,6 @@ class NWProject():
self.trashRoot = None self.trashRoot = None
self.projPath = None self.projPath = None
self.projMeta = None self.projMeta = None
self.projCache = None
self.projDict = None self.projDict = None
self.projFile = nwFiles.PROJ_FILE self.projFile = nwFiles.PROJ_FILE
self.projName = "" self.projName = ""
@@ -179,7 +179,12 @@ class NWProject():
return return
def openProject(self, fileName): def openProject(self, fileName, overrideLock=False):
"""Open the project file provided, or if doesn't exist, assume
it is a folder, and look for the file within it. If successful,
parse the XML of the file and populate the project variables and
build the tree of project items.
"""
if not path.isfile(fileName): if not path.isfile(fileName):
fileName = path.join(fileName, nwFiles.PROJ_FILE) fileName = path.join(fileName, nwFiles.PROJ_FILE)
@@ -188,24 +193,53 @@ class NWProject():
return False return False
self.clearProject() self.clearProject()
self.projPath = path.dirname(fileName) self.projPath = path.abspath(path.dirname(fileName))
logger.debug("Opening project: %s" % self.projPath) logger.debug("Opening project: %s" % self.projPath)
self.projMeta = path.join(self.projPath,"meta") self.projMeta = path.join(self.projPath,"meta")
self.projCache = path.join(self.projPath,"cache") self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT)
self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT)
if not self._checkFolder(self.projMeta): if not self._checkFolder(self.projMeta):
return return
if not self._checkFolder(self.projCache):
return if overrideLock:
self._clearLockFile()
lockStatus = self._readLockFile()
if len(lockStatus) > 0:
if lockStatus[0] == "ERROR":
logger.warning("Failed to check lock file")
else:
logger.error("Project is locked, so not opening")
self.lockedBy = lockStatus
self.clearProject()
return False
else:
logger.verbose("Project is not locked")
try:
projectMaintenance(self)
except Exception as E:
logger.error(str(E))
try: try:
nwXML = etree.parse(fileName) nwXML = etree.parse(fileName)
except Exception as e: except Exception as e:
self.makeAlert(["Failed to parse project xml.",str(e)], nwAlert.ERROR) self.makeAlert(["Failed to parse project xml.",str(e)], nwAlert.ERROR)
self.clearProject()
return False # Trying to open backup file instead
backFile = fileName[:-3]+"bak"
if path.isfile(backFile):
self.makeAlert("Attempting to open backup project file instead.", nwAlert.INFO)
try:
nwXML = etree.parse(backFile)
except Exception as e:
self.makeAlert(["Failed to parse project xml.",str(e)], nwAlert.ERROR)
self.clearProject()
return False
else:
self.clearProject()
return False
xRoot = nwXML.getroot() xRoot = nwXML.getroot()
nwxRoot = xRoot.tag nwxRoot = xRoot.tag
@@ -278,41 +312,47 @@ class NWProject():
nwItem.setFromTag(xValue.tag,xValue.text) nwItem.setFromTag(xValue.tag,xValue.text)
self._appendItem(tHandle,pHandle,nwItem) self._appendItem(tHandle,pHandle,nwItem)
self.mainConf.setRecent(self.projPath) self.optState.loadSettings()
# Update recent projects
self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time())
self.mainConf.saveRecentCache()
self.theParent.setStatus("Opened Project: %s" % self.projName) self.theParent.setStatus("Opened Project: %s" % self.projName)
self._scanProjectFolder() self._scanProjectFolder()
self.setProjectChanged(False) self.setProjectChanged(False)
self.projOpened = time() self.projOpened = time()
self.projAltered = False self.projAltered = False
self._writeLockFile()
return True return True
def saveProject(self, isAuto=False): def saveProject(self):
"""Save the project main XML file. The saving command itself
uses a temporary filename, and the file is renamed afterwards to
make sure if the save fails, we're not left with a truncated
file.
"""
if self.projPath is None: if self.projPath is None:
self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR) self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR)
return False return False
self.projMeta = path.join(self.projPath,"meta") self.projMeta = path.join(self.projPath,"meta")
self.projCache = path.join(self.projPath,"cache") saveTime = time()
if not self._checkFolder(self.projPath): return if not self._checkFolder(self.projPath): return
if not self._checkFolder(self.projMeta): return if not self._checkFolder(self.projMeta): return
if not self._checkFolder(self.projCache): return
logger.debug("Saving project: %s" % self.projPath) logger.debug("Saving project: %s" % self.projPath)
# Save a copy of the current file, just in case
if not isAuto:
self._maintainPrevious()
# 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__),
"fileVersion" : "1.0", "fileVersion" : "1.0",
"timeStamp" : datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "timeStamp" : datetime.fromtimestamp(saveTime).strftime("%Y-%m-%d %H:%M:%S"),
}) })
# Save Project Meta # Save Project Meta
@@ -345,9 +385,11 @@ class NWProject():
self.projTree[tHandle].packXML(xContent) self.projTree[tHandle].packXML(xContent)
# Write the xml tree to file # Write the xml tree to file
saveFile = path.join(self.projPath,self.projFile) tempFile = path.join(self.projPath, self.projFile+"~")
saveFile = path.join(self.projPath, self.projFile)
backFile = path.join(self.projPath, self.projFile[:-3]+"bak")
try: try:
with open(saveFile,mode="wb") as outFile: with open(tempFile, mode="wb") as outFile:
outFile.write(etree.tostring( outFile.write(etree.tostring(
nwXML, nwXML,
pretty_print = True, pretty_print = True,
@@ -358,15 +400,34 @@ class NWProject():
self.makeAlert(["Failed to save project.",str(e)], nwAlert.ERROR) self.makeAlert(["Failed to save project.",str(e)], nwAlert.ERROR)
return False return False
self.mainConf.setRecent(self.projPath) # If we're here, the file was successfully saved,
# so let's sort out the temps and backups
if path.isfile(backFile):
unlink(backFile)
if path.isfile(saveFile):
rename(saveFile, backFile)
rename(tempFile, saveFile)
# Save project GUI options
self.optState.saveSettings()
# Update recent projects
self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime)
self.mainConf.saveRecentCache()
self._writeLockFile()
self.theParent.setStatus("Saved Project: %s" % self.projName) self.theParent.setStatus("Saved Project: %s" % self.projName)
self.setProjectChanged(False) self.setProjectChanged(False)
return True return True
def closeProject(self): def closeProject(self):
"""Close the current project and clear all meta data.
"""
self._appendSessionStats() self._appendSessionStats()
self._clearLockFile()
self.clearProject() self.clearProject()
self.lockedBy = None
return True return True
## ##
@@ -452,9 +513,6 @@ class NWProject():
self.setProjectChanged(True) self.setProjectChanged(True)
return True return True
def getSessionWordCount(self):
return self.currWCount - self.lastWCount
def setStatusColours(self, newCols): def setStatusColours(self, newCols):
replaceMap = self.statusItems.setNewEntries(newCols) replaceMap = self.statusItems.setNewEntries(newCols)
if self.projTree is not None: if self.projTree is not None:
@@ -497,6 +555,9 @@ class NWProject():
logger.error("No tree item with handle %s" % str(tHandle)) logger.error("No tree item with handle %s" % str(tHandle))
return None return None
def getSessionWordCount(self):
return self.currWCount - self.lastWCount
def getRootItem(self, tHandle): def getRootItem(self, tHandle):
"""Iterate upwards in the tree until we find the item with """Iterate upwards in the tree until we find the item with
parent None, the root item. We do this with a for loop with a parent None, the root item. We do this with a for loop with a
@@ -505,10 +566,13 @@ class NWProject():
tItem = self.getItem(tHandle) tItem = self.getItem(tHandle)
if tItem is not None: if tItem is not None:
for i in range(200): for i in range(200):
tHandle = tItem.parHandle if tItem.parHandle is None:
tItem = self.getItem(tHandle)
if tItem is None:
return tHandle return tHandle
else:
tHandle = tItem.parHandle
tItem = self.getItem(tHandle)
if tItem is None:
return tHandle
return None return None
def getProjectItems(self): def getProjectItems(self):
@@ -560,6 +624,11 @@ class NWProject():
"""This only removes the item from the order list, but not from """This only removes the item from the order list, but not from
the project tree. the project tree.
""" """
if tHandle not in self.treeOrder:
logger.warning(
"Could not remove item %s from treeOrder as it does not exist" % tHandle
)
return False
self.treeOrder.remove(tHandle) self.treeOrder.remove(tHandle)
self.setProjectChanged(True) self.setProjectChanged(True)
return True return True
@@ -598,6 +667,73 @@ class NWProject():
# Internal Functions # Internal Functions
## ##
def _readLockFile(self):
"""Reads the lock file in the project folder.
"""
if self.projPath is None:
return ["ERROR"]
lockFile = path.join(self.projPath, nwFiles.PROJ_LOCK)
if not path.isfile(lockFile):
return []
try:
with open(lockFile, mode="r", encoding="utf8") as inFile:
theData = inFile.read()
theLines = theData.splitlines()
if len(theLines) == 4:
return theLines
else:
return ["ERROR"]
except Exception as e:
logger.error("Failed to read project lockfile")
logger.error(str(e))
return ["ERROR"]
return ["ERROR"]
def _writeLockFile(self):
"""Writes a lock file to the project folder.
"""
if self.projPath is None:
return False
lockFile = path.join(self.projPath, nwFiles.PROJ_LOCK)
try:
with open(lockFile, mode="w+", encoding="utf8") as outFile:
outFile.write("%s\n" % self.mainConf.hostName)
outFile.write("%s\n" % self.mainConf.osType)
outFile.write("%s\n" % self.mainConf.kernelVer)
outFile.write("%d\n" % time())
except Exception as e:
logger.error("Failed to write project lockfile")
logger.error(str(e))
return False
return True
def _clearLockFile(self):
"""Remove the lock file, if it exists.
"""
if self.projPath is None:
return False
lockFile = path.join(self.projPath, nwFiles.PROJ_LOCK)
if path.isfile(lockFile):
try:
unlink(lockFile)
return True
except Exception as e:
logger.error("Failed to remove project lockfile")
logger.error(str(e))
return False
return None
def _checkFolder(self, thePath): def _checkFolder(self, thePath):
if not path.isdir(thePath): if not path.isdir(thePath):
try: try:
@@ -732,42 +868,4 @@ class NWProject():
itemHandle = self._makeHandle(addSeed+"!") itemHandle = self._makeHandle(addSeed+"!")
return itemHandle return itemHandle
def _maintainPrevious(self):
"""This function will take the current project file and copy it
into the project cache folder with an incremental file extension
added. These serve as a backup in case the xml file gets
corrupted.
"""
countFile = path.join(self.projCache, nwFiles.PROJ_COUNT)
projCount = 0
if path.isfile(countFile):
try:
with open(countFile, mode="r") as inFile:
projCount = int(inFile.read())+1
except:
projCount = 0
if projCount > 9:
projCount = 0
projBackup = "%s.%d" % (nwFiles.PROJ_FILE, projCount)
try:
copyfile(
path.join(self.projPath, self.projFile),
path.join(self.projCache, projBackup)
)
except:
logger.error("Failed to write to file %s" % projBackup)
try:
with open(countFile, mode="w") as outFile:
outFile.write(str(projCount))
except:
logger.error("Failed to write to file %s" % countFile)
return
# END Class NWProject # END Class NWProject
+4 -2
View File
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from nw.tools.analyse import TextAnalysis from nw.tools.analyse import TextAnalysis
from nw.tools.optlaststate import OptLastState from nw.tools.legacy import projectMaintenance
from nw.tools.optionstate import OptionState
from nw.tools.spellcheck import NWSpellCheck from nw.tools.spellcheck import NWSpellCheck
from nw.tools.spellenchant import NWSpellEnchant from nw.tools.spellenchant import NWSpellEnchant
from nw.tools.spellsimple import NWSpellSimple from nw.tools.spellsimple import NWSpellSimple
@@ -10,7 +11,8 @@ from nw.tools.wordcount import countWords
__all__ = [ __all__ = [
"TextAnalysis", "TextAnalysis",
"OptLastState", "projectMaintenance",
"OptionState",
"NWSpellCheck", "NWSpellCheck",
"NWSpellEnchant", "NWSpellEnchant",
"NWSpellSimple", "NWSpellSimple",
+63
View File
@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
"""novelWriter Legacy Tools
novelWriter Legacy Tools
============================
Various functions to handle old projects
File History:
Created: 2020-02-13 [0.4.3]
"""
import logging
import nw
from os import path, unlink, rmdir
logger = logging.getLogger(__name__)
def projectMaintenance(theProject):
"""Wrapper class for handling various tasks related to managing old
projects with content from older versions of novelWriter.
"""
# Remove no longer used project cache folder
if path.isdir(theProject.projPath):
cacheDir = path.join(theProject.projPath, "cache")
if path.isdir(cacheDir):
logger.info("Deprecated cache folder found")
rmList = []
for i in range(10):
rmList.append(path.join(cacheDir, "nwProject.nwx.%d" % i))
rmList.append(path.join(cacheDir, "projCount.txt"))
for rmFile in rmList:
if path.isfile(rmFile):
logger.info("Deleting: %s" % rmFile)
try:
unlink(rmFile)
except Exception as e:
logger.error(str(e))
logger.info("Deleting: %s" % cacheDir)
try:
rmdir(cacheDir)
except Exception as e:
logger.error(str(e))
# Remove no longer used meta files
rmList = []
rmList.append(path.join(theProject.projMeta, "mainOptions.json"))
rmList.append(path.join(theProject.projMeta, "exportOptions.json"))
rmList.append(path.join(theProject.projMeta, "outlineOptions.json"))
rmList.append(path.join(theProject.projMeta, "timelineOptions.json"))
rmList.append(path.join(theProject.projMeta, "docMergeOptions.json"))
rmList.append(path.join(theProject.projMeta, "sessionLogOptions.json"))
for rmFile in rmList:
if path.isfile(rmFile):
logger.info("Deleting: %s" % rmFile)
try:
unlink(rmFile)
except Exception as e:
logger.error(str(e))
return
+162
View File
@@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
"""novelWriter Options State
novelWriter Options State
=============================
Class holding the last state of GUI options
File History:
Created: 2019-10-21 [0.3.1] - Original version meant to be sub classed
Created: 2020-02-19 [0.4.5] - Rewritten from superclass to single file tool
"""
import logging
import json
import nw
from os import path
from nw.common import checkString, checkBool, checkInt
from nw.constants import nwFiles
logger = logging.getLogger(__name__)
class OptionState():
def __init__(self, theProject):
self.theProject = theProject
self.theState = {}
self.stringOpt = ()
self.boolOpt = ()
self.intOpt = ()
return
def loadSettings(self):
"""Load the options dictionary from the project settings file.
"""
stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
theState = {}
if path.isfile(stateFile):
logger.debug("Loading GUI options file")
try:
with open(stateFile,mode="r",encoding="utf8") as inFile:
theJson = inFile.read()
theState = json.loads(theJson)
except Exception as e:
logger.error("Failed to load GUI options file")
logger.error(str(e))
return False
for anOpt in theState:
self.theState[anOpt] = theState[anOpt]
return True
def saveSettings(self):
"""Save the options dictionary to the project settings file.
"""
stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
logger.debug("Saving GUI options file")
try:
with open(stateFile,mode="w+",encoding="utf8") as outFile:
outFile.write(json.dumps(self.theState, indent=2))
except Exception as e:
logger.error("Failed to save GUI options file")
logger.error(str(e))
return False
return True
def setValue(self, setGroup, setName, setValue):
"""Saves a value, with a given group and name.
"""
if not setGroup in self.theState:
self.theState[setGroup] = {}
self.theState[setGroup][setName] = setValue
return True
def getValue(self, getGroup, getName, defaultValue):
"""Return an arbitrary type value, if it exists. Otherwise,
return the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return self.theState[getGroup][getName]
except:
return defaultValue
return defaultValue
def getString(self, getGroup, getName, defaultValue):
"""Return the value as a string, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return str(self.theState[getGroup][getName])
except:
return defaultValue
return defaultValue
def getInt(self, getGroup, getName, defaultValue):
"""Return the value as an int, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return int(self.theState[getGroup][getName])
except:
return defaultValue
return defaultValue
def getFloat(self, getGroup, getName, defaultValue):
"""Return the value as a float, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return float(self.theState[getGroup][getName])
except:
return defaultValue
return defaultValue
def getBool(self, getGroup, getName, defaultValue):
"""Return the value as a bool, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return bool(self.theState[getGroup][getName])
except:
return defaultValue
return defaultValue
def validIntRange(self, theValue, intA, intB, intDefault):
"""Check that an int is in a given range. If it isn't, return
the default value.
"""
if isinstance(theValue, int):
if theValue >= intA and theValue <= intB:
return theValue
return intDefault
def validIntTuple(self, theValue, theTuple, intDefault):
"""Check that an int is an element of a tuple. If it isn't,
return the default value.
"""
if isinstance(theValue, int):
if theValue in theTuple:
return theValue
return intDefault
# END Class OptionState
-115
View File
@@ -1,115 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Options Last State
novelWriter Options Last State
==================================
Class holding the last state of GUI options
File History:
Created: 2019-10-21 [0.3.1]
"""
import logging
import json
import nw
from os import path
from nw.common import checkString, checkBool, checkInt
logger = logging.getLogger(__name__)
class OptLastState():
def __init__(self, theProject, theFile):
self.theProject = theProject
self.theFile = theFile
self.theState = {}
self.stringOpt = ()
self.boolOpt = ()
self.dictOpt = ()
self.intOpt = ()
return
def loadSettings(self):
if self.theProject.projMeta is None or self.theFile is None:
logger.error("Cannot load file '%s' to path '%s'" % (
str(self.theFile), str(self.theProject.projMeta)
))
return False
stateFile = path.join(self.theProject.projMeta,self.theFile)
theState = {}
if path.isfile(stateFile):
logger.debug("Loading options file")
try:
with open(stateFile,mode="r",encoding="utf8") as inFile:
theJson = inFile.read()
theState = json.loads(theJson)
except Exception as e:
logger.error("Failed to load options file")
logger.error(str(e))
return False
for anOpt in theState:
self.theState[anOpt] = theState[anOpt]
return True
def saveSettings(self):
if self.theProject.projMeta is None or self.theFile is None:
logger.error("Cannot save file '%s' to path '%s'" % (
str(self.theFile), str(self.theProject.projMeta)
))
return False
stateFile = path.join(self.theProject.projMeta,self.theFile)
logger.debug("Saving options file")
try:
with open(stateFile,mode="w+",encoding="utf8") as outFile:
outFile.write(json.dumps(self.theState, indent=2))
except Exception as e:
logger.error("Failed to save options file")
logger.error(str(e))
return False
return True
def setSetting(self, setName, setValue):
if setName in self.theState:
self.theState[setName] = setValue
else:
return False
return True
def getSetting(self, setName):
if setName in self.stringOpt:
return checkString(self.theState[setName],self.theState[setName],False)
elif setName in self.boolOpt:
return checkBool(self.theState[setName],self.theState[setName],False)
elif setName in self.dictOpt:
if isinstance(self.theState[setName], dict):
return self.theState[setName]
else:
return {}
elif setName in self.intOpt:
return checkInt(self.theState[setName],self.theState[setName],False)
return None
def validIntRange(self, theValue, intA, intB, intDefault):
if isinstance(theValue, int):
if theValue >= intA and theValue <= intB:
return theValue
return intDefault
def validIntTuple(self, theValue, theTuple, intDefault):
if isinstance(theValue, int):
if theValue in theTuple:
return theValue
return intDefault
# END Class OptLastState
+1
View File
@@ -4,3 +4,4 @@ markers =
core: Core functionality tests core: Core functionality tests
gui: Qt5 GUI tests gui: Qt5 GUI tests
serial serial
qt_api = pyqt5
-1
View File
@@ -1,5 +1,4 @@
pyqt5 pyqt5
appdirs
lxml lxml
pyenchant pyenchant
latexcodec latexcodec
+3 -3
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.4.1" fileVersion="1.0" timeStamp="2019-11-16 14:42:29"> <novelWriterXML appVersion="0.4.5" fileVersion="1.0" timeStamp="2020-02-27 00:15:30">
<project> <project>
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
@@ -33,7 +33,7 @@
<entry blue="175" green="0" red="117">Main</entry> <entry blue="175" green="0" red="117">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="21"> <content count="20">
<item handle="7031beac91f75" order="0" parent="None"> <item handle="7031beac91f75" order="0" parent="None">
<name>Novel</name> <name>Novel</name>
<type>ROOT</type> <type>ROOT</type>
@@ -82,7 +82,7 @@
<charCount>1199</charCount> <charCount>1199</charCount>
<wordCount>216</wordCount> <wordCount>216</wordCount>
<paraCount>7</paraCount> <paraCount>7</paraCount>
<cursorPos>949</cursorPos> <cursorPos>19</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>
+2 -3
View File
@@ -6,7 +6,7 @@ with open("README.md", "r") as inFile:
setuptools.setup( setuptools.setup(
name = "novelWriter", name = "novelWriter",
version = "0.4.2", version = "0.4.5",
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",
@@ -35,10 +35,9 @@ setuptools.setup(
"Natural Language :: English", "Natural Language :: English",
"Topic :: Text Editors", "Topic :: Text Editors",
], ],
python_requires = ">=3.5", python_requires = ">=3.6",
install_requires = [ install_requires = [
"pyqt5", "pyqt5",
"appdirs",
"lxml", "lxml",
"pyenchant", "pyenchant",
"latexcodec", "latexcodec",
+1 -11
View File
@@ -1,5 +1,5 @@
[Main] [Main]
timestamp = 2019-11-19 21:49:29 timestamp = 2020-02-26 18:10:35
theme = default theme = default
syntax = default_light syntax = default_light
guidark = False guidark = False
@@ -49,14 +49,4 @@ viewcomments = True
[Path] [Path]
lastpath = lastpath =
recent0 =
recent1 =
recent2 =
recent3 =
recent4 =
recent5 =
recent6 =
recent7 =
recent8 =
recent9 =
+5 -5
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.1.4" fileVersion="1.0" timeStamp="2019-06-08 20:53:03"> <novelWriterXML appVersion="0.4.5" fileVersion="1.0" timeStamp="2020-02-26 22:05:41">
<project> <project>
<name></name> <name></name>
<title></title> <title></title>
@@ -72,28 +72,28 @@
<paraCount>0</paraCount> <paraCount>0</paraCount>
<cursorPos>0</cursorPos> <cursorPos>0</cursorPos>
</item> </item>
<item handle="39fa9ec190eee" order="None" parent="None"> <item handle="8722616204217" order="None" parent="None">
<name>Timeline</name> <name>Timeline</name>
<type>ROOT</type> <type>ROOT</type>
<class>TIMELINE</class> <class>TIMELINE</class>
<status>New</status> <status>New</status>
<expanded>False</expanded> <expanded>False</expanded>
</item> </item>
<item handle="d029fa3a95e17" order="None" parent="None"> <item handle="96061e92f58e4" order="None" parent="None">
<name>Object</name> <name>Object</name>
<type>ROOT</type> <type>ROOT</type>
<class>OBJECT</class> <class>OBJECT</class>
<status>New</status> <status>New</status>
<expanded>False</expanded> <expanded>False</expanded>
</item> </item>
<item handle="81b8a03f97e87" order="None" parent="None"> <item handle="eb624dbe56eb6" order="None" parent="None">
<name>Custom1</name> <name>Custom1</name>
<type>ROOT</type> <type>ROOT</type>
<class>CUSTOM</class> <class>CUSTOM</class>
<status>New</status> <status>New</status>
<expanded>False</expanded> <expanded>False</expanded>
</item> </item>
<item handle="da4ea2a5506f2" order="None" parent="None"> <item handle="f369cb89fc627" order="None" parent="None">
<name>Custom2</name> <name>Custom2</name>
<type>ROOT</type> <type>ROOT</type>
<class>CUSTOM</class> <class>CUSTOM</class>
+38 -2
View File
@@ -13,7 +13,7 @@ theConf = Config()
def testConfigInit(nwTemp,nwRef): def testConfigInit(nwTemp,nwRef):
tmpConf = path.join(nwTemp,"novelwriter.conf") tmpConf = path.join(nwTemp,"novelwriter.conf")
refConf = path.join(nwRef, "novelwriter.conf") refConf = path.join(nwRef, "novelwriter.conf")
assert theConf.initConfig(nwTemp) assert theConf.initConfig(nwTemp, nwTemp)
assert theConf.setLastPath("") assert theConf.setLastPath("")
assert theConf.saveConfig() assert theConf.saveConfig()
assert cmpFiles(tmpConf, refConf, [2]) assert cmpFiles(tmpConf, refConf, [2])
@@ -37,6 +37,14 @@ def testConfigSetConfPath(nwTemp):
assert theConf.confFile == "novelwriter.conf" assert theConf.confFile == "novelwriter.conf"
assert not theConf.confChanged assert not theConf.confChanged
@pytest.mark.core
def testConfigSetDataPath(nwTemp):
assert theConf.setDataPath(None)
assert not theConf.setDataPath(path.join("somewhere","over","the","rainbow"))
assert theConf.setDataPath(nwTemp)
assert theConf.dataPath == nwTemp
assert not theConf.confChanged
@pytest.mark.core @pytest.mark.core
def testConfigLoad(): def testConfigLoad():
assert theConf.loadConfig() assert theConf.loadConfig()
@@ -67,7 +75,7 @@ def testConfigSetTreeColWidths(nwTemp,nwRef):
assert not theConf.confChanged assert not theConf.confChanged
@pytest.mark.core @pytest.mark.core
def testConfigSetMainPanePos(nwTemp,nwRef): def testConfigSetPanePos(nwTemp,nwRef):
tmpConf = path.join(nwTemp,"novelwriter.conf") tmpConf = path.join(nwTemp,"novelwriter.conf")
refConf = path.join(nwRef, "novelwriter.conf") refConf = path.join(nwRef, "novelwriter.conf")
assert theConf.setMainPanePos([0, 0]) assert theConf.setMainPanePos([0, 0])
@@ -77,3 +85,31 @@ def testConfigSetMainPanePos(nwTemp,nwRef):
assert theConf.saveConfig() assert theConf.saveConfig()
assert cmpFiles(tmpConf, refConf, [2]) assert cmpFiles(tmpConf, refConf, [2])
assert not theConf.confChanged assert not theConf.confChanged
@pytest.mark.core
def testConfigFlags(nwTemp,nwRef):
tmpConf = path.join(nwTemp,"novelwriter.conf")
refConf = path.join(nwRef, "novelwriter.conf")
assert not theConf.setShowRefPanel(False)
assert theConf.setShowRefPanel(True)
assert not theConf.setViewComments(False)
assert theConf.setViewComments(True)
assert theConf.confChanged
assert theConf.saveConfig()
assert cmpFiles(tmpConf, refConf, [2])
assert not theConf.confChanged
@pytest.mark.core
def testConfigErrors(nwTemp):
nonPath = path.join("somewhere","over","the","rainbow")
assert theConf.initConfig(nonPath, nonPath)
assert theConf.hasError
assert not theConf.loadConfig()
assert not theConf.saveConfig()
assert not theConf.loadRecentCache()
assert len(theConf.getErrData()) > 0
@pytest.mark.core
def testConfigInternals():
assert theConf._checkNone(None) is None
assert theConf._checkNone("None") is None
+6 -8
View File
@@ -17,8 +17,8 @@ keyDelay = 10
stepDelay = 50 stepDelay = 50
@pytest.mark.gui @pytest.mark.gui
def testMainWindows(qtbot, nwTempGUI, nwRef): def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI]) nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI) qtbot.addWidget(nwGUI)
nwGUI.show() nwGUI.show()
qtbot.waitForWindowShown(nwGUI) qtbot.waitForWindowShown(nwGUI)
@@ -36,7 +36,6 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
assert nwGUI.theProject.trashRoot is None assert nwGUI.theProject.trashRoot is None
assert nwGUI.theProject.projPath is None assert nwGUI.theProject.projPath is None
assert nwGUI.theProject.projMeta is None assert nwGUI.theProject.projMeta is None
assert nwGUI.theProject.projCache is None
assert nwGUI.theProject.projFile == "nwProject.nwx" assert nwGUI.theProject.projFile == "nwProject.nwx"
assert nwGUI.theProject.projName == "" assert nwGUI.theProject.projName == ""
assert nwGUI.theProject.bookTitle == "" assert nwGUI.theProject.bookTitle == ""
@@ -61,7 +60,6 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
assert nwGUI.theProject.trashRoot is None assert nwGUI.theProject.trashRoot is None
assert nwGUI.theProject.projPath == nwTempGUI assert nwGUI.theProject.projPath == nwTempGUI
assert nwGUI.theProject.projMeta == path.join(nwTempGUI,"meta") assert nwGUI.theProject.projMeta == path.join(nwTempGUI,"meta")
assert nwGUI.theProject.projCache == path.join(nwTempGUI,"cache")
assert nwGUI.theProject.projFile == "nwProject.nwx" assert nwGUI.theProject.projFile == "nwProject.nwx"
assert nwGUI.theProject.projName == "" assert nwGUI.theProject.projName == ""
assert nwGUI.theProject.bookTitle == "" assert nwGUI.theProject.bookTitle == ""
@@ -252,8 +250,8 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
# qtbot.stopForInteraction() # qtbot.stopForInteraction()
@pytest.mark.gui @pytest.mark.gui
def testProjectEditor(qtbot, nwTempGUI, nwRef): def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI]) nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI) qtbot.addWidget(nwGUI)
nwGUI.show() nwGUI.show()
qtbot.waitForWindowShown(nwGUI) qtbot.waitForWindowShown(nwGUI)
@@ -339,8 +337,8 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef):
# qtbot.stopForInteraction() # qtbot.stopForInteraction()
@pytest.mark.gui @pytest.mark.gui
def testItemEditor(qtbot, nwTempGUI, nwRef): def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI]) nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
qtbot.addWidget(nwGUI) qtbot.addWidget(nwGUI)
nwGUI.show() nwGUI.show()
qtbot.waitForWindowShown(nwGUI) qtbot.waitForWindowShown(nwGUI)
+18 -2
View File
@@ -22,13 +22,14 @@ theProject = NWProject(theMain)
theProject.handleSeed = 42 theProject.handleSeed = 42
@pytest.mark.project @pytest.mark.project
def testProjectNew(nwTempProj,nwRef): def testProjectNew(nwTempProj,nwRef,nwTemp):
projFile = path.join(nwTempProj,"nwProject.nwx") projFile = path.join(nwTempProj,"nwProject.nwx")
refFile = path.join(nwRef,"proj","1_nwProject.nwx") refFile = path.join(nwRef,"proj","1_nwProject.nwx")
assert theConf.initConfig(nwRef) assert theConf.initConfig(nwRef, nwTemp)
assert theProject.newProject() assert theProject.newProject()
assert theProject.setProjectPath(nwTempProj) assert theProject.setProjectPath(nwTempProj)
assert theProject.saveProject() assert theProject.saveProject()
assert theProject.closeProject()
assert cmpFiles(projFile, refFile, [2]) assert cmpFiles(projFile, refFile, [2])
@pytest.mark.project @pytest.mark.project
@@ -41,9 +42,21 @@ def testProjectSave(nwTempProj,nwRef):
projFile = path.join(nwTempProj,"nwProject.nwx") projFile = path.join(nwTempProj,"nwProject.nwx")
refFile = path.join(nwRef,"proj","1_nwProject.nwx") refFile = path.join(nwRef,"proj","1_nwProject.nwx")
assert theProject.saveProject() assert theProject.saveProject()
assert theProject.closeProject()
assert cmpFiles(projFile, refFile, [2]) assert cmpFiles(projFile, refFile, [2])
assert not theProject.projChanged assert not theProject.projChanged
@pytest.mark.project
def testProjectOpenTwice(nwTempProj,nwRef):
projFile = path.join(nwTempProj,"nwProject.nwx")
refFile = path.join(nwRef,"proj","1_nwProject.nwx")
assert theProject.openProject(projFile)
assert not theProject.openProject(projFile)
assert theProject.openProject(projFile, overrideLock=True)
assert theProject.saveProject()
assert theProject.closeProject()
assert cmpFiles(projFile, refFile, [2])
@pytest.mark.project @pytest.mark.project
def testProjectNewRoot(nwTempProj,nwRef): def testProjectNewRoot(nwTempProj,nwRef):
projFile = path.join(nwTempProj,"nwProject.nwx") projFile = path.join(nwTempProj,"nwProject.nwx")
@@ -59,6 +72,7 @@ def testProjectNewRoot(nwTempProj,nwRef):
assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str) assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str)
assert theProject.projChanged assert theProject.projChanged
assert theProject.saveProject() assert theProject.saveProject()
assert theProject.closeProject()
assert cmpFiles(projFile, refFile, [2]) assert cmpFiles(projFile, refFile, [2])
assert not theProject.projChanged assert not theProject.projChanged
@@ -95,3 +109,5 @@ def testIndexScanThis(nwTempProj):
assert isValid assert isValid
assert str(theBits) == "['@tag', 'this', 'and this']" assert str(theBits) == "['@tag', 'this', 'and this']"
assert str(thePos) == "[0, 6, 12]" assert str(thePos) == "[0, 6, 12]"
assert theProject.closeProject()