Fix merge conflicts
This commit is contained in:
+4
-3
@@ -1,10 +1,9 @@
|
||||
os: linux
|
||||
dist: xenial
|
||||
dist: bionic
|
||||
services:
|
||||
- xvfb
|
||||
language: python
|
||||
cache: bundler
|
||||
sudo: required
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
@@ -12,9 +11,11 @@ addons:
|
||||
- python3-pyqt5
|
||||
- python3-pyqt5.qtsvg
|
||||
python:
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
- "3.8"
|
||||
install:
|
||||
- pip install --upgrade pip
|
||||
- pip install -r requirements.txt
|
||||
# - pip install pytest-faulthandler
|
||||
- pip install pytest-xvfb
|
||||
|
||||
@@ -1,5 +1,46 @@
|
||||
# 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]
|
||||
|
||||
**User Interface**
|
||||
|
||||
@@ -70,14 +70,13 @@ For the apt package manager on Debian systems, the following Python3 packages ar
|
||||
|
||||
* `python3-pyqt5` for the GUI
|
||||
* `python3-pyqt5.qtsvg` may need to be installed separately
|
||||
* `python3-appdirs` for locating the system's config folder
|
||||
* `python3-lxml` for writing project files
|
||||
|
||||
These are optional, but recommended:
|
||||
|
||||
* `python3-enchant` for better spell checking
|
||||
* `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
|
||||
```
|
||||
@@ -88,7 +87,6 @@ in the application folder.
|
||||
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 appdirs
|
||||
python3 -m pip install lxml
|
||||
python3 -m pip install pyenchant
|
||||
python3 -m pip install latexcodec
|
||||
|
||||
+3
-3
@@ -20,13 +20,13 @@
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "novelWriter"
|
||||
copyright = "2018-2019, Veronica Berglyd Olsen"
|
||||
copyright = "2018-2020, Veronica Berglyd Olsen"
|
||||
author = "Veronica Berglyd Olsen"
|
||||
|
||||
# The short X.Y version
|
||||
version = "0.4.2"
|
||||
version = "0.4.5"
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = "0.4.2"
|
||||
release = "0.4.5"
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
@@ -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 three. 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 renderred as italics text."
|
||||
"``__text__``", "The text is renderred as underlined text."
|
||||
"``**text**``", "The text is rendered as bold text."
|
||||
"``_text_``", "The text is rendered as italicized text."
|
||||
"``__text__``", "The text is rendered as underlined text."
|
||||
"``% text...``", "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."
|
||||
|
||||
|
||||
@@ -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.
|
||||
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.
|
||||
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.
|
||||
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
|
||||
---------------------------------
|
||||
|
||||
@@ -6,8 +6,8 @@ You can download novelWriter from https://github.com/vkbo/novelWriter/releases
|
||||
|
||||
Latest version is |version|:
|
||||
|
||||
* ZIP file: https://github.com/vkbo/novelWriter/archive/v0.3.2.zip
|
||||
* TAR file: https://github.com/vkbo/novelWriter/archive/v0.3.2.tar.gz
|
||||
* ZIP file: https://github.com/vkbo/novelWriter/archive/v0.4.5.zip
|
||||
* TAR file: https://github.com/vkbo/novelWriter/archive/v0.4.5.tar.gz
|
||||
|
||||
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:
|
||||
|
||||
* ``pyqt5`` for the GUI
|
||||
* ``appdirs`` for locating the system's config folder
|
||||
* ``lxml`` for writing project files
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -28,7 +28,7 @@ The project XML file is suitable for diff tools and version control, although a
|
||||
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.
|
||||
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.
|
||||
|
||||
+2
-2
@@ -63,8 +63,8 @@ else:
|
||||
instOpt = [
|
||||
"--name=novelWriter",
|
||||
"--onefile",
|
||||
"--add-data=%s%s%s" % (os.path.join("nw", "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", "themes"), dotDot,"themes"),
|
||||
"--add-data=%s%s%s" % (os.path.join("nw", "assets", "graphics"), dotDot,"graphics"),
|
||||
"--icon=%s" % os.path.join("nw", "assets", "icons", "novelWriter.ico"),
|
||||
]
|
||||
if buildWindowed:
|
||||
|
||||
+2
-8
@@ -3,8 +3,8 @@
|
||||
|
||||
import sys
|
||||
|
||||
if sys.hexversion < 0x030500F0:
|
||||
print("ERROR: At least Python 3.5 is required")
|
||||
if sys.hexversion < 0x030600F0:
|
||||
print("ERROR: At least Python 3.6 is required")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
@@ -27,12 +27,6 @@ except:
|
||||
print("ERROR: Failed to load dependency python3-lxml")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
import appdirs
|
||||
except:
|
||||
print("ERROR: Failed to load dependency python3-appdirs")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import nw
|
||||
nw.main(sys.argv[1:])
|
||||
|
||||
+19
-14
@@ -23,10 +23,10 @@ from nw.config import Config
|
||||
|
||||
__package__ = "novelWriter"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__copyright__ = "Copyright 2018–2019, Veronica Berglyd Olsen"
|
||||
__copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen"
|
||||
__license__ = "GPLv3"
|
||||
__version__ = "0.4.2"
|
||||
__date__ = "2019-11-17"
|
||||
__version__ = "0.4.5"
|
||||
__date__ = "2020-02-17"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
__status__ = "Development"
|
||||
@@ -77,17 +77,17 @@ def main(sysArgs=None):
|
||||
sysArgs = sys.argv[1:]
|
||||
|
||||
# Valid Input Options
|
||||
shortOpt = "hdiqtl:v"
|
||||
shortOpt = "hdiql:v"
|
||||
longOpt = [
|
||||
"help",
|
||||
"debug",
|
||||
"info",
|
||||
"verbose",
|
||||
"quiet",
|
||||
"time",
|
||||
"logfile=",
|
||||
"version",
|
||||
"config=",
|
||||
"data=",
|
||||
"testmode",
|
||||
"style=",
|
||||
]
|
||||
@@ -103,10 +103,10 @@ def main(sysArgs=None):
|
||||
" -d, --debug Print debug output.\n"
|
||||
" --verbose Increase verbosity of debug output.\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"
|
||||
" --style= Set Qt5 style flag. Defaults to 'Fusion'.\n"
|
||||
" --config= Alternative config file.\n"
|
||||
" --data= Alternative user data path.\n"
|
||||
" --headless Do not display GUI. Useful for testing scripts.\n"
|
||||
).format(
|
||||
appname = __package__,
|
||||
@@ -118,22 +118,25 @@ def main(sysArgs=None):
|
||||
# Defaults
|
||||
debugLevel = logging.WARN
|
||||
debugStr = "{levelname:8} {message:}"
|
||||
timeStr = "[{asctime:}] "
|
||||
logFile = ""
|
||||
toFile = False
|
||||
toStd = True
|
||||
showTime = False
|
||||
confPath = None
|
||||
dataPath = None
|
||||
testMode = False
|
||||
qtStyle = "Fusion"
|
||||
cmdOpen = None
|
||||
|
||||
# Parse Options
|
||||
try:
|
||||
inOpts, inArgs = getopt.getopt(sysArgs,shortOpt,longOpt)
|
||||
inOpts, inRemain = getopt.getopt(sysArgs,shortOpt,longOpt)
|
||||
except getopt.GetoptError:
|
||||
print(helpMsg)
|
||||
sys.exit(2)
|
||||
|
||||
if len(inRemain) > 0:
|
||||
cmdOpen = inRemain[0]
|
||||
|
||||
for inOpt, inArg in inOpts:
|
||||
if inOpt in ("-h","--help"):
|
||||
print(helpMsg)
|
||||
@@ -145,7 +148,7 @@ def main(sysArgs=None):
|
||||
debugLevel = logging.INFO
|
||||
elif inOpt in ("-d", "--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"):
|
||||
logFile = inArg
|
||||
toFile = True
|
||||
@@ -153,21 +156,21 @@ def main(sysArgs=None):
|
||||
toStd = False
|
||||
elif inOpt in ("--verbose"):
|
||||
debugLevel = VERBOSE
|
||||
elif inOpt in ("-t","--time"):
|
||||
showTime = True
|
||||
elif inOpt in ("--style"):
|
||||
qtStyle = inArg
|
||||
elif inOpt in ("--config"):
|
||||
confPath = inArg
|
||||
elif inOpt in ("--data"):
|
||||
dataPath = inArg
|
||||
elif inOpt in ("--testmode"):
|
||||
testMode = True
|
||||
|
||||
# Set Config Options
|
||||
CONFIG.showGUI = not testMode
|
||||
CONFIG.debugInfo = debugLevel < logging.INFO
|
||||
CONFIG.cmdOpen = cmdOpen
|
||||
|
||||
# Set Logging
|
||||
if showTime: debugStr = timeStr+debugStr
|
||||
logFmt = logging.Formatter(fmt=debugStr,datefmt="%Y-%m-%d %H:%M:%S",style="{")
|
||||
|
||||
if not logFile == "" and toFile:
|
||||
@@ -189,13 +192,15 @@ def main(sysArgs=None):
|
||||
|
||||
logger.setLevel(debugLevel)
|
||||
|
||||
CONFIG.initConfig(confPath)
|
||||
CONFIG.initConfig(confPath, dataPath)
|
||||
|
||||
if testMode:
|
||||
nwGUI = GuiMain()
|
||||
return nwGUI
|
||||
else:
|
||||
nwApp = QApplication([__package__,("-style=%s" % qtStyle)])
|
||||
nwApp.setApplicationName(__package__)
|
||||
nwApp.setApplicationVersion(__version__)
|
||||
nwGUI = GuiMain()
|
||||
sys.exit(nwApp.exec_())
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
FROM ICON SET: Typicons
|
||||
LICENSE: Creative Commons (Attribution-Share Alike 3.0 Unported)
|
||||
https://creativecommons.org/licenses/by-sa/3.0/
|
||||
|
||||
Apllies to:
|
||||
export.svg
|
||||
merge.svg
|
||||
split.svg
|
||||
@@ -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 |
@@ -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 |
@@ -88,6 +88,25 @@ def colRange(rgbStart, rgbEnd, nStep):
|
||||
|
||||
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):
|
||||
""" Splits a version string on the form aa.bb.cc into major, minor
|
||||
and patch, and computes an integer value aabbcc.
|
||||
|
||||
+172
-41
@@ -12,15 +12,15 @@
|
||||
|
||||
import logging
|
||||
import configparser
|
||||
import json
|
||||
import sys
|
||||
import nw
|
||||
|
||||
from os import path, mkdir, makedirs
|
||||
from appdirs import user_config_dir
|
||||
from os import path, mkdir, unlink, rename
|
||||
from datetime import datetime
|
||||
|
||||
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.common import splitVersionNumber
|
||||
@@ -41,10 +41,16 @@ class Config:
|
||||
self.appHandle = nw.__package__.lower()
|
||||
self.showGUI = True
|
||||
self.debugInfo = False
|
||||
self.cmdOpen = None
|
||||
|
||||
# Config Error Handling
|
||||
self.hasError = False
|
||||
self.errData = []
|
||||
|
||||
# Set Paths
|
||||
self.confPath = None
|
||||
self.confFile = None
|
||||
self.dataPath = None
|
||||
self.homePath = None
|
||||
self.lastPath = None
|
||||
self.appPath = None
|
||||
@@ -57,7 +63,7 @@ class Config:
|
||||
self.iconPath = None
|
||||
|
||||
# Set default values
|
||||
self.confChanged = False
|
||||
self.confChanged = False
|
||||
|
||||
## General
|
||||
self.guiTheme = "default"
|
||||
@@ -111,9 +117,6 @@ class Config:
|
||||
self.showRefPanel = True
|
||||
self.viewComments = True
|
||||
|
||||
## Path
|
||||
self.recentList = [""]*10
|
||||
|
||||
# Check Qt5 Versions
|
||||
verQt = splitVersionNumber(QT_VERSION_STR)
|
||||
self.verQtString = QT_VERSION_STR
|
||||
@@ -153,24 +156,48 @@ class Config:
|
||||
else:
|
||||
self.osUnknown = True
|
||||
|
||||
# Other System Info
|
||||
self.hostName = QSysInfo.machineHostName()
|
||||
self.kernelVer = QSysInfo.kernelVersion()
|
||||
|
||||
# Packages
|
||||
self.hasEnchant = False
|
||||
self.hasSymSpell = False
|
||||
|
||||
# Recent Cache
|
||||
self.recentProj = {}
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# 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:
|
||||
self.confPath = user_config_dir(self.appHandle)
|
||||
confRoot = QStandardPaths.writableLocation(QStandardPaths.ConfigLocation)
|
||||
self.confPath = path.join(path.abspath(confRoot), self.appHandle)
|
||||
else:
|
||||
logger.info("Setting config from alternative path: %s" % 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.homePath = path.expanduser("~")
|
||||
self.lastPath = self.homePath
|
||||
@@ -185,20 +212,42 @@ class Config:
|
||||
|
||||
# If config folder does not exist, make it.
|
||||
# This assumes that the os config folder itself exists.
|
||||
if self.osWindows:
|
||||
if not path.isdir(self.confPath):
|
||||
makedirs(self.confPath)
|
||||
else:
|
||||
if not path.isdir(self.confPath):
|
||||
if not path.isdir(self.confPath):
|
||||
try:
|
||||
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
|
||||
if path.isfile(path.join(self.confPath,self.confFile)):
|
||||
# If it exists, load it
|
||||
self.loadConfig()
|
||||
else:
|
||||
# If it does not exist, save a copy of the default values
|
||||
self.saveConfig()
|
||||
if self.confPath is not None:
|
||||
if path.isfile(path.join(self.confPath,self.confFile)):
|
||||
# If it exists, load it
|
||||
self.loadConfig()
|
||||
else:
|
||||
# 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
|
||||
self._checkOptionalPackages()
|
||||
@@ -220,6 +269,10 @@ class Config:
|
||||
)
|
||||
except Exception as e:
|
||||
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
|
||||
|
||||
## Main
|
||||
@@ -353,10 +406,6 @@ class Config:
|
||||
self.lastPath = self._parseLine(
|
||||
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
|
||||
self.spellLanguage = self._checkNone(self.spellLanguage)
|
||||
@@ -435,8 +484,6 @@ class Config:
|
||||
cnfSec = "Path"
|
||||
cnfParse.add_section(cnfSec)
|
||||
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
|
||||
try:
|
||||
@@ -444,24 +491,93 @@ class Config:
|
||||
self.confChanged = False
|
||||
except Exception as e:
|
||||
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 True
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
def loadRecentCache(self):
|
||||
"""Load the cache file for recent projects.
|
||||
"""
|
||||
|
||||
def setRecent(self, recentPath):
|
||||
if recentPath == "": return
|
||||
if recentPath in self.recentList[0:10]:
|
||||
self.recentList.remove(recentPath)
|
||||
self.recentList.insert(0,recentPath)
|
||||
return
|
||||
if self.dataPath is None:
|
||||
return False
|
||||
|
||||
def clearRecent(self):
|
||||
self.recentList = [""]*10
|
||||
return
|
||||
cacheFile = path.join(self.dataPath, nwFiles.RECENT_FILE)
|
||||
self.recentProj = {}
|
||||
|
||||
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):
|
||||
if newPath is None:
|
||||
@@ -473,6 +589,15 @@ class Config:
|
||||
self.confFile = path.basename(newPath)
|
||||
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):
|
||||
if lastPath is None or lastPath == "":
|
||||
self.lastPath = ""
|
||||
@@ -507,12 +632,18 @@ class Config:
|
||||
def setShowRefPanel(self, checkState):
|
||||
self.showRefPanel = checkState
|
||||
self.confChanged = True
|
||||
return
|
||||
return self.showRefPanel
|
||||
|
||||
def setViewComments(self, checkState):
|
||||
self.viewComments = checkState
|
||||
self.confChanged = True
|
||||
return
|
||||
return self.viewComments
|
||||
|
||||
def getErrData(self):
|
||||
errMessage = "<br>".join(self.errData)
|
||||
self.hasError = False
|
||||
self.errData = []
|
||||
return errMessage
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
@@ -550,7 +681,7 @@ class Config:
|
||||
if checkVal is None:
|
||||
return None
|
||||
if isinstance(checkVal, str):
|
||||
if checkVal.lower == "none":
|
||||
if checkVal.lower() == "none":
|
||||
return None
|
||||
return checkVal
|
||||
|
||||
|
||||
@@ -22,13 +22,12 @@ class nwFiles():
|
||||
|
||||
APP_ICON = "novelWriter.svg"
|
||||
PROJ_FILE = "nwProject.nwx"
|
||||
PROJ_COUNT = "projCount.txt"
|
||||
PROJ_DICT = "wordlist.txt"
|
||||
PROJ_LOCK = "nwProject.lock"
|
||||
SESS_INFO = "sessionInfo.log"
|
||||
INDEX_FILE = "tagsIndex.json"
|
||||
EXPORT_OPT = "exportOptions.json"
|
||||
OUTLINE_OPT = "outlineOptions.json"
|
||||
SLOG_OPT = "sessionLogOptions.json"
|
||||
OPTS_FILE = "guiOptions.json"
|
||||
RECENT_FILE = "recentProjects.json"
|
||||
|
||||
# END Class nwFiles
|
||||
|
||||
@@ -116,10 +115,6 @@ class nwDependencies():
|
||||
"site" : "",
|
||||
"docs" : "",
|
||||
},
|
||||
"appdirs" : {
|
||||
"site" : "",
|
||||
"docs" : "",
|
||||
},
|
||||
"lxml" : {
|
||||
"site" : "",
|
||||
"docs" : "",
|
||||
|
||||
@@ -8,9 +8,12 @@ from nw.gui.theme import GuiTheme
|
||||
|
||||
# Dialogs
|
||||
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.itemeditor import GuiItemEditor
|
||||
from nw.gui.dialogs.projecteditor import GuiProjectEditor
|
||||
from nw.gui.dialogs.projectload import GuiProjectLoad
|
||||
from nw.gui.dialogs.sessionlog import GuiSessionLogView
|
||||
|
||||
# GUI Elements
|
||||
@@ -33,9 +36,12 @@ __all__ = [
|
||||
"GuiMainStatus",
|
||||
"GuiTheme",
|
||||
"GuiConfigEditor",
|
||||
"GuiDocMerge",
|
||||
"GuiDocSplit",
|
||||
"GuiExport",
|
||||
"GuiItemEditor",
|
||||
"GuiProjectEditor",
|
||||
"GuiProjectLoad",
|
||||
"GuiSessionLogView",
|
||||
"GuiDocDetails",
|
||||
"GuiDocEditor",
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
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.itemeditor import GuiItemEditor
|
||||
from nw.gui.dialogs.projecteditor import GuiProjectEditor
|
||||
from nw.gui.dialogs.projectload import GuiProjectLoad
|
||||
from nw.gui.dialogs.sessionlog import GuiSessionLogView
|
||||
|
||||
__all__ = [
|
||||
"GuiConfigEditor",
|
||||
"GuiDocMerge",
|
||||
"GuiDocSplit",
|
||||
"GuiExport",
|
||||
"GuiItemEditor",
|
||||
"GuiProjectEditor",
|
||||
"GuiProjectLoad",
|
||||
"GuiSessionLogView",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -24,7 +24,6 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from nw.convert import TextFile, HtmlFile, MarkdownFile, LaTeXFile, ConcatFile
|
||||
from nw.tools import OptLastState
|
||||
from nw.common import packageRefURL
|
||||
from nw.constants import nwFiles, nwItemType, nwAlert
|
||||
|
||||
@@ -40,8 +39,7 @@ class GuiExport(QDialog):
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
self.optState = ExportLastState(self.theProject,nwFiles.EXPORT_OPT)
|
||||
self.optState.loadSettings()
|
||||
self.optState = self.theProject.optState
|
||||
|
||||
self.outerBox = QHBoxLayout()
|
||||
self.innerBox = QVBoxLayout()
|
||||
@@ -50,8 +48,8 @@ class GuiExport(QDialog):
|
||||
|
||||
self.guiDeco = self.theParent.theTheme.loadDecoration("export",(64,64))
|
||||
|
||||
self.tabMain = GuiExportMain(self.theParent, self.theProject, self.optState)
|
||||
self.tabPandoc = GuiExportPandoc(self.theParent, self.theProject, self.optState)
|
||||
self.tabMain = GuiExportMain(self.theParent, self.theProject)
|
||||
self.tabPandoc = GuiExportPandoc(self.theParent, self.theProject)
|
||||
|
||||
self.tabWidget = QTabWidget()
|
||||
self.tabWidget.addTab(self.tabMain, "Settings")
|
||||
@@ -290,24 +288,24 @@ class GuiExport(QDialog):
|
||||
if saveTo.startswith("~"):
|
||||
saveTo = path.expanduser(saveTo)
|
||||
|
||||
self.optState.setSetting("wNovel", wNovel)
|
||||
self.optState.setSetting("wNotes", wNotes)
|
||||
self.optState.setSetting("eFormat", eFormat)
|
||||
self.optState.setSetting("fixWidth", fixWidth)
|
||||
self.optState.setSetting("wComments",wComments)
|
||||
self.optState.setSetting("wKeywords",wKeywords)
|
||||
self.optState.setSetting("chFormat", chFormat)
|
||||
self.optState.setSetting("unFormat", unFormat)
|
||||
self.optState.setSetting("scFormat", scFormat)
|
||||
self.optState.setSetting("seFormat", seFormat)
|
||||
self.optState.setSetting("saveTo", saveTo)
|
||||
self.optState.setSetting("hScene", hScene)
|
||||
self.optState.setSetting("hSection", hSection)
|
||||
self.optState.setValue("GuiExport", "wNovel", wNovel)
|
||||
self.optState.setValue("GuiExport", "wNotes", wNotes)
|
||||
self.optState.setValue("GuiExport", "eFormat", eFormat)
|
||||
self.optState.setValue("GuiExport", "fixWidth", fixWidth)
|
||||
self.optState.setValue("GuiExport", "wComments", wComments)
|
||||
self.optState.setValue("GuiExport", "wKeywords", wKeywords)
|
||||
self.optState.setValue("GuiExport", "chFormat", chFormat)
|
||||
self.optState.setValue("GuiExport", "unFormat", unFormat)
|
||||
self.optState.setValue("GuiExport", "scFormat", scFormat)
|
||||
self.optState.setValue("GuiExport", "seFormat", seFormat)
|
||||
self.optState.setValue("GuiExport", "saveTo", saveTo)
|
||||
self.optState.setValue("GuiExport", "hScene", hScene)
|
||||
self.optState.setValue("GuiExport", "hSection", hSection)
|
||||
|
||||
# Pandoc Settings
|
||||
pFormat = self.tabPandoc.outputFormat.currentData()
|
||||
|
||||
self.optState.setSetting("pFormat", pFormat)
|
||||
self.optState.setValue("GuiExport", "pFormat", pFormat)
|
||||
|
||||
self.optState.saveSettings()
|
||||
self.close()
|
||||
@@ -362,14 +360,14 @@ class GuiExportMain(QWidget):
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, theParent, theProject, optState):
|
||||
def __init__(self, theParent, theProject):
|
||||
QWidget.__init__(self, theParent)
|
||||
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
self.theTheme = theParent.theTheme
|
||||
self.outerBox = QGridLayout()
|
||||
self.optState = optState
|
||||
self.optState = self.theProject.optState
|
||||
self.currFormat = self.FMT_TXT
|
||||
|
||||
# Select Files
|
||||
@@ -378,19 +376,27 @@ class GuiExportMain(QWidget):
|
||||
self.guiFiles.setLayout(self.guiFilesForm)
|
||||
|
||||
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.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.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.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.guiFilesForm.addWidget(self.expNovel, 0, 1)
|
||||
@@ -406,13 +412,17 @@ class GuiExportMain(QWidget):
|
||||
|
||||
self.chapterFormat = QLineEdit()
|
||||
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.setMinimumWidth(250)
|
||||
|
||||
self.unnumFormat = QLineEdit()
|
||||
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.setMinimumWidth(250)
|
||||
|
||||
@@ -428,22 +438,30 @@ class GuiExportMain(QWidget):
|
||||
|
||||
self.sceneFormat = QLineEdit()
|
||||
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.setMinimumWidth(100)
|
||||
|
||||
self.sectionFormat = QLineEdit()
|
||||
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.setMinimumWidth(100)
|
||||
|
||||
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.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.guiScenesForm.addWidget(QLabel("Scenes"), 0, 0)
|
||||
@@ -458,8 +476,9 @@ class GuiExportMain(QWidget):
|
||||
self.exportToForm = QGridLayout(self)
|
||||
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.clicked.connect(self._exportFolder)
|
||||
|
||||
@@ -486,7 +505,9 @@ class GuiExportMain(QWidget):
|
||||
self.outputFormat.addItem("Pandoc via Markdown or HTML", self.FMT_PDOC)
|
||||
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:
|
||||
self.outputFormat.setCurrentIndex(1)
|
||||
self._updateFormat(1)
|
||||
@@ -508,7 +529,9 @@ class GuiExportMain(QWidget):
|
||||
self.fixedWidth.setMinimum(0)
|
||||
self.fixedWidth.setMaximum(999)
|
||||
self.fixedWidth.setSingleStep(1)
|
||||
self.fixedWidth.setValue(self.optState.getSetting("fixWidth"))
|
||||
self.fixedWidth.setValue(
|
||||
self.optState.getInt("GuiExport", "fixWidth", 80)
|
||||
)
|
||||
self.fixedWidth.setToolTip(
|
||||
"Applies to .txt and .md files. A value of '0' disables the feature."
|
||||
)
|
||||
@@ -609,13 +632,13 @@ class GuiExportPandoc(QWidget):
|
||||
FMT_ZIM : "markdown",
|
||||
}
|
||||
|
||||
def __init__(self, theParent, theProject, optState):
|
||||
def __init__(self, theParent, theProject):
|
||||
QWidget.__init__(self, theParent)
|
||||
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
self.outerBox = QGridLayout()
|
||||
self.optState = optState
|
||||
self.optState = self.theProject.optState
|
||||
|
||||
try:
|
||||
import pypandoc
|
||||
@@ -659,7 +682,9 @@ class GuiExportPandoc(QWidget):
|
||||
self.outputFormat.addItem("ePUB eBook v3 (.epub3)", self.FMT_EPUB3)
|
||||
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:
|
||||
self.outputFormat.setCurrentIndex(1)
|
||||
else:
|
||||
@@ -678,30 +703,3 @@ class GuiExportPandoc(QWidget):
|
||||
return
|
||||
|
||||
# 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
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from os import path
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QHBoxLayout, QVBoxLayout, QGroupBox, QFormLayout, QLineEdit,
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from os import path
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QIcon, QPixmap, QColor, QBrush
|
||||
from PyQt5.QtWidgets import (
|
||||
|
||||
@@ -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
|
||||
@@ -24,7 +24,6 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
|
||||
from nw.constants import nwConst, nwFiles, nwAlert
|
||||
from nw.tools import OptLastState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,8 +37,7 @@ class GuiSessionLogView(QDialog):
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theProject = theProject
|
||||
self.theParent = theParent
|
||||
self.optState = SessionLogLastState(self.theProject,nwFiles.SLOG_OPT)
|
||||
self.optState.loadSettings()
|
||||
self.optState = self.theProject.optState
|
||||
|
||||
self.timeFilter = 0.0
|
||||
self.timeTotal = 0.0
|
||||
@@ -52,13 +50,13 @@ class GuiSessionLogView(QDialog):
|
||||
self.setMinimumHeight(400)
|
||||
|
||||
widthCol0 = self.optState.validIntRange(
|
||||
self.optState.getSetting("widthCol0"), 30, 999, 180
|
||||
self.optState.getInt("GuiSession", "widthCol0", 180), 30, 999, 180
|
||||
)
|
||||
widthCol1 = self.optState.validIntRange(
|
||||
self.optState.getSetting("widthCol1"), 30, 999, 80
|
||||
self.optState.getInt("GuiSession", "widthCol1", 80), 30, 999, 80
|
||||
)
|
||||
widthCol2 = self.optState.validIntRange(
|
||||
self.optState.getSetting("widthCol2"), 30, 999, 80
|
||||
self.optState.getInt("GuiSession", "widthCol2", 80), 30, 999, 80
|
||||
)
|
||||
|
||||
self.listBox = QTreeWidget()
|
||||
@@ -77,10 +75,11 @@ class GuiSessionLogView(QDialog):
|
||||
|
||||
sortValid = (Qt.AscendingOrder, Qt.DescendingOrder)
|
||||
sortCol = self.optState.validIntRange(
|
||||
self.optState.getSetting("sortCol"), 0, 2, 0
|
||||
self.optState.getInt("GuiSession", "sortCol", 0), 0, 2, 0
|
||||
)
|
||||
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)
|
||||
@@ -110,11 +109,15 @@ class GuiSessionLogView(QDialog):
|
||||
self.filterBox.setLayout(self.filterBoxForm)
|
||||
|
||||
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.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.filterBoxForm.addWidget(self.hideZeros, 0, 0)
|
||||
@@ -208,13 +211,13 @@ class GuiSessionLogView(QDialog):
|
||||
hideZeros = self.hideZeros.isChecked()
|
||||
hideNegative = self.hideNegative.isChecked()
|
||||
|
||||
self.optState.setSetting("widthCol0", widthCol0)
|
||||
self.optState.setSetting("widthCol1", widthCol1)
|
||||
self.optState.setSetting("widthCol2", widthCol2)
|
||||
self.optState.setSetting("sortCol", sortCol)
|
||||
self.optState.setSetting("sortOrder", sortOrder)
|
||||
self.optState.setSetting("hideZeros", hideZeros)
|
||||
self.optState.setSetting("hideNegative",hideNegative)
|
||||
self.optState.setValue("GuiSession", "widthCol0", widthCol0)
|
||||
self.optState.setValue("GuiSession", "widthCol1", widthCol1)
|
||||
self.optState.setValue("GuiSession", "widthCol2", widthCol2)
|
||||
self.optState.setValue("GuiSession", "sortCol", sortCol)
|
||||
self.optState.setValue("GuiSession", "sortOrder", sortOrder)
|
||||
self.optState.setValue("GuiSession", "hideZeros", hideZeros)
|
||||
self.optState.setValue("GuiSession", "hideNegative", hideNegative)
|
||||
|
||||
self.optState.saveSettings()
|
||||
self.close()
|
||||
@@ -237,23 +240,3 @@ class GuiSessionLogView(QDialog):
|
||||
return "%02d:%02d:%02d" % (tH,tM,tS)
|
||||
|
||||
# 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
|
||||
|
||||
@@ -23,11 +23,6 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiDocDetails(QFrame):
|
||||
|
||||
C_NAME = 0
|
||||
C_COUNT = 1
|
||||
C_FLAGS = 2
|
||||
C_HANDLE = 3
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
QFrame.__init__(self, theParent)
|
||||
|
||||
|
||||
@@ -21,13 +21,13 @@ from PyQt5.QtWidgets import (
|
||||
)
|
||||
from PyQt5.QtGui import (
|
||||
QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette,
|
||||
QTextDocument
|
||||
QTextDocument, QCursor
|
||||
)
|
||||
|
||||
from nw.project import NWDoc
|
||||
from nw.gui.tools import GuiDocHighlighter, WordCounter
|
||||
from nw.tools import NWSpellCheck, NWSpellSimple
|
||||
from nw.constants import nwFiles, nwUnicode, nwDocAction, nwAlert
|
||||
from nw.tools import NWSpellSimple
|
||||
from nw.constants import nwUnicode, nwDocAction
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -209,7 +209,7 @@ class GuiDocEditor(QTextEdit):
|
||||
risk overwriting the file if it exists. This can for instance
|
||||
happen of the file contains binary elements or an encoding that
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -219,6 +219,7 @@ class GuiDocEditor(QTextEdit):
|
||||
self.clearEditor()
|
||||
return False
|
||||
|
||||
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
|
||||
self.hLight.setHandle(tHandle)
|
||||
|
||||
# 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.hLight.spellCheck = spTemp
|
||||
qApp.restoreOverrideCursor()
|
||||
|
||||
return True
|
||||
|
||||
@@ -277,8 +279,9 @@ class GuiDocEditor(QTextEdit):
|
||||
return True
|
||||
|
||||
def updateDocMargins(self):
|
||||
"""Automatically adjust the margins so the text is centred, but
|
||||
only if Config.textFixedW is enabled or we're in Zen mode.
|
||||
"""Automatically adjust the margins so the text is centred if
|
||||
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:
|
||||
@@ -335,7 +338,7 @@ class GuiDocEditor(QTextEdit):
|
||||
return theText
|
||||
|
||||
def setCursorPosition(self, thePosition):
|
||||
if thePosition > 0:
|
||||
if thePosition >= 0:
|
||||
theCursor = self.textCursor()
|
||||
theCursor.setPosition(thePosition)
|
||||
self.setTextCursor(theCursor)
|
||||
@@ -361,8 +364,8 @@ class GuiDocEditor(QTextEdit):
|
||||
def setSpellCheck(self, theMode):
|
||||
"""This is the master spell check setting function, and this one
|
||||
should call all other setSpellCheck functions in other classes.
|
||||
If the spell check mode is not defined, then toggle the current
|
||||
status saved in the class.
|
||||
If the spell check mode (theMode) is not defined (None), then
|
||||
toggle the current status saved in this class.
|
||||
"""
|
||||
|
||||
if theMode is None:
|
||||
@@ -375,23 +378,29 @@ class GuiDocEditor(QTextEdit):
|
||||
self.theParent.mainMenu.setSpellCheck(theMode)
|
||||
self.theProject.setSpellCheck(theMode)
|
||||
self.hLight.setSpellCheck(theMode)
|
||||
self.reHighlightDocument()
|
||||
if not self.bigDoc:
|
||||
self.spellCheckDocument()
|
||||
|
||||
logger.verbose("Spell check is set to %s" % str(theMode))
|
||||
|
||||
return True
|
||||
|
||||
def reHighlightDocument(self):
|
||||
def spellCheckDocument(self):
|
||||
"""Rerun the highlighter to update spell checking status of the
|
||||
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.
|
||||
"""
|
||||
|
||||
logger.verbose("Running spell checker")
|
||||
if self.spellCheck:
|
||||
theText = self.getText()
|
||||
self.clear()
|
||||
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()
|
||||
logger.debug("Document re-highlighted in %.3f milliseconds" % (1000*(afTime-bfTime)))
|
||||
|
||||
|
||||
+120
-19
@@ -16,10 +16,10 @@ import nw
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtGui import QFont, QColor
|
||||
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 (
|
||||
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert
|
||||
)
|
||||
@@ -171,14 +171,21 @@ class GuiDocTree(QTreeWidget):
|
||||
return False
|
||||
|
||||
# Add the new item to the tree
|
||||
nwItem = self.theProject.getItem(tHandle)
|
||||
trItem = self._addTreeItem(nwItem)
|
||||
self.revealTreeItem(tHandle)
|
||||
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():
|
||||
self.theMap[pHandle].setExpanded(True)
|
||||
self.clearSelection()
|
||||
trItem.setSelected(True)
|
||||
self.theParent.editItem()
|
||||
|
||||
return True
|
||||
|
||||
def moveTreeItem(self, nStep):
|
||||
@@ -221,6 +228,16 @@ class GuiDocTree(QTreeWidget):
|
||||
self.theProject.setTreeOrder(theList)
|
||||
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):
|
||||
retVals = [
|
||||
self.columnWidth(0),
|
||||
@@ -229,7 +246,44 @@ class GuiDocTree(QTreeWidget):
|
||||
]
|
||||
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
|
||||
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
|
||||
@@ -246,21 +300,61 @@ class GuiDocTree(QTreeWidget):
|
||||
trItemS = self._getTreeItem(tHandle)
|
||||
nwItemS = self.theProject.getItem(tHandle)
|
||||
|
||||
if nwItemS is None:
|
||||
return False
|
||||
|
||||
if nwItemS.itemType == nwItemType.FILE:
|
||||
logger.debug("User requested file %s moved to trash" % tHandle)
|
||||
trItemP = trItemS.parent()
|
||||
trItemT = self._addTrashRoot()
|
||||
if trItemP is None or trItemT is None:
|
||||
logger.error("Could not move item to trash")
|
||||
logger.error("Could not delete item")
|
||||
return False
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
trItemC = trItemP.takeChild(tIndex)
|
||||
trItemT.addChild(trItemC)
|
||||
nwItemS.setParent(self.theProject.trashRoot)
|
||||
self.clearSelection()
|
||||
trItemP.setSelected(True)
|
||||
self.theProject.setProjectChanged(True)
|
||||
self.theParent.theIndex.deleteHandle(tHandle)
|
||||
|
||||
pHandle = nwItemS.parHandle
|
||||
if pHandle is not None and pHandle == self.theProject.trashRoot:
|
||||
# If the file is in the trash folder already, as the
|
||||
# user if they want to permanently delete the file.
|
||||
|
||||
doPermanent = False
|
||||
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:
|
||||
logger.debug("User requested folder %s deleted" % tHandle)
|
||||
@@ -271,8 +365,6 @@ class GuiDocTree(QTreeWidget):
|
||||
tIndex = trItemP.indexOfChild(trItemS)
|
||||
if trItemS.childCount() == 0:
|
||||
trItemP.takeChild(tIndex)
|
||||
self.clearSelection()
|
||||
trItemP.setSelected(True)
|
||||
self.theProject.deleteItem(tHandle)
|
||||
else:
|
||||
self.makeAlert(["Cannot delete folder.","It is not empty."], nwAlert.ERROR)
|
||||
@@ -428,6 +520,9 @@ class GuiDocTree(QTreeWidget):
|
||||
return newItem
|
||||
|
||||
def _addTrashRoot(self):
|
||||
"""Adds the trash root folder if it doesn't already exist in the
|
||||
project tree.
|
||||
"""
|
||||
if self.theProject.trashRoot is None:
|
||||
self.theProject.addTrash()
|
||||
trItem = self._addTreeItem(
|
||||
@@ -518,16 +613,22 @@ class GuiDocTree(QTreeWidget):
|
||||
"""
|
||||
sHandle = self.getSelectedHandle()
|
||||
if sHandle is None:
|
||||
logger.error("No handle selected")
|
||||
return
|
||||
|
||||
dIndex = self.indexAt(theEvent.pos())
|
||||
dIndex = self.indexAt(theEvent.pos())
|
||||
if not dIndex.isValid():
|
||||
logger.error("Invalid drop index")
|
||||
return
|
||||
|
||||
dItem = self.itemFromIndex(dIndex)
|
||||
dHandle = dItem.text(self.C_HANDLE)
|
||||
snItem = self.theProject.getItem(sHandle)
|
||||
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
|
||||
isNone = snItem.itemClass == nwItemClass.NO_CLASS
|
||||
isNote = snItem.itemLayout == nwItemLayout.NOTE
|
||||
|
||||
@@ -21,7 +21,6 @@ from PyQt5.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QTreeWidget, QTreeWidgetItem
|
||||
)
|
||||
|
||||
from nw.tools import OptLastState
|
||||
from nw.constants import nwItemLayout, nwKeyWords, nwLabels, nwFiles
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -75,7 +74,7 @@ class GuiProjectOutline(QWidget):
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
self.theIndex = self.theParent.theIndex
|
||||
self.optState = OutlineLastState(self.theProject,nwFiles.OUTLINE_OPT)
|
||||
self.optState = self.theProject.optState
|
||||
|
||||
self.showWords = True
|
||||
self.showSynopsis = True
|
||||
@@ -111,13 +110,13 @@ class GuiProjectOutline(QWidget):
|
||||
colW.append(self.mainTree.columnWidth(iCol))
|
||||
|
||||
self.treeCols["width"] = colW
|
||||
self.optState.setSetting("headState", self.treeCols)
|
||||
self.optState.setValue("GuiProjectOutline", "headState", self.treeCols)
|
||||
self.optState.saveSettings()
|
||||
return
|
||||
|
||||
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 not isinstance(treeCols["order"], list): return
|
||||
@@ -253,15 +252,3 @@ class GuiProjectOutline(QWidget):
|
||||
return
|
||||
|
||||
# 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
|
||||
|
||||
@@ -86,13 +86,13 @@ class GuiSearchBar(QFrame):
|
||||
if not self.isVisible():
|
||||
self.setVisible(True)
|
||||
self.searchBox.setText(theText)
|
||||
self.searchBox.setFocus(True)
|
||||
self.searchBox.setFocus()
|
||||
logger.verbose("Setting search text to '%s'" % theText)
|
||||
return True
|
||||
|
||||
def setReplaceText(self, theText):
|
||||
self._replaceVisible(True)
|
||||
self.replaceBox.setFocus(True)
|
||||
self.replaceBox.setFocus()
|
||||
self.replaceBox.setText(theText)
|
||||
return True
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ from PyQt5.QtWidgets import (
|
||||
QWidget, QLabel, QScrollArea, QFrame, QToolButton, QCheckBox, QGridLayout
|
||||
)
|
||||
|
||||
from nw.constants import nwLabels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiDocViewDetails(QWidget):
|
||||
|
||||
+27
-3
@@ -45,8 +45,11 @@ class GuiIcons:
|
||||
}
|
||||
|
||||
DECO_MAP = {
|
||||
"export" : "export.svg",
|
||||
"settings" : "gear.svg",
|
||||
"nwicon" : ["icons", "novelWriter.svg"],
|
||||
"export" : ["graphics", "export.svg"],
|
||||
"merge" : ["graphics", "merge.svg"],
|
||||
"settings" : ["graphics", "gear.svg"],
|
||||
"split" : ["graphics", "split.svg"],
|
||||
}
|
||||
|
||||
def __init__(self, theParent):
|
||||
@@ -65,6 +68,9 @@ class GuiIcons:
|
||||
return
|
||||
|
||||
def initIcons(self, priPath):
|
||||
"""Load all icons listed in the icon map. Can be overridden by
|
||||
the selected theme.
|
||||
"""
|
||||
|
||||
self.priPath = priPath
|
||||
self.secPath = self.mainConf.iconPath
|
||||
@@ -76,12 +82,19 @@ class GuiIcons:
|
||||
return
|
||||
|
||||
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:
|
||||
logger.error("Decoration with name '%s' does not exist" % decoKey)
|
||||
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):
|
||||
logger.error("Decoration file '%s' not in assets folder" % self.DECO_MAP[decoKey])
|
||||
return QSvgWidget()
|
||||
@@ -93,11 +106,17 @@ class GuiIcons:
|
||||
return svgDeco
|
||||
|
||||
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:
|
||||
return self.qIcons[iconKey]
|
||||
return QIcon()
|
||||
|
||||
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:
|
||||
return self.qIcons[iconKey].pixmap(iconSize[0], iconSize[1], QIcon.Normal)
|
||||
return QPixmap()
|
||||
@@ -107,6 +126,11 @@ class GuiIcons:
|
||||
##
|
||||
|
||||
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:
|
||||
logger.error("Icon with name '%s' does not exist" % iconKey)
|
||||
|
||||
+21
-41
@@ -48,11 +48,6 @@ class GuiMainMenu(QMenuBar):
|
||||
|
||||
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):
|
||||
for itemClass in nwItemClass:
|
||||
if itemClass == nwItemClass.NO_CLASS: continue
|
||||
@@ -66,30 +61,6 @@ class GuiMainMenu(QMenuBar):
|
||||
# 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):
|
||||
"""Set the spell check check box to theMode. This is controlled
|
||||
by the document editor class, which holds the master spell check
|
||||
@@ -166,11 +137,6 @@ class GuiMainMenu(QMenuBar):
|
||||
QDesktopServices.openUrl(QUrl(nw.__docurl__))
|
||||
return True
|
||||
|
||||
def _clearRecentProjects(self):
|
||||
self.mainConf.clearRecent()
|
||||
self.updateRecentProjects()
|
||||
return True
|
||||
|
||||
def _showDocumentLocation(self):
|
||||
self.theParent.docEditor.revealLocation()
|
||||
return True
|
||||
@@ -194,7 +160,7 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aOpenProject = QAction("Open Project", self)
|
||||
self.aOpenProject.setStatusTip("Open project")
|
||||
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)
|
||||
|
||||
# Project > Save Project
|
||||
@@ -211,10 +177,6 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aCloseProject.triggered.connect(lambda : self.theParent.closeProject(False))
|
||||
self.projMenu.addAction(self.aCloseProject)
|
||||
|
||||
# Project > Recent Projects
|
||||
self.recentMenu = self.projMenu.addMenu("Recent Projects")
|
||||
self.updateRecentProjects()
|
||||
|
||||
# Project > Project Settings
|
||||
self.aProjectSettings = QAction("Project Settings", self)
|
||||
self.aProjectSettings.setStatusTip("Project settings")
|
||||
@@ -255,7 +217,7 @@ class GuiMainMenu(QMenuBar):
|
||||
self.rootItems[itemClass].triggered.connect(
|
||||
lambda nCount, itemClass=itemClass : self._newTreeItem(nwItemType.ROOT, itemClass)
|
||||
)
|
||||
self.rootMenu.addActions(self.rootItems.values())
|
||||
self.rootMenu.addAction(self.rootItems[itemClass])
|
||||
|
||||
# Project > New Folder
|
||||
self.aCreateFolder = QAction("Create Folder", self)
|
||||
@@ -281,6 +243,12 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aDeleteItem.triggered.connect(lambda : self.theParent.treeView.deleteItem(None))
|
||||
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
|
||||
self.projMenu.addSeparator()
|
||||
|
||||
@@ -369,6 +337,18 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aImportFile.triggered.connect(self.theParent.importDocument)
|
||||
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
|
||||
|
||||
def _buildViewMenu(self):
|
||||
@@ -653,7 +633,7 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aReRunSpell = QAction("Re-Run Spell Check", self)
|
||||
self.aReRunSpell.setStatusTip("Run the spell checker on current document")
|
||||
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)
|
||||
|
||||
# Tools > Separator
|
||||
|
||||
+123
-35
@@ -15,19 +15,20 @@ import time
|
||||
import nw
|
||||
|
||||
from os import path
|
||||
from datetime import datetime
|
||||
|
||||
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 (
|
||||
qApp, QMainWindow, QVBoxLayout, QFrame, QSplitter, QFileDialog, QShortcut,
|
||||
QMessageBox, QProgressDialog, QDialog, QTabWidget
|
||||
)
|
||||
|
||||
from nw.gui import (
|
||||
GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor,
|
||||
GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar,
|
||||
GuiDocViewDetails, GuiConfigEditor, GuiProjectEditor, GuiExport,
|
||||
GuiItemEditor, GuiSessionLogView, GuiProjectOutline
|
||||
GuiMainMenu, GuiMainStatus, GuiTheme, GuiDocTree, GuiDocEditor, GuiExport,
|
||||
GuiDocViewer, GuiDocDetails, GuiSearchBar, GuiNoticeBar, GuiDocViewDetails,
|
||||
GuiConfigEditor, GuiProjectEditor, GuiItemEditor, GuiProjectOutline,
|
||||
GuiSessionLogView, GuiDocMerge, GuiDocSplit, GuiProjectLoad
|
||||
)
|
||||
from nw.project import NWProject, NWDoc, NWItem, NWIndex, NWBackup
|
||||
from nw.tools import countWords
|
||||
@@ -49,9 +50,9 @@ class GuiMain(QMainWindow):
|
||||
self.hasProject = False
|
||||
self.isZenMode = False
|
||||
|
||||
logger.info("OS: %s" % (
|
||||
self.mainConf.osType)
|
||||
)
|
||||
logger.info("OS: %s" % self.mainConf.osType)
|
||||
logger.info("Kernel: %s" % self.mainConf.kernelVer)
|
||||
logger.info("Host: %s" % self.mainConf.hostName)
|
||||
logger.info("Qt5 Version: %s (%d)" % (
|
||||
self.mainConf.verQtString, self.mainConf.verQtValue)
|
||||
)
|
||||
@@ -64,7 +65,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
self.resize(*self.mainConf.winGeometry)
|
||||
self._setWindowTitle()
|
||||
self.setWindowIcon(QIcon(path.join(self.mainConf.appIcon)))
|
||||
self.setWindowIcon(QIcon(self.mainConf.appIcon))
|
||||
|
||||
# Main GUI Elements
|
||||
self.statusBar = GuiMainStatus(self)
|
||||
@@ -84,7 +85,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
# Assemble Main Window
|
||||
self.treePane = QFrame()
|
||||
self.treeBox = QVBoxLayout()
|
||||
self.treeBox = QVBoxLayout()
|
||||
self.treeBox.setContentsMargins(0,0,0,0)
|
||||
self.treeBox.addWidget(self.treeView)
|
||||
self.treeBox.addWidget(self.treeMeta)
|
||||
@@ -163,17 +164,12 @@ class GuiMain(QMainWindow):
|
||||
|
||||
# Shortcuts and Actions
|
||||
self._connectMenuActions()
|
||||
QShortcut(
|
||||
Qt.Key_Return,
|
||||
self.treeView,
|
||||
context=Qt.WidgetShortcut,
|
||||
activated=self._treeKeyPressReturn
|
||||
)
|
||||
QShortcut(
|
||||
Qt.Key_Escape,
|
||||
self,
|
||||
activated=self._keyPressEscape
|
||||
)
|
||||
keyReturn = QShortcut(self.treeView)
|
||||
keyReturn.setKey(QKeySequence(Qt.Key_Return))
|
||||
keyReturn.activated.connect(self._treeKeyPressReturn)
|
||||
keyEscape = QShortcut(self)
|
||||
keyEscape.setKey(QKeySequence(Qt.Key_Escape))
|
||||
keyEscape.activated.connect(self._keyPressEscape)
|
||||
|
||||
# Forward Functions
|
||||
self.setStatus = self.statusBar.setStatus
|
||||
@@ -182,6 +178,9 @@ class GuiMain(QMainWindow):
|
||||
if self.mainConf.showGUI:
|
||||
self.show()
|
||||
|
||||
# Check that config loaded fine
|
||||
self.reportConfErr()
|
||||
|
||||
self.initMain()
|
||||
self.asProjTimer.start()
|
||||
self.asDocTimer.start()
|
||||
@@ -193,6 +192,12 @@ class GuiMain(QMainWindow):
|
||||
|
||||
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
|
||||
|
||||
def clearGUI(self):
|
||||
@@ -213,13 +218,30 @@ class GuiMain(QMainWindow):
|
||||
# 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):
|
||||
"""Create new project with a few default files and folders.
|
||||
"""
|
||||
|
||||
if self.hasProject:
|
||||
msgBox = QMessageBox()
|
||||
msgRes = msgBox.warning(
|
||||
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
|
||||
|
||||
@@ -232,7 +254,7 @@ class GuiMain(QMainWindow):
|
||||
msgBox = QMessageBox()
|
||||
msgRes = msgBox.critical(
|
||||
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
|
||||
|
||||
@@ -247,9 +269,9 @@ class GuiMain(QMainWindow):
|
||||
return True
|
||||
|
||||
def closeProject(self, isYes=False):
|
||||
"""Closes the project if one is open.
|
||||
isYes is passed on from the close application event so the user
|
||||
doesn't get prompted twice.
|
||||
"""Closes the project if one is open. isYes is passed on from
|
||||
the close application event so the user doesn't get prompted
|
||||
twice.
|
||||
"""
|
||||
if not self.hasProject:
|
||||
# There is no project loaded, everything OK
|
||||
@@ -309,7 +331,42 @@ class GuiMain(QMainWindow):
|
||||
|
||||
# Try to open the project
|
||||
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
|
||||
self.hasProject = True
|
||||
@@ -323,7 +380,6 @@ class GuiMain(QMainWindow):
|
||||
self.docEditor.setDictionaries()
|
||||
self.docEditor.setSpellCheck(self.theProject.spellCheck)
|
||||
self.statusBar.setRefTime(self.theProject.projOpened)
|
||||
self.mainMenu.updateMenu()
|
||||
|
||||
# Restore previously open documents, if any
|
||||
if self.theProject.lastEdited is not None:
|
||||
@@ -337,7 +393,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return True
|
||||
|
||||
def saveProject(self, isAuto=False):
|
||||
def saveProject(self):
|
||||
"""Save the current project.
|
||||
"""
|
||||
if not self.hasProject:
|
||||
@@ -351,9 +407,8 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
self.treeView.saveTreeOrder()
|
||||
self.theProject.saveProject(isAuto)
|
||||
self.theProject.saveProject()
|
||||
self.theIndex.saveIndex()
|
||||
self.mainMenu.updateRecentProjects()
|
||||
|
||||
return True
|
||||
|
||||
@@ -469,6 +524,22 @@ class GuiMain(QMainWindow):
|
||||
|
||||
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):
|
||||
"""Pass on document action theAction to whatever document has
|
||||
the focus. If no document has focus, the action is discarded.
|
||||
@@ -676,6 +747,16 @@ class GuiMain(QMainWindow):
|
||||
|
||||
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
|
||||
##
|
||||
@@ -691,9 +772,9 @@ class GuiMain(QMainWindow):
|
||||
return False
|
||||
|
||||
logger.info("Exiting %s" % nw.__package__)
|
||||
self.projView.saveHeaderState()
|
||||
|
||||
self.closeProject(True)
|
||||
if self.hasProject:
|
||||
self.projView.saveHeaderState()
|
||||
self.closeProject(True)
|
||||
|
||||
self.mainConf.setTreeColWidths(self.treeView.getColumnSizes())
|
||||
if not self.mainConf.isFullScreen:
|
||||
@@ -702,6 +783,7 @@ class GuiMain(QMainWindow):
|
||||
self.mainConf.setMainPanePos(self.splitMain.sizes())
|
||||
self.mainConf.setDocPanePos(self.splitView.sizes())
|
||||
self.mainConf.saveConfig()
|
||||
self.reportConfErr()
|
||||
|
||||
qApp.quit()
|
||||
|
||||
@@ -803,6 +885,12 @@ class GuiMain(QMainWindow):
|
||||
self.addAction(self.mainMenu.aFmtULine)
|
||||
self.addAction(self.mainMenu.aFmtDQuote)
|
||||
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.aReRunSpell)
|
||||
self.addAction(self.mainMenu.aPreferences)
|
||||
@@ -820,7 +908,7 @@ class GuiMain(QMainWindow):
|
||||
if (self.hasProject and self.theProject.projChanged and
|
||||
self.theProject.projPath is not None):
|
||||
logger.debug("Autosaving project")
|
||||
self.saveProject(isAuto=True)
|
||||
self.saveProject()
|
||||
return
|
||||
|
||||
def _autoSaveDocument(self):
|
||||
|
||||
@@ -57,7 +57,9 @@ class NWBackup():
|
||||
baseName = path.join(self.mainConf.backupPath, archName)
|
||||
|
||||
try:
|
||||
self.theProject._clearLockFile()
|
||||
make_archive(baseName, "zip", self.theProject.projPath, ".")
|
||||
self.theProject._writeLockFile()
|
||||
except Exception as e:
|
||||
self.theParent.makeAlert(
|
||||
["Could not write backup archive.",str(e)],
|
||||
|
||||
+37
-22
@@ -59,7 +59,7 @@ class NWDoc():
|
||||
if self.theItem.parHandle == self.theProject.trashRoot:
|
||||
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)
|
||||
logger.debug("Opening document %s" % self.fileLoc)
|
||||
dataDir = path.join(self.theProject.projPath, docDir)
|
||||
@@ -92,7 +92,7 @@ class NWDoc():
|
||||
if self.docHandle is None or not self.docEditable:
|
||||
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))
|
||||
dataPath = path.join(self.theProject.projPath, docDir)
|
||||
docPath = path.join(dataPath, docFile)
|
||||
@@ -100,39 +100,54 @@ class NWDoc():
|
||||
mkdir(dataPath)
|
||||
logger.debug("Created folder %s" % dataPath)
|
||||
|
||||
docTemp = path.join(dataPath,docFile[:-3]+"tmp")
|
||||
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)
|
||||
docTemp = path.join(dataPath, docFile+"~")
|
||||
docBack = path.join(dataPath, docFile[:-3]+"bak")
|
||||
|
||||
try:
|
||||
with open(docPath,mode="w",encoding="utf8") as outFile:
|
||||
with open(docTemp,mode="w",encoding="utf8") as outFile:
|
||||
outFile.write(docText)
|
||||
except Exception as e:
|
||||
self.makeAlert(["Could not save document.",str(e)], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
if path.isfile(docTemp):
|
||||
unlink(docTemp)
|
||||
# If we're here, the file was successfully saved,
|
||||
# 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)
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
def deleteDocument(self, tHandle):
|
||||
"""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):
|
||||
if self.docHandle is None:
|
||||
return None
|
||||
docDir = "data_"+self.docHandle[0]
|
||||
docFile = self.docHandle[1:13]+"_"+docExt
|
||||
@staticmethod
|
||||
def assemblePath(tHandle, docExt):
|
||||
if tHandle is None:
|
||||
return None, None
|
||||
docDir = "data_"+tHandle[0]
|
||||
docFile = tHandle[1:13]+"_"+docExt
|
||||
return docDir, docFile
|
||||
|
||||
# END Class NWDoc
|
||||
|
||||
@@ -14,7 +14,6 @@ import logging
|
||||
import nw
|
||||
|
||||
from lxml import etree
|
||||
from datetime import datetime
|
||||
|
||||
from nw.common import checkInt
|
||||
from nw.constants import nwItemType, nwItemClass, nwItemLayout
|
||||
@@ -23,8 +22,6 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class NWItem():
|
||||
|
||||
MAX_DEPTH = 8
|
||||
|
||||
def __init__(self, theProject):
|
||||
|
||||
self.theProject = theProject
|
||||
|
||||
+170
-72
@@ -13,8 +13,7 @@
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from os import path, mkdir, listdir
|
||||
from shutil import copyfile
|
||||
from os import path, mkdir, listdir, unlink, rename
|
||||
from lxml import etree
|
||||
from hashlib import sha256
|
||||
from datetime import datetime
|
||||
@@ -22,6 +21,7 @@ from time import time
|
||||
|
||||
from nw.project.status import NWStatus
|
||||
from nw.project.item import NWItem
|
||||
from nw.tools import projectMaintenance, OptionState
|
||||
from nw.common import checkString, checkBool, checkInt
|
||||
from nw.constants import (
|
||||
nwFiles, nwConst, nwItemType, nwItemClass, nwItemLayout, nwAlert
|
||||
@@ -36,9 +36,11 @@ class NWProject():
|
||||
# Internal
|
||||
self.theParent = theParent
|
||||
self.mainConf = self.theParent.mainConf
|
||||
self.optState = OptionState(self)
|
||||
self.projOpened = None # The time stamp of when the project file was opened
|
||||
self.projChanged = None # The project has unsaved changes
|
||||
self.projAltered = None # The project has been altered this session
|
||||
self.lockedBy = None # Data on which computer has the project open
|
||||
|
||||
# Debug
|
||||
self.handleSeed = None
|
||||
@@ -50,7 +52,6 @@ class NWProject():
|
||||
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.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.projFile = None # The file name of the project main xml file
|
||||
|
||||
@@ -154,7 +155,6 @@ class NWProject():
|
||||
self.trashRoot = None
|
||||
self.projPath = None
|
||||
self.projMeta = None
|
||||
self.projCache = None
|
||||
self.projDict = None
|
||||
self.projFile = nwFiles.PROJ_FILE
|
||||
self.projName = ""
|
||||
@@ -179,7 +179,12 @@ class NWProject():
|
||||
|
||||
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):
|
||||
fileName = path.join(fileName, nwFiles.PROJ_FILE)
|
||||
@@ -188,24 +193,53 @@ class NWProject():
|
||||
return False
|
||||
|
||||
self.clearProject()
|
||||
self.projPath = path.dirname(fileName)
|
||||
self.projPath = path.abspath(path.dirname(fileName))
|
||||
logger.debug("Opening project: %s" % self.projPath)
|
||||
|
||||
self.projMeta = path.join(self.projPath,"meta")
|
||||
self.projCache = path.join(self.projPath,"cache")
|
||||
self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT)
|
||||
self.projMeta = path.join(self.projPath,"meta")
|
||||
self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT)
|
||||
|
||||
if not self._checkFolder(self.projMeta):
|
||||
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:
|
||||
nwXML = etree.parse(fileName)
|
||||
except Exception as e:
|
||||
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()
|
||||
nwxRoot = xRoot.tag
|
||||
@@ -278,41 +312,47 @@ class NWProject():
|
||||
nwItem.setFromTag(xValue.tag,xValue.text)
|
||||
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._scanProjectFolder()
|
||||
self.setProjectChanged(False)
|
||||
self.projOpened = time()
|
||||
self.projAltered = False
|
||||
self._writeLockFile()
|
||||
|
||||
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:
|
||||
self.makeAlert("Project path not set, cannot save.", nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
self.projMeta = path.join(self.projPath,"meta")
|
||||
self.projCache = path.join(self.projPath,"cache")
|
||||
self.projMeta = path.join(self.projPath,"meta")
|
||||
saveTime = time()
|
||||
|
||||
if not self._checkFolder(self.projPath): return
|
||||
if not self._checkFolder(self.projMeta): return
|
||||
if not self._checkFolder(self.projCache): return
|
||||
if not self._checkFolder(self.projPath): return
|
||||
if not self._checkFolder(self.projMeta): return
|
||||
|
||||
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
|
||||
logger.debug("Writing project meta")
|
||||
nwXML = etree.Element("novelWriterXML",attrib={
|
||||
"appVersion" : str(nw.__version__),
|
||||
"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
|
||||
@@ -345,9 +385,11 @@ class NWProject():
|
||||
self.projTree[tHandle].packXML(xContent)
|
||||
|
||||
# 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:
|
||||
with open(saveFile,mode="wb") as outFile:
|
||||
with open(tempFile, mode="wb") as outFile:
|
||||
outFile.write(etree.tostring(
|
||||
nwXML,
|
||||
pretty_print = True,
|
||||
@@ -358,15 +400,34 @@ class NWProject():
|
||||
self.makeAlert(["Failed to save project.",str(e)], nwAlert.ERROR)
|
||||
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.setProjectChanged(False)
|
||||
|
||||
return True
|
||||
|
||||
def closeProject(self):
|
||||
"""Close the current project and clear all meta data.
|
||||
"""
|
||||
self._appendSessionStats()
|
||||
self._clearLockFile()
|
||||
self.clearProject()
|
||||
self.lockedBy = None
|
||||
return True
|
||||
|
||||
##
|
||||
@@ -452,9 +513,6 @@ class NWProject():
|
||||
self.setProjectChanged(True)
|
||||
return True
|
||||
|
||||
def getSessionWordCount(self):
|
||||
return self.currWCount - self.lastWCount
|
||||
|
||||
def setStatusColours(self, newCols):
|
||||
replaceMap = self.statusItems.setNewEntries(newCols)
|
||||
if self.projTree is not None:
|
||||
@@ -497,6 +555,9 @@ class NWProject():
|
||||
logger.error("No tree item with handle %s" % str(tHandle))
|
||||
return None
|
||||
|
||||
def getSessionWordCount(self):
|
||||
return self.currWCount - self.lastWCount
|
||||
|
||||
def getRootItem(self, tHandle):
|
||||
"""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
|
||||
@@ -505,10 +566,13 @@ class NWProject():
|
||||
tItem = self.getItem(tHandle)
|
||||
if tItem is not None:
|
||||
for i in range(200):
|
||||
tHandle = tItem.parHandle
|
||||
tItem = self.getItem(tHandle)
|
||||
if tItem is None:
|
||||
if tItem.parHandle is None:
|
||||
return tHandle
|
||||
else:
|
||||
tHandle = tItem.parHandle
|
||||
tItem = self.getItem(tHandle)
|
||||
if tItem is None:
|
||||
return tHandle
|
||||
return None
|
||||
|
||||
def getProjectItems(self):
|
||||
@@ -560,6 +624,11 @@ class NWProject():
|
||||
"""This only removes the item from the order list, but not from
|
||||
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.setProjectChanged(True)
|
||||
return True
|
||||
@@ -598,6 +667,73 @@ class NWProject():
|
||||
# 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):
|
||||
if not path.isdir(thePath):
|
||||
try:
|
||||
@@ -732,42 +868,4 @@ class NWProject():
|
||||
itemHandle = self._makeHandle(addSeed+"!")
|
||||
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
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
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.spellenchant import NWSpellEnchant
|
||||
from nw.tools.spellsimple import NWSpellSimple
|
||||
@@ -10,7 +11,8 @@ from nw.tools.wordcount import countWords
|
||||
|
||||
__all__ = [
|
||||
"TextAnalysis",
|
||||
"OptLastState",
|
||||
"projectMaintenance",
|
||||
"OptionState",
|
||||
"NWSpellCheck",
|
||||
"NWSpellEnchant",
|
||||
"NWSpellSimple",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -4,3 +4,4 @@ markers =
|
||||
core: Core functionality tests
|
||||
gui: Qt5 GUI tests
|
||||
serial
|
||||
qt_api = pyqt5
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pyqt5
|
||||
appdirs
|
||||
lxml
|
||||
pyenchant
|
||||
latexcodec
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?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>
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
@@ -33,7 +33,7 @@
|
||||
<entry blue="175" green="0" red="117">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="21">
|
||||
<content count="20">
|
||||
<item handle="7031beac91f75" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
@@ -82,7 +82,7 @@
|
||||
<charCount>1199</charCount>
|
||||
<wordCount>216</wordCount>
|
||||
<paraCount>7</paraCount>
|
||||
<cursorPos>949</cursorPos>
|
||||
<cursorPos>19</cursorPos>
|
||||
</item>
|
||||
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
|
||||
<name>Another Scene</name>
|
||||
|
||||
@@ -6,7 +6,7 @@ with open("README.md", "r") as inFile:
|
||||
|
||||
setuptools.setup(
|
||||
name = "novelWriter",
|
||||
version = "0.4.2",
|
||||
version = "0.4.5",
|
||||
author = "Veronica Berglyd Olsen",
|
||||
author_email = "code@vkbo.net",
|
||||
description = "A markdown-like document editor for writing novels",
|
||||
@@ -35,10 +35,9 @@ setuptools.setup(
|
||||
"Natural Language :: English",
|
||||
"Topic :: Text Editors",
|
||||
],
|
||||
python_requires = ">=3.5",
|
||||
python_requires = ">=3.6",
|
||||
install_requires = [
|
||||
"pyqt5",
|
||||
"appdirs",
|
||||
"lxml",
|
||||
"pyenchant",
|
||||
"latexcodec",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[Main]
|
||||
timestamp = 2019-11-19 21:49:29
|
||||
timestamp = 2020-02-26 18:10:35
|
||||
theme = default
|
||||
syntax = default_light
|
||||
guidark = False
|
||||
@@ -49,14 +49,4 @@ viewcomments = True
|
||||
|
||||
[Path]
|
||||
lastpath =
|
||||
recent0 =
|
||||
recent1 =
|
||||
recent2 =
|
||||
recent3 =
|
||||
recent4 =
|
||||
recent5 =
|
||||
recent6 =
|
||||
recent7 =
|
||||
recent8 =
|
||||
recent9 =
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?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>
|
||||
<name></name>
|
||||
<title></title>
|
||||
@@ -72,28 +72,28 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="39fa9ec190eee" order="None" parent="None">
|
||||
<item handle="8722616204217" order="None" parent="None">
|
||||
<name>Timeline</name>
|
||||
<type>ROOT</type>
|
||||
<class>TIMELINE</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="d029fa3a95e17" order="None" parent="None">
|
||||
<item handle="96061e92f58e4" order="None" parent="None">
|
||||
<name>Object</name>
|
||||
<type>ROOT</type>
|
||||
<class>OBJECT</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="81b8a03f97e87" order="None" parent="None">
|
||||
<item handle="eb624dbe56eb6" order="None" parent="None">
|
||||
<name>Custom1</name>
|
||||
<type>ROOT</type>
|
||||
<class>CUSTOM</class>
|
||||
<status>New</status>
|
||||
<expanded>False</expanded>
|
||||
</item>
|
||||
<item handle="da4ea2a5506f2" order="None" parent="None">
|
||||
<item handle="f369cb89fc627" order="None" parent="None">
|
||||
<name>Custom2</name>
|
||||
<type>ROOT</type>
|
||||
<class>CUSTOM</class>
|
||||
|
||||
+38
-2
@@ -13,7 +13,7 @@ theConf = Config()
|
||||
def testConfigInit(nwTemp,nwRef):
|
||||
tmpConf = path.join(nwTemp,"novelwriter.conf")
|
||||
refConf = path.join(nwRef, "novelwriter.conf")
|
||||
assert theConf.initConfig(nwTemp)
|
||||
assert theConf.initConfig(nwTemp, nwTemp)
|
||||
assert theConf.setLastPath("")
|
||||
assert theConf.saveConfig()
|
||||
assert cmpFiles(tmpConf, refConf, [2])
|
||||
@@ -37,6 +37,14 @@ def testConfigSetConfPath(nwTemp):
|
||||
assert theConf.confFile == "novelwriter.conf"
|
||||
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
|
||||
def testConfigLoad():
|
||||
assert theConf.loadConfig()
|
||||
@@ -67,7 +75,7 @@ def testConfigSetTreeColWidths(nwTemp,nwRef):
|
||||
assert not theConf.confChanged
|
||||
|
||||
@pytest.mark.core
|
||||
def testConfigSetMainPanePos(nwTemp,nwRef):
|
||||
def testConfigSetPanePos(nwTemp,nwRef):
|
||||
tmpConf = path.join(nwTemp,"novelwriter.conf")
|
||||
refConf = path.join(nwRef, "novelwriter.conf")
|
||||
assert theConf.setMainPanePos([0, 0])
|
||||
@@ -77,3 +85,31 @@ def testConfigSetMainPanePos(nwTemp,nwRef):
|
||||
assert theConf.saveConfig()
|
||||
assert cmpFiles(tmpConf, refConf, [2])
|
||||
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
@@ -17,8 +17,8 @@ keyDelay = 10
|
||||
stepDelay = 50
|
||||
|
||||
@pytest.mark.gui
|
||||
def testMainWindows(qtbot, nwTempGUI, nwRef):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI])
|
||||
def testMainWindows(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -36,7 +36,6 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
|
||||
assert nwGUI.theProject.trashRoot is None
|
||||
assert nwGUI.theProject.projPath is None
|
||||
assert nwGUI.theProject.projMeta is None
|
||||
assert nwGUI.theProject.projCache is None
|
||||
assert nwGUI.theProject.projFile == "nwProject.nwx"
|
||||
assert nwGUI.theProject.projName == ""
|
||||
assert nwGUI.theProject.bookTitle == ""
|
||||
@@ -61,7 +60,6 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
|
||||
assert nwGUI.theProject.trashRoot is None
|
||||
assert nwGUI.theProject.projPath == nwTempGUI
|
||||
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.projName == ""
|
||||
assert nwGUI.theProject.bookTitle == ""
|
||||
@@ -252,8 +250,8 @@ def testMainWindows(qtbot, nwTempGUI, nwRef):
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testProjectEditor(qtbot, nwTempGUI, nwRef):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI])
|
||||
def testProjectEditor(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
@@ -339,8 +337,8 @@ def testProjectEditor(qtbot, nwTempGUI, nwRef):
|
||||
# qtbot.stopForInteraction()
|
||||
|
||||
@pytest.mark.gui
|
||||
def testItemEditor(qtbot, nwTempGUI, nwRef):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI])
|
||||
def testItemEditor(qtbot, nwTempGUI, nwRef, nwTemp):
|
||||
nwGUI = nw.main(["--testmode","--config=%s" % nwTempGUI, "--data=%s" % nwTemp])
|
||||
qtbot.addWidget(nwGUI)
|
||||
nwGUI.show()
|
||||
qtbot.waitForWindowShown(nwGUI)
|
||||
|
||||
+18
-2
@@ -22,13 +22,14 @@ theProject = NWProject(theMain)
|
||||
theProject.handleSeed = 42
|
||||
|
||||
@pytest.mark.project
|
||||
def testProjectNew(nwTempProj,nwRef):
|
||||
def testProjectNew(nwTempProj,nwRef,nwTemp):
|
||||
projFile = path.join(nwTempProj,"nwProject.nwx")
|
||||
refFile = path.join(nwRef,"proj","1_nwProject.nwx")
|
||||
assert theConf.initConfig(nwRef)
|
||||
assert theConf.initConfig(nwRef, nwTemp)
|
||||
assert theProject.newProject()
|
||||
assert theProject.setProjectPath(nwTempProj)
|
||||
assert theProject.saveProject()
|
||||
assert theProject.closeProject()
|
||||
assert cmpFiles(projFile, refFile, [2])
|
||||
|
||||
@pytest.mark.project
|
||||
@@ -41,9 +42,21 @@ def testProjectSave(nwTempProj,nwRef):
|
||||
projFile = path.join(nwTempProj,"nwProject.nwx")
|
||||
refFile = path.join(nwRef,"proj","1_nwProject.nwx")
|
||||
assert theProject.saveProject()
|
||||
assert theProject.closeProject()
|
||||
assert cmpFiles(projFile, refFile, [2])
|
||||
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
|
||||
def testProjectNewRoot(nwTempProj,nwRef):
|
||||
projFile = path.join(nwTempProj,"nwProject.nwx")
|
||||
@@ -59,6 +72,7 @@ def testProjectNewRoot(nwTempProj,nwRef):
|
||||
assert isinstance(theProject.newRoot("Custom2", nwItemClass.CUSTOM), str)
|
||||
assert theProject.projChanged
|
||||
assert theProject.saveProject()
|
||||
assert theProject.closeProject()
|
||||
assert cmpFiles(projFile, refFile, [2])
|
||||
assert not theProject.projChanged
|
||||
|
||||
@@ -95,3 +109,5 @@ def testIndexScanThis(nwTempProj):
|
||||
assert isValid
|
||||
assert str(theBits) == "['@tag', 'this', 'and this']"
|
||||
assert str(thePos) == "[0, 6, 12]"
|
||||
|
||||
assert theProject.closeProject()
|
||||
|
||||
Reference in New Issue
Block a user