Fix merge conflicts

This commit is contained in:
Veronica K. B. Olsen
2020-02-27 00:19:18 +01:00
52 changed files with 1835 additions and 584 deletions
+4 -3
View File
@@ -1,10 +1,9 @@
os: linux
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
+41
View File
@@ -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**
+1 -3
View File
@@ -70,14 +70,13 @@ For the apt package manager on Debian systems, the following Python3 packages ar
* `python3-pyqt5` for the GUI
* `python3-pyqt5.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
View File
@@ -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 ---------------------------------------------------
+3 -3
View File
@@ -37,9 +37,9 @@ The editor also has a minimal set of keywords used for setting tags and referenc
"``## Title``", "Heading level two. The space after the # is mandatory."
"``### Title``", "Heading level 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."
+2 -2
View File
@@ -22,7 +22,7 @@ These other root folder types are intended for your notes on the various element
Using these is of course entirely optional.
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
---------------------------------
+2 -3
View File
@@ -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::
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -23,10 +23,10 @@ from nw.config import Config
__package__ = "novelWriter"
__author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 20182019, Veronica Berglyd Olsen"
__copyright__ = "Copyright 20182020, 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
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg4"
viewBox="0 0 20 20"
height="20"
width="20"
version="1.2">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<path
style="fill:#000000;fill-opacity:0.35294118;stroke:none;stroke-opacity:1"
id="path2"
d="m 16,5.816 v 1.851 c 0,1.93 -1.57,3.5 -3.5,3.5 -0.827,0 -1.5,0.673 -1.5,1.5 v 1.517 c 1.161,0.415 2,1.514 2,2.816 0,1.654 -1.346,3 -3,3 C 8.346,20 7,18.654 7,17 7,15.698 7.839,14.599 9,14.185 V 12.667 C 9,11.84 8.327,11.167 7.5,11.167 5.57,11.167 4,9.597 4,7.667 V 5.816 C 2.839,5.401 2,4.302 2,3 2,1.346 3.346,0 5,0 6.654,0 8,1.346 8,3 8,4.302 7.161,5.401 6,5.816 v 1.851 c 0,0.827 0.673,1.5 1.5,1.5 0.979,0 1.864,0.407 2.5,1.058 0.636,-0.651 1.521,-1.058 2.5,-1.058 0.827,0 1.5,-0.673 1.5,-1.5 V 5.816 C 12.839,5.401 12,4.302 12,3 c 0,-1.654 1.346,-3 3,-3 1.654,0 3,1.346 3,3 0,1.302 -0.839,2.401 -2,2.816 z M 5,2 C 4.448,2 4,2.449 4,3 4,3.551 4.448,4 5,4 5.552,4 6,3.551 6,3 6,2.449 5.552,2 5,2 Z m 5,16 c 0.552,0 1,-0.449 1,-1 0,-0.551 -0.448,-1 -1,-1 -0.552,0 -1,0.449 -1,1 0,0.551 0.448,1 1,1 z M 15,2 c -0.552,0 -1,0.449 -1,1 0,0.551 0.448,1 1,1 0.552,0 1,-0.449 1,-1 0,-0.551 -0.448,-1 -1,-1 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

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

After

Width:  |  Height:  |  Size: 1.7 KiB

+19
View File
@@ -88,6 +88,25 @@ def colRange(rgbStart, rgbEnd, nStep):
return retCol
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
View File
@@ -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
+3 -8
View File
@@ -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" : "",
+6
View File
@@ -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",
+6
View File
@@ -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",
]
+157
View File
@@ -0,0 +1,157 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Doc Merge
novelWriter GUI Doc Merge
=============================
Tool for merging multiple documents to one
File History:
Created: 2020-01-23 [0.4.3]
"""
import logging
import nw
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton,
QListWidget, QAbstractItemView, QListWidgetItem
)
from nw.constants import nwAlert, nwItemType
from nw.project import NWDoc
logger = logging.getLogger(__name__)
class GuiDocMerge(QDialog):
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiDocMerge ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
self.sourceItem = None
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Merge Documents")
self.setLayout(self.outerBox)
self.guiDeco = self.theParent.theTheme.loadDecoration("merge",(64,64))
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.doMergeForm = QGridLayout()
self.doMergeForm.setContentsMargins(0,0,0,0)
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
self.mergeButton = QPushButton("Merge")
self.mergeButton.clicked.connect(self._doMerge)
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
self.doMergeForm.addWidget(self.listBox, 0, 0, 1, 3)
self.doMergeForm.addWidget(self.mergeButton, 1, 1)
self.doMergeForm.addWidget(self.closeButton, 1, 2)
self.innerBox.addLayout(self.doMergeForm)
self.rejected.connect(self._doClose)
self.show()
self._populateList()
logger.debug("GuiDocMerge initialisation complete")
return
##
# Buttons
##
def _doMerge(self):
"""Perform the merge of the files in the selected folder, and
create a new file in the same parent folder. The old files are
not removed in the merge process, and must be deleted manually.
"""
logger.verbose("GuiDocMerge merge button clicked")
finalOrder = []
for i in range(self.listBox.count()):
finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
theDoc = NWDoc(self.theProject, self.theParent)
theText = ""
for tHandle in finalOrder:
theText += theDoc.openDocument(tHandle, False).rstrip()
theText += "\n\n"
if self.sourceItem is None:
self.theParent.makeAlert((
"Cannot parse source item."
), nwAlert.ERROR)
return
srcItem = self.theProject.getItem(self.sourceItem)
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.parHandle)
self.theParent.treeView.revealTreeItem(nHandle)
theDoc.openDocument(nHandle, False)
theDoc.saveDocument(theText)
self.theParent.openDocument(nHandle)
self.close()
return
def _doClose(self):
"""Close the dialog window without doing anything.
"""
logger.verbose("GuiDocMerge close button clicked")
self.close()
return
##
# Internal Functions
##
def _populateList(self):
"""Get the item selected in the tree, check that it is a folder,
and try to find all files associated with it. The valid files
are then added to the list view in order. The list itself can be
reordered by the user.
"""
tHandle = self.theParent.treeView.getSelectedHandle()
self.sourceItem = tHandle
if tHandle is None:
return
nwItem = self.theProject.getItem(tHandle)
if nwItem is None:
return
if nwItem.itemType is not nwItemType.FOLDER:
self.theParent.makeAlert((
"Element selected in the project tree must be a folder."
), nwAlert.ERROR)
return
for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle):
newItem = QListWidgetItem()
nwItem = self.theProject.getItem(sHandle)
if nwItem.itemType is not nwItemType.FILE:
continue
newItem.setText(nwItem.itemName)
newItem.setData(Qt.UserRole, sHandle)
self.listBox.addItem(newItem)
return
# END Class GuiDocMerge
+241
View File
@@ -0,0 +1,241 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Doc Split
novelWriter GUI Doc Split
=============================
Tool for splitting a single document into multiple documents
File History:
Created: 2020-02-01 [0.4.3]
"""
import logging
import nw
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QComboBox,
QListWidget, QAbstractItemView, QListWidgetItem
)
from nw.constants import nwAlert, nwItemType, nwItemClass, nwItemLayout
from nw.project import NWDoc
logger = logging.getLogger(__name__)
class GuiDocSplit(QDialog):
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiDocSplit ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
self.optState = self.theProject.optState
self.sourceItem = None
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Split Document")
self.setLayout(self.outerBox)
self.guiDeco = self.theParent.theTheme.loadDecoration("split",(64,64))
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.doMergeForm = QGridLayout()
self.doMergeForm.setContentsMargins(0,0,0,0)
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
self.splitLevel = QComboBox(self)
self.splitLevel.addItem("Split on Header Level 1 (Title)", 1)
self.splitLevel.addItem("Split up to Header Level 2 (Chapter)", 2)
self.splitLevel.addItem("Split up to Header Level 3 (Scene)", 3)
self.splitLevel.addItem("Split up to Header Level 4 (Section)", 4)
spIndex = self.splitLevel.findData(
self.optState.getInt("GuiDocSplit", "spLevel", 3)
)
if spIndex != -1:
self.splitLevel.setCurrentIndex(spIndex)
self.splitLevel.currentIndexChanged.connect(self._populateList)
self.splitButton = QPushButton("Split")
self.splitButton.clicked.connect(self._doSplit)
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
self.doMergeForm.addWidget(self.listBox, 0, 0, 1, 3)
self.doMergeForm.addWidget(self.splitLevel, 1, 0, 1, 3)
self.doMergeForm.addWidget(self.splitButton, 2, 1)
self.doMergeForm.addWidget(self.closeButton, 2, 2)
self.innerBox.addLayout(self.doMergeForm)
self.rejected.connect(self._doClose)
self.show()
self._populateList()
logger.debug("GuiDocSplit initialisation complete")
return
##
# Buttons
##
def _doSplit(self):
"""Perform the split of the file, create a new folder in the
same parent folder, and multiple files depending on split level
settings. The old file is not removed in the merge process, and
must be deleted manually.
"""
logger.verbose("GuiDocSplit split button clicked")
if self.sourceItem is None:
self.theParent.makeAlert((
"No source document selected. Nothing to do."
), nwAlert.ERROR)
return
srcItem = self.theProject.getItem(self.sourceItem)
if srcItem is None:
self.theParent.makeAlert((
"Could not parse source document."
), nwAlert.ERROR)
return
theDoc = NWDoc(self.theProject, self.theParent)
theText = theDoc.openDocument(self.sourceItem, False)
theLines = theText.splitlines()
nLines = len(theLines)
theLines.insert(0, "%Split Doc")
logger.debug(
"Splitting document %s with %d lines" % (self.sourceItem,nLines)
)
finalOrder = []
for i in range(self.listBox.count()):
listItem = self.listBox.item(i)
wTitle = listItem.text()
lineNo = listItem.data(Qt.UserRole)
finalOrder.append([wTitle, lineNo, nLines])
if i > 0:
finalOrder[i-1][2] = lineNo
if len(finalOrder) == 0:
self.theParent.makeAlert((
"No headers found. Nothing to do."
), nwAlert.ERROR)
return
fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemClass, srcItem.parHandle)
self.theParent.treeView.revealTreeItem(fHandle)
logger.verbose("Creating folder %s" % fHandle)
for wTitle, iStart, iEnd in finalOrder:
itemLayout = nwItemLayout.NOTE
if srcItem.itemClass == nwItemClass.NOVEL:
if wTitle.startswith("# "):
itemLayout = nwItemLayout.PARTITION
elif wTitle.startswith("## "):
itemLayout = nwItemLayout.CHAPTER
elif wTitle.startswith("### "):
itemLayout = nwItemLayout.SCENE
elif wTitle.startswith("#### "):
itemLayout = nwItemLayout.PAGE
wTitle = wTitle.lstrip("#")
wTitle = wTitle.strip()
nHandle = self.theProject.newFile(wTitle, srcItem.itemClass, fHandle)
newItem = self.theProject.getItem(nHandle)
newItem.setLayout(itemLayout)
logger.verbose(
"Creating new document %s with text from line %d to %d" % (nHandle, iStart, iEnd-1)
)
theText = "\n".join(theLines[iStart:iEnd])
theDoc.openDocument(nHandle, False)
theDoc.saveDocument(theText)
theDoc.clearDocument()
self.theParent.treeView.revealTreeItem(nHandle)
self.close()
return
def _doClose(self):
"""Close the dialog window without doing anything.
"""
logger.verbose("GuiDocSplit close button clicked")
self.optState.saveSettings()
self.close()
return
##
# Internal Functions
##
def _populateList(self):
"""Get the item selected in the tree, check that it is a folder,
and try to find all files associated with it. The valid files
are then added to the list view in order. The list itself can be
reordered by the user.
"""
if self.sourceItem is None:
self.sourceItem = self.theParent.treeView.getSelectedHandle()
if self.sourceItem is None:
return
nwItem = self.theProject.getItem(self.sourceItem)
if nwItem is None:
return
if nwItem.itemType is not nwItemType.FILE:
self.theParent.makeAlert((
"Element selected in the project tree must be a file."
), nwAlert.ERROR)
return
self.listBox.clear()
theDoc = NWDoc(self.theProject, self.theParent)
theText = theDoc.openDocument(self.sourceItem, False)
spLevel = self.splitLevel.currentData()
self.optState.setValue("GuiDocSplit", "spLevel", spLevel)
logger.debug("Scanning document %s for headings level <= %d" % (self.sourceItem, spLevel))
lineNo = 0
for aLine in theText.splitlines():
lineNo += 1
onLine = 0
if aLine.startswith("# ") and spLevel >= 1:
onLine = lineNo
elif aLine.startswith("## ") and spLevel >= 2:
onLine = lineNo
elif aLine.startswith("### ") and spLevel >= 3:
onLine = lineNo
elif aLine.startswith("#### ") and spLevel >= 4:
onLine = lineNo
if onLine > 0:
newItem = QListWidgetItem()
newItem.setText(aLine.strip())
newItem.setData(Qt.UserRole, onLine)
self.listBox.addItem(newItem)
return
# END Class GuiDocSplit
+63 -65
View File
@@ -24,7 +24,6 @@ from PyQt5.QtWidgets import (
)
from nw.convert import TextFile, HtmlFile, MarkdownFile, LaTeXFile, ConcatFile
from nw.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
-2
View File
@@ -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,
-2
View File
@@ -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 (
+169
View File
@@ -0,0 +1,169 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Open Project
novelWriter GUI Open Project
================================
New and open project dialog
File History:
Created: 2020-02-26 [0.4.5]
"""
import logging
import nw
from datetime import datetime
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QGridLayout, QPushButton, QTreeWidget,
QAbstractItemView, QTreeWidgetItem
)
from nw.common import formatInt
logger = logging.getLogger(__name__)
class GuiProjectLoad(QDialog):
def __init__(self, theParent):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiProjectLoad ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.sourceItem = None
self.openPath = None
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Open Project")
self.setLayout(self.outerBox)
self.guiDeco = self.theParent.theTheme.loadDecoration("nwicon", (128, 128))
self.outerBox.addWidget(self.guiDeco, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.projectForm = QGridLayout()
self.projectForm.setContentsMargins(0, 0, 0, 0)
self.listBox = QTreeWidget()
self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
self.listBox.setColumnCount(4)
self.listBox.setHeaderLabels(["Working Title","Words","Accessed","Path"])
self.listBox.setRootIsDecorated(False)
treeHead = self.listBox.headerItem()
treeHead.setTextAlignment(1, Qt.AlignRight)
self.recentButton = QPushButton("Open")
self.recentButton.clicked.connect(self._doOpenRecent)
self.browseButton = QPushButton("Browse")
self.browseButton.clicked.connect(self._doBrowse)
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
self.projectForm.addWidget(self.listBox, 0, 0, 1, 4)
self.projectForm.addWidget(self.recentButton, 1, 1)
self.projectForm.addWidget(self.browseButton, 1, 2)
self.projectForm.addWidget(self.closeButton, 1, 3)
self.projectForm.setColumnStretch(0, 1)
self.innerBox.addLayout(self.projectForm)
self.rejected.connect(self._doClose)
self.setModal(True)
self.setMinimumWidth(750)
self.setMinimumHeight(450)
self.show()
self._populateList()
logger.debug("GuiProjectLoad initialisation complete")
return
##
# Buttons
##
def _doOpenRecent(self):
"""Close the dialog window with a recent project selected.
"""
logger.verbose("GuiProjectLoad open button clicked")
selItems = self.listBox.selectedItems()
if selItems:
self.openPath = selItems[0].text(3)
self.accept()
else:
self.openPath = None
return
def _doBrowse(self):
"""Close the dialog window with no selected path, triggering the
project browser dialog.
"""
logger.verbose("GuiProjectLoad browse button clicked")
self.openPath = None
self.accept()
return
def _doClose(self):
"""Close the dialog window without doing anything.
"""
logger.verbose("GuiProjectLoad close button clicked")
self.close()
return
##
# Internal Functions
##
def _populateList(self):
"""Populate the list box with recent project data.
"""
listOrder = []
listData = {}
for projPath in self.mainConf.recentProj.keys():
theEntry = self.mainConf.recentProj[projPath]
theTitle = ""
theTime = 0
theWords = 0
if "title" in theEntry.keys():
theTitle = theEntry["title"]
if "time" in theEntry.keys():
theTime = theEntry["time"]
if "words" in theEntry.keys():
theWords = theEntry["words"]
if theTime > 0:
listOrder.append(theTime)
listData[theTime] = [theTitle, theWords, projPath]
self.listBox.clear()
hasSelection = False
for timeStamp in sorted(listOrder, reverse=True):
newItem = QTreeWidgetItem([""]*4)
newItem.setText(0, listData[timeStamp][0])
newItem.setText(1, formatInt(listData[timeStamp][1]))
newItem.setText(2, datetime.fromtimestamp(timeStamp).strftime("%x %X"))
newItem.setText(3, listData[timeStamp][2])
newItem.setTextAlignment(1, Qt.AlignRight)
self.listBox.addTopLevelItem(newItem)
if not hasSelection:
newItem.setSelected(True)
hasSelection = True
self.listBox.resizeColumnToContents(0)
self.listBox.resizeColumnToContents(1)
self.listBox.resizeColumnToContents(2)
return
# END Class GuiProjectLoad
+20 -37
View File
@@ -24,7 +24,6 @@ from PyQt5.QtWidgets import (
)
from nw.constants import nwConst, nwFiles, nwAlert
from nw.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
-5
View File
@@ -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)
+23 -14
View File
@@ -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
View File
@@ -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
+4 -17
View File
@@ -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
+2 -2
View File
@@ -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
-2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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):
+2
View File
@@ -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
View File
@@ -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
-3
View File
@@ -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
View File
@@ -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
+4 -2
View File
@@ -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",
+63
View File
@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
"""novelWriter Legacy Tools
novelWriter Legacy Tools
============================
Various functions to handle old projects
File History:
Created: 2020-02-13 [0.4.3]
"""
import logging
import nw
from os import path, unlink, rmdir
logger = logging.getLogger(__name__)
def projectMaintenance(theProject):
"""Wrapper class for handling various tasks related to managing old
projects with content from older versions of novelWriter.
"""
# Remove no longer used project cache folder
if path.isdir(theProject.projPath):
cacheDir = path.join(theProject.projPath, "cache")
if path.isdir(cacheDir):
logger.info("Deprecated cache folder found")
rmList = []
for i in range(10):
rmList.append(path.join(cacheDir, "nwProject.nwx.%d" % i))
rmList.append(path.join(cacheDir, "projCount.txt"))
for rmFile in rmList:
if path.isfile(rmFile):
logger.info("Deleting: %s" % rmFile)
try:
unlink(rmFile)
except Exception as e:
logger.error(str(e))
logger.info("Deleting: %s" % cacheDir)
try:
rmdir(cacheDir)
except Exception as e:
logger.error(str(e))
# Remove no longer used meta files
rmList = []
rmList.append(path.join(theProject.projMeta, "mainOptions.json"))
rmList.append(path.join(theProject.projMeta, "exportOptions.json"))
rmList.append(path.join(theProject.projMeta, "outlineOptions.json"))
rmList.append(path.join(theProject.projMeta, "timelineOptions.json"))
rmList.append(path.join(theProject.projMeta, "docMergeOptions.json"))
rmList.append(path.join(theProject.projMeta, "sessionLogOptions.json"))
for rmFile in rmList:
if path.isfile(rmFile):
logger.info("Deleting: %s" % rmFile)
try:
unlink(rmFile)
except Exception as e:
logger.error(str(e))
return
+162
View File
@@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
"""novelWriter Options State
novelWriter Options State
=============================
Class holding the last state of GUI options
File History:
Created: 2019-10-21 [0.3.1] - Original version meant to be sub classed
Created: 2020-02-19 [0.4.5] - Rewritten from superclass to single file tool
"""
import logging
import json
import nw
from os import path
from nw.common import checkString, checkBool, checkInt
from nw.constants import nwFiles
logger = logging.getLogger(__name__)
class OptionState():
def __init__(self, theProject):
self.theProject = theProject
self.theState = {}
self.stringOpt = ()
self.boolOpt = ()
self.intOpt = ()
return
def loadSettings(self):
"""Load the options dictionary from the project settings file.
"""
stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
theState = {}
if path.isfile(stateFile):
logger.debug("Loading GUI options file")
try:
with open(stateFile,mode="r",encoding="utf8") as inFile:
theJson = inFile.read()
theState = json.loads(theJson)
except Exception as e:
logger.error("Failed to load GUI options file")
logger.error(str(e))
return False
for anOpt in theState:
self.theState[anOpt] = theState[anOpt]
return True
def saveSettings(self):
"""Save the options dictionary to the project settings file.
"""
stateFile = path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
logger.debug("Saving GUI options file")
try:
with open(stateFile,mode="w+",encoding="utf8") as outFile:
outFile.write(json.dumps(self.theState, indent=2))
except Exception as e:
logger.error("Failed to save GUI options file")
logger.error(str(e))
return False
return True
def setValue(self, setGroup, setName, setValue):
"""Saves a value, with a given group and name.
"""
if not setGroup in self.theState:
self.theState[setGroup] = {}
self.theState[setGroup][setName] = setValue
return True
def getValue(self, getGroup, getName, defaultValue):
"""Return an arbitrary type value, if it exists. Otherwise,
return the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return self.theState[getGroup][getName]
except:
return defaultValue
return defaultValue
def getString(self, getGroup, getName, defaultValue):
"""Return the value as a string, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return str(self.theState[getGroup][getName])
except:
return defaultValue
return defaultValue
def getInt(self, getGroup, getName, defaultValue):
"""Return the value as an int, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return int(self.theState[getGroup][getName])
except:
return defaultValue
return defaultValue
def getFloat(self, getGroup, getName, defaultValue):
"""Return the value as a float, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return float(self.theState[getGroup][getName])
except:
return defaultValue
return defaultValue
def getBool(self, getGroup, getName, defaultValue):
"""Return the value as a bool, if it exists. Otherwise, return
the default value.
"""
if getGroup in self.theState:
if getName in self.theState[getGroup]:
try:
return bool(self.theState[getGroup][getName])
except:
return defaultValue
return defaultValue
def validIntRange(self, theValue, intA, intB, intDefault):
"""Check that an int is in a given range. If it isn't, return
the default value.
"""
if isinstance(theValue, int):
if theValue >= intA and theValue <= intB:
return theValue
return intDefault
def validIntTuple(self, theValue, theTuple, intDefault):
"""Check that an int is an element of a tuple. If it isn't,
return the default value.
"""
if isinstance(theValue, int):
if theValue in theTuple:
return theValue
return intDefault
# END Class OptionState
-115
View File
@@ -1,115 +0,0 @@
# -*- coding: utf-8 -*-
"""novelWriter Options Last State
novelWriter Options Last State
==================================
Class holding the last state of GUI options
File History:
Created: 2019-10-21 [0.3.1]
"""
import logging
import json
import nw
from os import path
from nw.common import checkString, checkBool, checkInt
logger = logging.getLogger(__name__)
class OptLastState():
def __init__(self, theProject, theFile):
self.theProject = theProject
self.theFile = theFile
self.theState = {}
self.stringOpt = ()
self.boolOpt = ()
self.dictOpt = ()
self.intOpt = ()
return
def loadSettings(self):
if self.theProject.projMeta is None or self.theFile is None:
logger.error("Cannot load file '%s' to path '%s'" % (
str(self.theFile), str(self.theProject.projMeta)
))
return False
stateFile = path.join(self.theProject.projMeta,self.theFile)
theState = {}
if path.isfile(stateFile):
logger.debug("Loading options file")
try:
with open(stateFile,mode="r",encoding="utf8") as inFile:
theJson = inFile.read()
theState = json.loads(theJson)
except Exception as e:
logger.error("Failed to load options file")
logger.error(str(e))
return False
for anOpt in theState:
self.theState[anOpt] = theState[anOpt]
return True
def saveSettings(self):
if self.theProject.projMeta is None or self.theFile is None:
logger.error("Cannot save file '%s' to path '%s'" % (
str(self.theFile), str(self.theProject.projMeta)
))
return False
stateFile = path.join(self.theProject.projMeta,self.theFile)
logger.debug("Saving options file")
try:
with open(stateFile,mode="w+",encoding="utf8") as outFile:
outFile.write(json.dumps(self.theState, indent=2))
except Exception as e:
logger.error("Failed to save options file")
logger.error(str(e))
return False
return True
def setSetting(self, setName, setValue):
if setName in self.theState:
self.theState[setName] = setValue
else:
return False
return True
def getSetting(self, setName):
if setName in self.stringOpt:
return checkString(self.theState[setName],self.theState[setName],False)
elif setName in self.boolOpt:
return checkBool(self.theState[setName],self.theState[setName],False)
elif setName in self.dictOpt:
if isinstance(self.theState[setName], dict):
return self.theState[setName]
else:
return {}
elif setName in self.intOpt:
return checkInt(self.theState[setName],self.theState[setName],False)
return None
def validIntRange(self, theValue, intA, intB, intDefault):
if isinstance(theValue, int):
if theValue >= intA and theValue <= intB:
return theValue
return intDefault
def validIntTuple(self, theValue, theTuple, intDefault):
if isinstance(theValue, int):
if theValue in theTuple:
return theValue
return intDefault
# END Class OptLastState
+1
View File
@@ -4,3 +4,4 @@ markers =
core: Core functionality tests
gui: Qt5 GUI tests
serial
qt_api = pyqt5
-1
View File
@@ -1,5 +1,4 @@
pyqt5
appdirs
lxml
pyenchant
latexcodec
+3 -3
View File
@@ -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>
+2 -3
View File
@@ -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 -11
View File
@@ -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 =
+5 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()