Merge branch 'main' into xml_rw_class

This commit is contained in:
Veronica Berglyd Olsen
2022-10-21 10:24:00 +02:00
139 changed files with 7375 additions and 6828 deletions
+173
View File
@@ -1,7 +1,147 @@
# novelWriter Changelog
## Version 2.0 RC 1 [2022-10-17]
### Release Notes
This is a release candidate of the next release version, and is intended for testing purposes.
Please be careful when using this version on live writing projects, and make sure you take frequent
backups.
Please check the changelog for an overview of changes. The full release notes will be added to the
final release.
**Note:** As of the 2.0 release, novelWriter requires Qt 5.10 or higher and Python 3.7 or higher.
### Detailed Changelog
**Note:** This will no longer be release 1.7, but 2.0 instead due to the major changes to the User
Interface. PR #1146.
**Features**
* The add new documents feature has been made a little smarter and now tries to choose whether the
added document should be a sibling or a child to the selected document. Issue #1107. PR #1110.
* A folder in the Project Tree can now be converted to a Novel Document or Project Note from the
item's context menu. This makes sense now that documents can have child items. Issue #1071.
PR #1128.
* All child elements of an item in the Project Tree can now be collectively expanded or collapsed
from the context menu. The same action can be triggered on the entire tree from the menu button
at the top. Issue #1122. PR #1129.
* If using the auto-insert feature for spaces next to punctuation designed for for instance French,
any existing spaces are first stripped before the correct space character is added. Some users
find it hard to unlearn the reflex to type the space. Issue #1061. PR #1131.
* The Project Tree now has Quick Links for navigating directly between root folders. This is
convenient for very large projects. Issue #1137. PR #1165.
* It is now possible to filter out entire root folders on the Build Tool. Issue #1138. PR #1168.
**Bugfixes**
* Fix a typo in the Lorem Ipsum tool and set a max width for the views bar. PR #1065.
* The language set in the Build Tool is now properly exported to Open Document files. Previously,
it would be set to English regardless of the user's selection. Issue #1073. PR #1077.
* Since the text editor cursor extends to the right of its position in the window, it would
disappear under the right-hand margin if it reached the edge. A minimum margin of the width of
the cursor has been added to the editor, and the same value subtracted from the margin of the
viewbox. Thus, the cursor no longer disappears. Issue #1112. PR #1113.
* The `Shift+Enter` key combination no longer inserts a Unicode line separator in the text editor.
Issue #1150. PR #1151.
* Fixed an issue where idle time would not be properly reset when a new project was created.
Issue #1149. PR #1153.
* Removed the context menu that would appear on the Views Bar, from which the bar could be hidden.
This is some feature imposed by the Qt library, and it is not wanted. Issue #1147. PR #1153.
**Known Issues**
* An attempt has been made to fix the missing mime icon for novelWriter projects on Linux. The
newer versions of Nautilus don't seem to display the correct icon due to some changes in the way
icons are extracted from the theme. The icon displays fine in other places. PR #1068.
**Internationalisation**
* Norwegian and US English translations have been updated. PR #1170.
**User Interface**
* The New Project Wizard has been updated based on user feedback. The "Working Title" setting is
now called "Project Name". The number of root folders one can create on the options page has been
reduced to the standard ones. An option to add sample notes has been added, and the option to
generate chapter folders has been removed. The "Minimal Project" option also no longer creates
folders. An Archive root folder is automatically added to all new projects, and a Trash folder is
added to custom projects. PR #1067.
* The border around a lot of Widgets on the main GUI have been removed. PR #1069.
* The Views Bar has been updated since the beta re;ease with better icons, tooltips, and an
expanding menu on the Settings button. The Build Tool can now also be accessed from the Views
Bar. PR #1069.
* The editor theme setting in Preferences has been moved back to the General tab where users seem
to expect to find it. PR #1069.
* A toolbar has been added to the Outline View where the user can select which Novel folder to
view. A refresh button for a forced reload of the selected Novel folder has been added, and the
menu to select which columns to show has been added to a new button. This makes it easier to find
for users instead of having to right-click the table header. Issue #1105. PRs #1063, #1094, and
#1111.
* The Rebuild Outline and Auto-Update Outline options have been removed from the Main Menu.
PR #1063.
* The Project Tree has been redesigned. The header is now hidden, and the columns resize
automatically. A toolbar has been added with buttons for moving items up and down in the tree, a
button for adding new files, folders and root folders, and a menu button for further options
affecting the whole tree. These features have mostly bee removed from the Main Menu. PR #1079.
* The context menu of the Project Tree has been rewritten completely. It is now possible to set
importance and status directly from this menu. Part of issue #973. PRs #1079 and #1105.
* The Edit Item dialog has been replaced with a simple Edit Label dialog. Most of the features
handled by the old dialog have been moved to the Project Tree context menu. PR #1082.
* The Novel View has been redesigned to match the new Outline View and Project Tree. The header is
now hidden, and the columns auto-size. The third column can be selected from a set of options, or
be hidden entirely. A button with a menu can be used to select which Novel folder to show.
Issue #1041. PR #1084.
* An arrow icon has been added behind each item in the Novel View. Clicking it, will pop up a
tooltip showing the collected meta data for the heading the item represents. PR #1088.
* The Project Details dialog now supports multiple novel folders. Issue #1078. PRs #1130 and #1167.
* The Split and Merge tools have been rewritten to work with the new feature of allowing documents
to have child documents. The tools have also had more options added to them. The feature is now
accessible through the Project Tree item's context menu, and is no longer available from the Main
Menu. Issues #1072 and #1032. PRs #1148 and #1154.
**Installation and Packaging**
* AppImage distribution has been added by @Ryex. Issue #1091. PR #1092.
**Code Improvements**
* A new test project generator function has been added to replace the dependency on the New Project
tools in the app itself. This ensures that changing the in-app feature doesn't affect the entire
test suite. Over time, this generator function should also replace the minimal sample project
saved in the test suite. PR #1067.
* The index class has been rewritten. The index data is now stored in a hierarchy of objects rather
than a set of nested dictionaries. The `itemIndex` holds all the data collected from the project,
and the `tagsIndex` is a reverse lookup index for linking tags back to where they are used. The
reading/writing of the index to disk between sessions is now handled by pack/unpack functions in
each object. The index will be regenerated when the user first opens a project as it has been
completely restructured. PR #1074.
* The index instance is now an instance of the project class, not the main GUI. PR #1074.
* The Outline View has been restructured into a single parent widget in the same manner as the
Novel View was in the previous pre-release. Related to #1041. PR #1063.
* A lot of main GUI objects have been given new names in the code to better represent what they do.
PR #1081.
* The converter for the old 1.0 project structure has been simplified. Mostly to reduce the amount
of code and number of translation labels needed for it. It now produces a single error if
something went wrong. PR #1083.
* A number of unused icons have been removed from the code base. PR #1164.
* All checks for Qt versions below 5.10 have been removed. PR #1174.
**Not Implemented**
* An idea to make the indexer run in the global thread pool was not implemented. Issue #1076.
* Feature #997 is now obsolete due to the changes made to the Edit Item dialog in #1082.
* Feature #1106 is not implemented. It proposes to hide the option to select Novel folder if there
is only one. We will see if this is really needed based on user feedback.
----
## Version 1.7 Beta 1 [2022-05-17]
**Note:** The 1.7 release cycle was renamed 2.0 on 2022-10-06. See #1144.
### Release Notes
This is a beta release of the next release version, and is intended for testing purposes. Please be
@@ -89,6 +229,39 @@ final release.
----
## Version 1.6.5 [2022-10-13]
### Release Notes
This is a bugfix release that fixes a a few minor issues. The idle time for new projects would be
artificially inflated as the clock was not reset when the project was first created. This only
affects the first entry in the writing statistics. A scaling issue for the Preferences dialog has
also been fixed. It only affected screens with UI scaling enabled. Lastly, typing `Shift+Enter` in
the text editor now creates a regular line break instead of a special line separator. The line
separator serves no purpose in plain text, and was producing inconsistencies in how text is
processed and displayed.
### Detailed Changelog
**Bugfixes**
* Fixed a bug where the idle time was not properly zeroed when a new project was generated after
the wizard was closed. The idle time would be calculated from the time the previous project
closed, thus inflating the value. Issue #1149. PR #1159.
* Fixes an issue where the window size of the Preferences dialog would have the GUI scaling factor
applied twice when the dialog was closed, resulting in the dialog growing in size each time it is
opened. Issue #989. PR #1159.
**Other Changes**
* The text editor no longer creates a Unicode line separator (U+2028) when the user presses
`Shift+Enter`. The line separator serves no purpose in a plain text editor, and the code in
general treats them as regular line break. This caused the line separator to display differently
before and after saving. The line separator character is now automatically replaced by a
paragraph separator. Issue #1150. PR #1159.
----
## Version 1.6.4 [2022-09-29]
### Release Notes
+1 -1
View File
@@ -33,7 +33,7 @@ You can also follow novelWriter on Mastodon at [fosstodon.org/@novelwriter](http
## Implementation
The application is written with Python 3 (3.7+) using Qt5 and PyQt5 (5.3+). It is developed on
The application is written with Python 3 (3.7+) using Qt5 and PyQt5 (5.10+). It is developed on
Linux, but should in principle work fine on other operating systems as well as long as dependencies
are met. It is regularly tested on Debian and Ubuntu Linux, Windows, and macOS.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 KiB

After

Width:  |  Height:  |  Size: 534 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 431 KiB

After

Width:  |  Height:  |  Size: 539 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 379 KiB

+1 -1
View File
@@ -71,5 +71,5 @@ On Windows, file extensions may not be visible by default, so make sure you only
extension, and don't end up with two.
The QSS files are Qt Style Sheet files. See Qt's
`The Style Sheet Syntax <https://doc.qt.io/qt-5/stylesheet-syntax.html>`_` documentation for more
`The Style Sheet Syntax <https://doc.qt.io/qt-5/stylesheet-syntax.html>`_ documentation for more
details.
+1 -1
View File
@@ -34,7 +34,7 @@ The following Python packages are needed to run novelWriter:
* ``lxml`` needed for full XML support.
* ``PyEnchant`` needed for spell checking (optional).
PyQt/Qt should be at least 5.3, but ideally 5.10 or higher for nearly all features to work. For
PyQt/Qt should be at least 5.10, but ideally 5.13 or higher for nearly all features to work. For
instance, searching using regular expressions with full Unicode support requires 5.13. There is no
known minimum version requirement for package ``lxml``, but the code was originally written with
4.2, which is therefore set as the minimum. It may work on lower versions. You have to test it.
+1 -2
View File
@@ -40,8 +40,7 @@ Application Data
novelWriter also stores a bit of data that is generated by the user's actions. This includes the
list of recent projects form the :guilabel:`Open Project` dialog. Custom themes are also saved
here. The system paths are provided by the Qt QStandardPaths_ class and its AppDataLocation value
on Qt 5.4 or greater, or DataLocation for earlier versions.
here. The system paths are provided by the Qt QStandardPaths_ class and its AppDataLocation.
The standard paths are:
+1
View File
@@ -47,6 +47,7 @@ The main shorcuts are as follows:
":kbd:`Ctrl`:kbd:`H`", "Open the search and replace bar and search for the selected word, if any is selected. (On Mac, this is :kbd:`Cmd`:kbd:`=`.)"
":kbd:`Ctrl`:kbd:`I`", "Format selected text, or word under cursor, with emphasis (italic)."
":kbd:`Ctrl`:kbd:`K`", "Activate the insert commands. The commands are listed in :ref:`a_kb_ins`."
":kbd:`Ctrl`:kbd:`L`", "Open the :guilabel:`Quick Links` menu in the Project Tree."
":kbd:`Ctrl`:kbd:`N`", "Create new project item."
":kbd:`Ctrl`:kbd:`O`", "Open selected document."
":kbd:`Ctrl`:kbd:`Q`", "Exit novelWriter."
+1116 -1093
View File
File diff suppressed because it is too large Load Diff
+1134 -1111
View File
File diff suppressed because it is too large Load Diff
+1138 -1115
View File
File diff suppressed because it is too large Load Diff
+7 -38
View File
@@ -60,9 +60,9 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__version__ = "1.7-beta1"
__hexversion__ = "0x010700b1"
__date__ = "2022-05-17"
__version__ = "2.0-rc1"
__hexversion__ = "0x020000c1"
__date__ = "2022-10-17"
__status__ = "Stable"
__domain__ = "novelwriter.io"
__url__ = "https://novelwriter.io"
@@ -72,32 +72,6 @@ __helpurl__ = "https://github.com/vkbo/novelWriter/discussions"
__releaseurl__ = "https://github.com/vkbo/novelWriter/releases/latest"
__docurl__ = "https://novelwriter.readthedocs.io"
##
# Logging
# =========
# Standard used for logging levels in novelWriter:
# CRITICAL Use for errors that result in termination of the program
# ERROR Use when an action fails, but execution continues
# WARNING When something unexpected, but non-critical happens
# INFO Any useful user information like open, save, exit initiated
# ----------- SPAM Threshold : Output above should be minimal -----------------
# DEBUG Use for descriptions of main program flow
# VERBOSE Use for outputting values and program flow details
##
# Add verbose logging level
VERBOSE = 5
logging.addLevelName(VERBOSE, "VERBOSE")
def logVerbose(self, message, *args, **kws):
if self.isEnabledFor(VERBOSE):
self._log(VERBOSE, message, args, **kws)
logging.Logger.verbose = logVerbose
# Initiating logging
logger = logging.getLogger(__name__)
@@ -122,7 +96,6 @@ def main(sysArgs=None):
"version",
"info",
"debug",
"verbose",
"style=",
"config=",
"data=",
@@ -143,7 +116,6 @@ def main(sysArgs=None):
" -v, --version Print program version and exit.\n"
" --info Print additional runtime information.\n"
" --debug Print debug output. Includes --info.\n"
" --verbose Increase verbosity of debug output. Includes --debug.\n"
" --style= Sets Qt5 style flag. Defaults to 'Fusion'.\n"
" --config= Alternative config file.\n"
" --data= Alternative user data path.\n"
@@ -181,9 +153,6 @@ def main(sysArgs=None):
elif inOpt == "--debug":
logLevel = logging.DEBUG
logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}"
elif inOpt == "--verbose":
logLevel = VERBOSE
logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}"
elif inOpt == "--style":
qtStyle = inArg
elif inOpt == "--config":
@@ -214,14 +183,14 @@ def main(sysArgs=None):
"At least Python 3.7 is required, found %s" % CONFIG.verPyString
)
errorCode |= 0x04
if CONFIG.verQtValue < 50300:
if CONFIG.verQtValue < 51000:
errorData.append(
"At least Qt5 version 5.3 is required, found %s" % CONFIG.verQtString
"At least Qt5 version 5.10 is required, found %s" % CONFIG.verQtString
)
errorCode |= 0x08
if CONFIG.verPyQtValue < 50300:
if CONFIG.verPyQtValue < 51000:
errorData.append(
"At least PyQt5 version 5.3 is required, found %s" % CONFIG.verPyQtString
"At least PyQt5 version 5.10 is required, found %s" % CONFIG.verPyQtString
)
errorCode |= 0x10
@@ -18,10 +18,10 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Map]
add = typ_plus.svg
backward = typ_chevron-left.svg
bookmark = typ_bookmark.svg
bullet-off = typ_media-record-outline.svg
bullet-on = typ_media-record.svg
check = typ_tick.svg
clear = typ_backspace.svg
close = typ_times.svg
cls_archive = typ_delete.svg
cls_character = typ_user.svg
@@ -35,17 +35,9 @@ cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg
cls_world = typ_location.svg
cross = typ_times.svg
delete = typ_delete.svg
doc_h0 = mixed_heading0.svg
doc_h1 = mixed_heading1.svg
doc_h2 = mixed_heading2.svg
doc_h3 = mixed_heading3.svg
doc_h4 = mixed_heading4.svg
done = typ_input-checked.svg
down = typ_chevron-down.svg
edit = typ_pencil.svg
forward = typ_chevron-right.svg
hash = typ_hash.svg
maximise = typ_arrow-maximise.svg
menu = typ_th-menu.svg
minimise = typ_arrow-minimise.svg
@@ -54,13 +46,13 @@ proj_details = typ_th-list-grey.svg
proj_document = typ_document-text.svg
proj_folder = typ_folder.svg
proj_note = mixed_document-note.svg
proj_section = mixed_document-section.svg
proj_scene = mixed_document-scene.svg
proj_stats = typ_chart-bar-grey.svg
proj_title = mixed_document-title.svg
reference = typ_at.svg
refresh = typ_refresh.svg
remove = typ_minus.svg
save = typ_download.svg
search = typ_search.svg
search_cancel = typ_cancel-grey.svg
search_case = nw_search-case.svg
@@ -0,0 +1,47 @@
<?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"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg2161">
<metadata
id="metadata2167">
<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="defs2165" />
<path
id="rect12986"
style="display:inline;opacity:0.95;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.88635;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 5.8886719 3.4453125 C 5.2152275 3.4453125 4.6660156 3.9925712 4.6660156 4.6660156 L 4.6660156 19.333984 C 4.6660156 20.007428 5.2152275 20.554687 5.8886719 20.554688 L 18.111328 20.554688 C 18.784774 20.554688 19.333984 20.007428 19.333984 19.333984 L 19.333984 4.6660156 C 19.333984 3.9925712 18.784774 3.4453125 18.111328 3.4453125 L 5.8886719 3.4453125 z " />
<path
id="rect935"
style="fill:#999999;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
d="M 5.8886719 3.4453125 C 5.2152275 3.4453125 4.6660156 3.9925712 4.6660156 4.6660156 L 4.6660156 6 L 4.6660156 7.1894531 L 4.6660156 10.058594 L 19.333984 10.058594 L 19.333984 7.1894531 L 19.333984 6 L 19.333984 4.6660156 C 19.333984 3.9925712 18.784774 3.4453125 18.111328 3.4453125 L 5.8886719 3.4453125 z " />
<path
id="path2157"
style="display:inline;fill:#595959;fill-opacity:1;stroke-width:1.22222"
d="M 7.1113281,13.222656 C 6.7739949,13.222656 6.5,13.49665 6.5,13.833984 c 0,0.337332 0.2739949,0.611328 0.6113281,0.611328 h 9.7773439 c 0.337334,1e-6 0.611328,-0.273996 0.611328,-0.611328 0,-0.337334 -0.273994,-0.611328 -0.611328,-0.611328 z m 0,3.666016 C 6.7739949,16.888672 6.5,17.162666 6.5,17.5 c 0,0.337334 0.2739949,0.611328 0.6113281,0.611328 H 16.888672 C 17.226006,18.111328 17.5,17.837334 17.5,17.5 c 0,-0.337334 -0.273994,-0.611328 -0.611328,-0.611328 z" />
<path
id="path902"
style="display:inline;fill:#848484;fill-opacity:1;stroke-width:1.22222"
d="M 5.8886719,1 C 3.8671165,1 2.2226562,2.6444602 2.2226562,4.6660156 V 19.333984 C 2.2226563,21.355538 3.8671165,23 5.8886719,23 H 18.111328 c 2.021556,0 3.666016,-1.644462 3.666016,-3.666016 V 4.6660156 C 21.777344,2.6444602 20.132884,1 18.111328,1 Z m 0,2.4453125 H 18.111328 c 0.673446,0 1.222656,0.5472587 1.222656,1.2207031 V 19.333984 c 0,0.673444 -0.54921,1.220704 -1.222656,1.220704 H 5.8886719 c -0.6734444,-10e-7 -1.2226563,-0.54726 -1.2226563,-1.220704 V 4.6660156 c 0,-0.6734444 0.5492119,-1.2207031 1.2226563,-1.2207031 z" />
<path
id="path2157-0"
style="display:inline;fill:#ffffff;fill-opacity:1;stroke-width:1.22222"
d="M 7.1113281 5.8886719 C 6.7739949 5.8886719 6.5 6.1626667 6.5 6.5 C 6.5 6.837334 6.7739949 7.1113281 7.1113281 7.1113281 L 16.888672 7.1113281 C 17.226006 7.1113281 17.5 6.837334 17.5 6.5 C 17.5 6.1626667 17.226006 5.8886719 16.888672 5.8886719 L 7.1113281 5.8886719 z " />
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -1,31 +0,0 @@
<?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="svg1897"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata1903">
<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="defs1901" />
<path
style="fill:#6699cc;fill-opacity:1;stroke-width:1.42859697"
id="path1895"
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,37 +0,0 @@
<?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="svg1897"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata1903">
<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="defs1901" />
<path
style="fill:#6699cc;fill-opacity:1;stroke-width:1.42859697"
id="path1895"
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
<circle
r="2.2222221"
cy="4.2222223"
cx="19.470242"
id="path2467"
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,43 +0,0 @@
<?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="svg1897"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata1903">
<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="defs1901" />
<path
style="fill:#6699cc;fill-opacity:1;stroke-width:1.42859697"
id="path1895"
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
<circle
r="2.2222221"
cy="4.2222223"
cx="19.470242"
id="path2467"
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle2469"
cx="19.470242"
cy="9.4077778"
r="2.2222221" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

@@ -1,49 +0,0 @@
<?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="svg1897"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata1903">
<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="defs1901" />
<path
style="fill:#6699cc;fill-opacity:1;stroke-width:1.42859697"
id="path1895"
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
<circle
r="2.2222221"
cy="4.2222223"
cx="19.470242"
id="path2467"
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle2469"
cx="19.470242"
cy="9.4077778"
r="2.2222221" />
<circle
r="2.2222221"
cy="14.592222"
cx="19.470242"
id="circle2471"
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,59 +0,0 @@
<?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="svg1897"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata1903">
<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="defs1901" />
<g
transform="matrix(1.1111111,0,0,1.1111111,-1.3333334,-1.3333332)"
id="g2616">
<path
d="m 4.029904,3.7531206 c -1.0041608,1.0041608 -1.0041608,2.6319041 0,3.6360649 L 8.6392724,11.999839 4.029904,16.610493 c -1.0041608,1.004161 -1.0041608,2.631904 0,3.636065 C 4.5313415,20.749281 5.189639,21 5.8479364,21 c 0.658298,0 1.316595,-0.250719 1.818033,-0.753442 L 15.913973,11.999839 7.6659694,3.7531206 c -1.002876,-1.0041608 -2.6331904,-1.0041608 -3.6360654,0 z"
id="path1895"
style="fill:#6699cc;fill-opacity:1;stroke-width:1.28573728" />
<circle
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.79999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path2467"
cx="18.723217"
cy="5"
r="2" />
<circle
r="2"
cy="9.6669998"
cx="18.723217"
id="circle2469"
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.79999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.79999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle2471"
cx="18.723217"
cy="14.333"
r="2" />
<circle
r="2"
cy="19"
cx="18.723217"
id="circle2473"
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.79999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

@@ -1,31 +0,0 @@
<?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"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg13156">
<metadata
id="metadata13162">
<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="defs13160" />
<path
d="M 19.496214,4.4886395 H 9.4810665 c -1.2679176,0 -2.8382927,0.8082224 -3.5754075,1.8397825 L 3.2917056,9.9869555 2.0908895,11.668499 c -0.1211833,0.175265 -0.1221848,0.492744 0.003,0.665006 l 1.1897995,1.66652 2.622967,3.672554 c 0.7361133,1.030558 2.3054869,1.838782 3.574406,1.838782 H 19.496214 C 20.877302,19.511361 22,18.387661 22,17.007573 V 6.9924263 C 22,5.612339 20.877302,4.4886395 19.496214,4.4886395 Z M 17.19974,14.296473 c 0.391593,0.391592 0.391593,1.02455 0,1.416141 -0.195295,0.195296 -0.451683,0.293445 -0.70807,0.293445 -0.256388,0 -0.512776,-0.09815 -0.708071,-0.293445 l -2.296474,-2.296473 -2.296473,2.296473 c -0.195295,0.195296 -0.451683,0.293445 -0.708071,0.293445 -0.256388,0 -0.512775,-0.09815 -0.7080706,-0.293445 -0.3915923,-0.391591 -0.3915923,-1.024549 0,-1.416141 L 12.070984,12 9.7745104,9.7035265 c -0.3915923,-0.3915921 -0.3915923,-1.0245494 0,-1.4161417 0.3915926,-0.3915922 1.0245496,-0.3915922 1.4161416,0 l 2.296473,2.2964732 2.296474,-2.2964732 c 0.391592,-0.3915922 1.024549,-0.3915922 1.416141,0 0.391593,0.3915923 0.391593,1.0245496 0,1.4161417 L 14.903267,12 Z"
id="path13154"
style="stroke-width:1.00151;fill:#d64848;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

@@ -9,9 +9,9 @@
width="24"
height="24"
viewBox="0 0 24 24"
id="svg2608">
id="svg901">
<metadata
id="metadata2614">
id="metadata907">
<rdf:RDF>
<cc:Work
rdf:about="">
@@ -23,9 +23,9 @@
</rdf:RDF>
</metadata>
<defs
id="defs2612" />
id="defs905" />
<path
d="M 16.203226,2 H 7.796774 C 6.05874,2 4.6443545,3.4143855 4.6443545,5.1524195 V 19.86371 c 0,0.540115 0.1092839,0.994063 0.3236484,1.350287 0.5926549,0.982504 1.9072138,1.059213 2.9559187,0.0084 L 11.25708,17.889239 c 0.394052,-0.393002 1.091787,-0.393002 1.485841,0 l 3.333158,3.333158 C 16.592024,21.738349 17.128987,22 17.673304,22 c 0.837492,0 1.682342,-0.660957 1.682342,-2.13629 V 5.1524195 C 19.355646,3.4143855 17.941259,2 16.203226,2 Z M 7.796774,4.101613 h 8.406452 c 0.578994,0 1.050806,0.4718121 1.050806,1.0508065 V 15.560658 l -2.575527,-2.361162 c -1.477434,-1.35449 -3.880628,-1.353439 -5.3580615,0 l -2.574476,2.361162 V 5.1524195 c 0,-0.5789944 0.4718121,-1.0508065 1.0508065,-1.0508065 z m 6.431987,12.301792 C 13.635055,15.809699 12.843798,15.482898 12,15.482898 c -0.843797,0 -1.635054,0.327852 -2.2287605,0.920507 l -3.025272,3.025272 v -2.442075 l 3.2848215,-3.011611 c 1.085483,-0.995114 2.851889,-0.995114 3.937372,0 l 3.285871,3.011611 v 2.442075 z"
id="path2606"
style="stroke-width:1.05081;fill:#6699cc;fill-opacity:1" />
id="path899"
style="fill:#6699cc;fill-opacity:1;stroke-width:1.05081" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,35 +0,0 @@
<?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"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg13801">
<metadata
id="metadata13807">
<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="defs13805" />
<path
style="display:inline;fill:#cda66f;fill-opacity:1"
id="path6288"
d="m 20.987,16 c 0,-0.105 -0.004,-0.211 -0.039,-0.316 l -2,-6 C 18.812,9.275 18.431,9 18,9 h -0.219 c -0.094,0.188 -0.21,0.368 -0.367,0.525 L 15.932,11 h 1.348 l 1.667,5 H 5.054 L 6.721,11 H 8.069 L 6.586,9.525 C 6.429,9.368 6.312,9.188 6.219,9 H 6 C 5.569,9 5.188,9.275 5.052,9.684 l -2,6 C 3.017,15.789 3.013,15.895 3.013,16 3,16 3,21 3,21 c 0,0.553 0.447,1 1,1 h 16 c 0.553,0 1,-0.447 1,-1 0,0 0,-5 -0.013,-5 z" />
<path
d="M 16.707,7.404 C 16.518,7.216 16.259,7.121 16,7.121 c -0.259,0 -0.518,0.095 -0.707,0.283 L 13,9.697 V 3 C 13,2.448 12.552,2 12,2 11.448,2 11,2.448 11,3 V 9.697 L 8.707,7.404 C 8.518,7.216 8.267,7.111 8,7.111 c -0.267,0 -0.518,0.105 -0.707,0.293 -0.39,0.39 -0.39,1.024 0,1.414 L 12,13.5 16.709,8.816 C 17.097,8.429 17.097,7.794 16.707,7.404 Z"
id="path13799"
style="display:inline;fill:#99cc99;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

@@ -1,31 +0,0 @@
<?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"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg3369">
<metadata
id="metadata3375">
<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="defs3373" />
<path
d="m 10.828125,20 h -4 c -0.553,0 -1,0.447 -1,1 0,0.553 0.447,1 1,1 h 10 c 0.553,0 1,-0.447 1,-1 0,-0.553 -0.447,-1 -1,-1 h -4 v -1.23 c 1.64,-0.371 3.146,-1.188 4.363,-2.406 1.7,-1.7 2.637,-3.96 2.637,-6.364 0,-2.067 -0.692,-4.029 -1.968,-5.619 l 0.675,-0.673 c 0.391,-0.391 0.391,-1.023 10e-4,-1.415 -0.391,-0.391 -1.024,-0.39 -1.415,-0.001 l -2.052,2.049 0.708,0.708 c 1.322,1.322 2.051,3.081 2.051,4.951 0,1.87 -0.729,3.627 -2.051,4.949 -1.322,1.322 -3.079,2.051 -4.949,2.051 -1.87,0 -3.627,-0.729 -4.949,-2.051 -0.391,-0.391 -1.023,-0.391 -1.414,0 -0.391,0.39 -0.391,1.023 0,1.414 1.699,1.7 3.959,2.637 6.363,2.637 z m 0,-16 c 1.657,0 3.157,0.672 4.243,1.757 1.085,1.086 1.757,2.586 1.757,4.243 0,1.656 -0.672,3.156 -1.757,4.242 -1.086,1.086 -2.586,1.758 -4.243,1.758 -1.658,0 -3.157,-0.672 -4.242,-1.757 -1.085,-1.086 -1.756,-2.586 -1.756,-4.243 0,-1.657 0.671,-3.157 1.756,-4.243 1.085,-1.085 2.584,-1.757 4.242,-1.757 z"
id="path3367"
style="fill:#aeaeae;fill-opacity:1;stroke-width:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,31 +0,0 @@
<?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"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg5204">
<metadata
id="metadata5210">
<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="defs5208" />
<path
id="rect928"
d="M 7.75 2 C 6.7805 2 6 2.7805 6 3.75 L 6 5.5 L 4.1621094 5.5 C 3.2410844 5.5 2.5 6.2805 2.5 7.25 C 2.5 8.2195 3.2410844 9 4.1621094 9 L 6 9 L 6 15 L 4.1621094 15 C 3.2410844 15 2.5 15.7805 2.5 16.75 C 2.5 17.7195 3.2410844 18.5 4.1621094 18.5 L 6 18.5 L 6 20.25 C 6 21.2195 6.7805 22 7.75 22 C 8.7195 22 9.5 21.2195 9.5 20.25 L 9.5 18.5 L 14.5 18.5 L 14.5 20.25 C 14.5 21.2195 15.2805 22 16.25 22 C 17.2195 22 18 21.2195 18 20.25 L 18 18.5 L 19.837891 18.5 C 20.758916 18.5 21.5 17.7195 21.5 16.75 C 21.5 15.7805 20.758916 15 19.837891 15 L 18 15 L 18 9 L 19.837891 9 C 20.758916 9 21.5 8.2195 21.5 7.25 C 21.5 6.2805 20.758916 5.5 19.837891 5.5 L 18 5.5 L 18 3.75 C 18 2.7805 17.2195 2 16.25 2 C 15.2805 2 14.5 2.7805 14.5 3.75 L 14.5 5.5 L 9.5 5.5 L 9.5 3.75 C 9.5 2.7805 8.7195 2 7.75 2 z M 9.5 9 L 14.5 9 L 14.5 15 L 9.5 15 L 9.5 9 z "
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.7544229;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,35 +0,0 @@
<?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="svg11367"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata11373">
<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="defs11371" />
<path
style="display:inline;fill:#99cc99;fill-opacity:1;stroke-width:1.28570998"
id="path11365"
d="m 13.499143,15.642429 c -0.45,0 -0.885857,-0.178715 -1.209857,-0.502715 L 8.859,11.710714 c -0.668571,-0.669857 -0.668571,-1.755 0,-2.423571 0.669857,-0.669857 1.755,-0.669857 2.426143,0 L 13.105714,11.109 17.573571,4.0954286 c 0.459,-0.828 1.504286,-1.1275715 2.332286,-0.6672857 0.826714,0.4602857 1.125,1.5042857 0.666,2.331 l -5.572286,9.0000001 c -0.261,0.470571 -0.727714,0.790714 -1.26,0.865286 z" />
<path
d="M 17.142857,21 H 6.8571429 C 4.7305714,21 3,19.269429 3,17.142857 V 6.8571429 C 3,4.7305714 4.7305714,3 6.8571429,3 h 6.4285711 c 0.711,0 1.285715,0.576 1.285715,1.2857143 0,0.7097143 -0.574715,1.2857143 -1.285715,1.2857143 H 6.8571429 c -0.7097143,0 -1.2857143,0.5772857 -1.2857143,1.2857143 V 17.142857 c 0,0.708429 0.576,1.285714 1.2857143,1.285714 H 17.142857 c 0.709714,0 1.285714,-0.577285 1.285714,-1.285714 V 13.285714 C 18.428571,12.576 19.003286,12 19.714286,12 20.425286,12 21,12.576 21,13.285714 v 3.857143 C 21,19.269429 19.269429,21 17.142857,21 Z"
id="path16196"
style="display:inline;fill:#aeaeae;fill-opacity:1;stroke-width:1.28570998" />
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

@@ -1,39 +0,0 @@
<?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"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg873">
<metadata
id="metadata879">
<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="defs877" />
<path
id="path871"
style="display:inline;fill:#6699cc;fill-opacity:1;stroke-width:1.19007"
d="m 10.330622,1.9999997 c -4.5924663,0 -8.3306193,3.738153 -8.3306193,8.3306193 0,1.391929 0.3467825,2.702587 0.9532665,3.857242 0.084308,-0.108411 0.1590865,-0.228838 0.2534294,-0.323181 L 4.7482003,12.323178 C 4.5237338,11.697056 4.378519,11.032991 4.3785189,10.330619 c 0,-3.2810129 2.6710902,-5.949778 5.9521031,-5.949778 0.702303,0 1.36649,0.1429324 1.992559,0.3673563 L 14.141363,2.9300158 C 12.997884,2.3384803 11.704084,1.9999997 10.330622,1.9999997 Z m 5.635898,3.4852355 -0.60451,0.6045104 2.545919,2.5482442 0.460357,-0.4626829 C 18.332278,8.0413114 18.299136,7.9066594 18.256684,7.7754 Z m 0.292955,4.4245521 c 0.0083,0.1189097 0.01733,0.2373427 0.01861,0.3580557 l 0.169728,-0.169728 z m 2.059986,2.6993717 -5.710299,5.710299 c 0.105352,-0.03032 0.217183,-0.04484 0.320855,-0.07906 l 0.892816,0.89049 1.799581,1.797261 0.07208,0.07208 0.07672,0.06743 C 16.486443,21.66984 17.375591,22 18.275281,22 c 2.054056,0 3.724716,-1.671795 3.724716,-3.72704 0,-0.999657 -0.390465,-1.936781 -1.099745,-2.638921 l -1.129969,-1.174145 -0.639387,-0.639386 -0.890489,-0.892815 c 0.03403,-0.103138 0.04885,-0.213757 0.07906,-0.318531 z m -11.7600542,2.28784 -0.4929085,0.492908 0.5417343,0.541734 0.57196,-0.571959 C 6.960254,15.221393 6.7588057,15.062002 6.5594068,14.896999 Z m 2.7179722,1.285747 -1.209021,1.209021 0.5417345,0.541734 1.6577535,-1.655429 c -0.3372847,-0.0036 -0.668706,-0.03764 -0.990467,-0.09533 z" />
<path
d="m 15.96652,2.4766329 c -0.396657,0 -0.792437,0.1507259 -1.095095,0.4533829 L 3.57173,14.229712 c -0.3026571,0.302657 -0.5759103,0.751428 -0.7742385,1.227621 -0.1983282,0.47826 -0.3208556,0.99063 -0.3208556,1.418274 v 4.647756 h 4.6477558 c 0.427645,0 0.9376894,-0.122526 1.4159495,-0.320855 0.4782601,-0.198328 0.9272894,-0.471583 1.2299463,-0.774239 L 21.069984,9.1285734 c 0.302656,-0.302657 0.453382,-0.7017958 0.453382,-1.0974191 0,-0.3966562 -0.150726,-0.7924369 -0.453382,-1.0950939 L 17.063939,2.9300158 C 16.761281,2.6273588 16.362143,2.4766329 15.96652,2.4766329 Z m 0,2.2785396 3.275981,3.2759818 -1.334572,1.3368983 -3.275982,-3.2759819 z M 13.901884,6.8221334 15.173682,8.0939305 6.6082326,16.661704 5.3364357,15.387582 Z m 2.004185,2.0041848 1.271796,1.2717968 -8.5654475,8.565448 -1.2741221,-1.271796 z M 4.7063497,16.250176 c 0.010333,-0.02065 3.0644031,3.034179 3.0644031,3.034179 C 7.43814,19.422771 7.2039295,19.456408 7.1243917,19.456408 H 5.5759147 L 4.5435968,18.424084 v -1.548477 c 0,-0.07953 0.033633,-0.315548 0.1627529,-0.625436 z"
style="display:inline;fill:#99cc99;fill-opacity:1;stroke-width:1.19007"
id="path15590" />
<path
id="rect19553"
d="m 2.4766359,18.937425 v 2.585938 h 2.5605469 z"
style="display:inline;opacity:0.95;fill:#0a0a0a;fill-opacity:1;stroke:none;stroke-width:1.88249397;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 3.6 KiB

@@ -1,36 +0,0 @@
<?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="svg7030"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata7036">
<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="defs7034" />
<g
style="fill:#aeaeae;fill-opacity:1"
transform="matrix(0.95238095,0,0,0.95238095,0.5714286,0.09523812)"
id="g7028">
<path
style="fill:#aeaeae;fill-opacity:1"
id="path7026"
d="M 19.414,8.902 C 19.518,8.854 19.62,8.794 19.707,8.707 l 0.5,-0.5 c 0.391,-0.391 0.391,-1.023 0,-1.414 -0.391,-0.391 -1.023,-0.391 -1.414,0 l -0.5,0.5 -0.115,0.173 C 16.791,6.154 14.99,5.276 12.989,5.056 L 13,5 V 4 h 1 C 14.55,4 15,3.55 15,3 15,2.45 14.55,2 14,2 H 10 C 9.45,2 9,2.45 9,3 9,3.55 9.45,4 10,4 h 1 v 1 l 0.012,0.057 C 6.506,5.549 3,9.364 3,14 c 0,4.971 4.029,9 9,9 4.971,0 9,-4.029 9,-9 0,-1.894 -0.588,-3.648 -1.586,-5.098 z M 12,21 C 8.141,21 5,17.86 5,14 5,10.14 8.141,7 12,7 c 3.859,0 7,3.14 7,7 0,3.86 -3.141,7 -7,7 z m 1,-8 v -2 c 0,-0.55 -0.45,-1 -1,-1 -0.55,0 -1,0.45 -1,1 v 3 c 0,0.55 0.45,1 1,1 h 3 c 0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1 z M 12,8 c -3.312,0 -6,2.688 -6,6 0,3.312 2.688,6 6,6 3.312,0 6,-2.688 6,-6 0,-3.312 -2.688,-6 -6,-6 z m 0,11 C 9.243,19 7,16.757 7,14 7,11.243 9.243,9 12,9 c 2.757,0 5,2.243 5,5 0,2.757 -2.243,5 -5,5 z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -18,10 +18,10 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Map]
add = typ_plus.svg
backward = typ_chevron-left.svg
bookmark = typ_bookmark.svg
bullet-off = typ_media-record-outline.svg
bullet-on = typ_media-record.svg
check = typ_tick.svg
clear = typ_backspace.svg
close = typ_times.svg
cls_archive = typ_delete.svg
cls_character = typ_user.svg
@@ -35,17 +35,9 @@ cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg
cls_world = typ_location.svg
cross = typ_times.svg
delete = typ_delete.svg
doc_h0 = mixed_heading0.svg
doc_h1 = mixed_heading1.svg
doc_h2 = mixed_heading2.svg
doc_h3 = mixed_heading3.svg
doc_h4 = mixed_heading4.svg
done = typ_input-checked.svg
down = typ_chevron-down.svg
edit = typ_pencil.svg
forward = typ_chevron-right.svg
hash = typ_hash.svg
maximise = typ_arrow-maximise.svg
menu = typ_th-menu.svg
minimise = typ_arrow-minimise.svg
@@ -54,13 +46,13 @@ proj_details = typ_th-list-grey.svg
proj_document = typ_document-text.svg
proj_folder = typ_folder.svg
proj_note = mixed_document-note.svg
proj_section = mixed_document-section.svg
proj_scene = mixed_document-scene.svg
proj_stats = typ_chart-bar-grey.svg
proj_title = mixed_document-title.svg
reference = typ_at.svg
refresh = typ_refresh.svg
remove = typ_minus.svg
save = typ_download.svg
search = typ_search.svg
search_cancel = typ_cancel-grey.svg
search_case = nw_search-case.svg
@@ -0,0 +1,47 @@
<?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"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg2161">
<metadata
id="metadata2167">
<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="defs2165" />
<path
id="rect12986"
style="display:inline;opacity:0.95;fill:#ededed;fill-opacity:1;stroke:none;stroke-width:1.88635;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 5.8886719 3.4453125 C 5.2152275 3.4453125 4.6660156 3.9925712 4.6660156 4.6660156 L 4.6660156 19.333984 C 4.6660156 20.007428 5.2152275 20.554687 5.8886719 20.554688 L 18.111328 20.554688 C 18.784774 20.554688 19.333984 20.007428 19.333984 19.333984 L 19.333984 4.6660156 C 19.333984 3.9925712 18.784774 3.4453125 18.111328 3.4453125 L 5.8886719 3.4453125 z " />
<path
id="rect935"
style="fill:#787878;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
d="M 5.8886719 3.4453125 C 5.2152275 3.4453125 4.6660156 3.9925712 4.6660156 4.6660156 L 4.6660156 6 L 4.6660156 7.1894531 L 4.6660156 10.058594 L 19.333984 10.058594 L 19.333984 7.1894531 L 19.333984 6 L 19.333984 4.6660156 C 19.333984 3.9925712 18.784774 3.4453125 18.111328 3.4453125 L 5.8886719 3.4453125 z " />
<path
id="path2157"
style="display:inline;fill:#333333;fill-opacity:1;stroke-width:1.22222"
d="M 7.1113281,13.222656 C 6.7739949,13.222656 6.5,13.49665 6.5,13.833984 c 0,0.337332 0.2739949,0.611328 0.6113281,0.611328 h 9.7773439 c 0.337334,1e-6 0.611328,-0.273996 0.611328,-0.611328 0,-0.337334 -0.273994,-0.611328 -0.611328,-0.611328 z m 0,3.666016 C 6.7739949,16.888672 6.5,17.162666 6.5,17.5 c 0,0.337334 0.2739949,0.611328 0.6113281,0.611328 H 16.888672 C 17.226006,18.111328 17.5,17.837334 17.5,17.5 c 0,-0.337334 -0.273994,-0.611328 -0.611328,-0.611328 z" />
<path
id="path902"
style="display:inline;fill:#333333;fill-opacity:1;stroke-width:1.22222"
d="M 5.8886719,1 C 3.8671165,1 2.2226562,2.6444602 2.2226562,4.6660156 V 19.333984 C 2.2226563,21.355538 3.8671165,23 5.8886719,23 H 18.111328 c 2.021556,0 3.666016,-1.644462 3.666016,-3.666016 V 4.6660156 C 21.777344,2.6444602 20.132884,1 18.111328,1 Z m 0,2.4453125 H 18.111328 c 0.673446,0 1.222656,0.5472587 1.222656,1.2207031 V 19.333984 c 0,0.673444 -0.54921,1.220704 -1.222656,1.220704 H 5.8886719 c -0.6734444,-10e-7 -1.2226563,-0.54726 -1.2226563,-1.220704 V 4.6660156 c 0,-0.6734444 0.5492119,-1.2207031 1.2226563,-1.2207031 z" />
<path
id="path2157-0"
style="display:inline;fill:#ededed;fill-opacity:1;stroke-width:1.22222"
d="M 7.1113281 5.8886719 C 6.7739949 5.8886719 6.5 6.1626667 6.5 6.5 C 6.5 6.837334 6.7739949 7.1113281 7.1113281 7.1113281 L 16.888672 7.1113281 C 17.226006 7.1113281 17.5 6.837334 17.5 6.5 C 17.5 6.1626667 17.226006 5.8886719 16.888672 5.8886719 L 7.1113281 5.8886719 z " />
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -1,31 +0,0 @@
<?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="svg1897"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata1903">
<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="defs1901" />
<path
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
id="path1895"
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,37 +0,0 @@
<?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="svg1897"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata1903">
<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="defs1901" />
<path
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
id="path1895"
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
<circle
r="2.2222221"
cy="4.2222223"
cx="19.470242"
id="path2467"
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,43 +0,0 @@
<?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="svg1897"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata1903">
<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="defs1901" />
<path
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
id="path1895"
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
<circle
r="2.2222221"
cy="4.2222223"
cx="19.470242"
id="path2467"
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle2469"
cx="19.470242"
cy="9.4077778"
r="2.2222221" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

@@ -1,49 +0,0 @@
<?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="svg1897"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata1903">
<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="defs1901" />
<path
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
id="path1895"
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
<circle
r="2.2222221"
cy="4.2222223"
cx="19.470242"
id="path2467"
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle2469"
cx="19.470242"
cy="9.4077778"
r="2.2222221" />
<circle
r="2.2222221"
cy="14.592222"
cx="19.470242"
id="circle2471"
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,55 +0,0 @@
<?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="svg1897"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata1903">
<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="defs1901" />
<path
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
id="path1895"
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
<circle
r="2.2222221"
cy="4.2222223"
cx="19.470242"
id="path2467"
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle2469"
cx="19.470242"
cy="9.4077778"
r="2.2222221" />
<circle
r="2.2222221"
cy="14.592222"
cx="19.470242"
id="circle2471"
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle2473"
cx="19.470242"
cy="19.777779"
r="2.2222221" />
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

@@ -1,31 +0,0 @@
<?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="svg13156"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata13162">
<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="defs13160" />
<path
style="stroke-width:1.00151;fill:#c82829;fill-opacity:1"
id="path13154"
d="M 19.496214,4.4886395 H 9.4810665 c -1.2679176,0 -2.8382927,0.8082224 -3.5754075,1.8397825 L 3.2917056,9.9869555 2.0908895,11.668499 c -0.1211833,0.175265 -0.1221848,0.492744 0.003,0.665006 l 1.1897995,1.66652 2.622967,3.672554 c 0.7361133,1.030558 2.3054869,1.838782 3.574406,1.838782 H 19.496214 C 20.877302,19.511361 22,18.387661 22,17.007573 V 6.9924263 C 22,5.612339 20.877302,4.4886395 19.496214,4.4886395 Z M 17.19974,14.296473 c 0.391593,0.391592 0.391593,1.02455 0,1.416141 -0.195295,0.195296 -0.451683,0.293445 -0.70807,0.293445 -0.256388,0 -0.512776,-0.09815 -0.708071,-0.293445 l -2.296474,-2.296473 -2.296473,2.296473 c -0.195295,0.195296 -0.451683,0.293445 -0.708071,0.293445 -0.256388,0 -0.512775,-0.09815 -0.7080706,-0.293445 -0.3915923,-0.391591 -0.3915923,-1.024549 0,-1.416141 L 12.070984,12 9.7745104,9.7035265 c -0.3915923,-0.3915921 -0.3915923,-1.0245494 0,-1.4161417 0.3915926,-0.3915922 1.0245496,-0.3915922 1.4161416,0 l 2.296473,2.2964732 2.296474,-2.2964732 c 0.391592,-0.3915922 1.024549,-0.3915922 1.416141,0 0.391593,0.3915923 0.391593,1.0245496 0,1.4161417 L 14.903267,12 Z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

@@ -5,13 +5,13 @@
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="svg2608"
viewBox="0 0 24 24"
height="24"
version="1.2"
width="24"
version="1.2">
height="24"
viewBox="0 0 24 24"
id="svg901">
<metadata
id="metadata2614">
id="metadata907">
<rdf:RDF>
<cc:Work
rdf:about="">
@@ -23,9 +23,9 @@
</rdf:RDF>
</metadata>
<defs
id="defs2612" />
id="defs905" />
<path
style="stroke-width:1.05081;fill:#4271ae;fill-opacity:1"
id="path2606"
d="M 16.203226,2 H 7.796774 C 6.05874,2 4.6443545,3.4143855 4.6443545,5.1524195 V 19.86371 c 0,0.540115 0.1092839,0.994063 0.3236484,1.350287 0.5926549,0.982504 1.9072138,1.059213 2.9559187,0.0084 L 11.25708,17.889239 c 0.394052,-0.393002 1.091787,-0.393002 1.485841,0 l 3.333158,3.333158 C 16.592024,21.738349 17.128987,22 17.673304,22 c 0.837492,0 1.682342,-0.660957 1.682342,-2.13629 V 5.1524195 C 19.355646,3.4143855 17.941259,2 16.203226,2 Z M 7.796774,4.101613 h 8.406452 c 0.578994,0 1.050806,0.4718121 1.050806,1.0508065 V 15.560658 l -2.575527,-2.361162 c -1.477434,-1.35449 -3.880628,-1.353439 -5.3580615,0 l -2.574476,2.361162 V 5.1524195 c 0,-0.5789944 0.4718121,-1.0508065 1.0508065,-1.0508065 z m 6.431987,12.301792 C 13.635055,15.809699 12.843798,15.482898 12,15.482898 c -0.843797,0 -1.635054,0.327852 -2.2287605,0.920507 l -3.025272,3.025272 v -2.442075 l 3.2848215,-3.011611 c 1.085483,-0.995114 2.851889,-0.995114 3.937372,0 l 3.285871,3.011611 v 2.442075 z" />
d="M 16.203226,2 H 7.796774 C 6.05874,2 4.6443545,3.4143855 4.6443545,5.1524195 V 19.86371 c 0,0.540115 0.1092839,0.994063 0.3236484,1.350287 0.5926549,0.982504 1.9072138,1.059213 2.9559187,0.0084 L 11.25708,17.889239 c 0.394052,-0.393002 1.091787,-0.393002 1.485841,0 l 3.333158,3.333158 C 16.592024,21.738349 17.128987,22 17.673304,22 c 0.837492,0 1.682342,-0.660957 1.682342,-2.13629 V 5.1524195 C 19.355646,3.4143855 17.941259,2 16.203226,2 Z M 7.796774,4.101613 h 8.406452 c 0.578994,0 1.050806,0.4718121 1.050806,1.0508065 V 15.560658 l -2.575527,-2.361162 c -1.477434,-1.35449 -3.880628,-1.353439 -5.3580615,0 l -2.574476,2.361162 V 5.1524195 c 0,-0.5789944 0.4718121,-1.0508065 1.0508065,-1.0508065 z m 6.431987,12.301792 C 13.635055,15.809699 12.843798,15.482898 12,15.482898 c -0.843797,0 -1.635054,0.327852 -2.2287605,0.920507 l -3.025272,3.025272 v -2.442075 l 3.2848215,-3.011611 c 1.085483,-0.995114 2.851889,-0.995114 3.937372,0 l 3.285871,3.011611 v 2.442075 z"
id="path899"
style="fill:#4271ae;fill-opacity:1;stroke-width:1.05081" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,35 +0,0 @@
<?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"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg13801">
<metadata
id="metadata13807">
<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="defs13805" />
<path
style="display:inline;fill:#a2694f;fill-opacity:1"
id="path6288"
d="m 20.987,16 c 0,-0.105 -0.004,-0.211 -0.039,-0.316 l -2,-6 C 18.812,9.275 18.431,9 18,9 h -0.219 c -0.094,0.188 -0.21,0.368 -0.367,0.525 L 15.932,11 h 1.348 l 1.667,5 H 5.054 L 6.721,11 H 8.069 L 6.586,9.525 C 6.429,9.368 6.312,9.188 6.219,9 H 6 C 5.569,9 5.188,9.275 5.052,9.684 l -2,6 C 3.017,15.789 3.013,15.895 3.013,16 3,16 3,21 3,21 c 0,0.553 0.447,1 1,1 h 16 c 0.553,0 1,-0.447 1,-1 0,0 0,-5 -0.013,-5 z" />
<path
d="M 16.707,7.404 C 16.518,7.216 16.259,7.121 16,7.121 c -0.259,0 -0.518,0.095 -0.707,0.283 L 13,9.697 V 3 C 13,2.448 12.552,2 12,2 11.448,2 11,2.448 11,3 V 9.697 L 8.707,7.404 C 8.518,7.216 8.267,7.111 8,7.111 c -0.267,0 -0.518,0.105 -0.707,0.293 -0.39,0.39 -0.39,1.024 0,1.414 L 12,13.5 16.709,8.816 C 17.097,8.429 17.097,7.794 16.707,7.404 Z"
id="path13799"
style="display:inline;fill:#718c00;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

@@ -1,31 +0,0 @@
<?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="svg3369"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata3375">
<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="defs3373" />
<path
style="fill:#333333;fill-opacity:1;stroke-width:1"
id="path3367"
d="m 10.828125,20 h -4 c -0.553,0 -1,0.447 -1,1 0,0.553 0.447,1 1,1 h 10 c 0.553,0 1,-0.447 1,-1 0,-0.553 -0.447,-1 -1,-1 h -4 v -1.23 c 1.64,-0.371 3.146,-1.188 4.363,-2.406 1.7,-1.7 2.637,-3.96 2.637,-6.364 0,-2.067 -0.692,-4.029 -1.968,-5.619 l 0.675,-0.673 c 0.391,-0.391 0.391,-1.023 10e-4,-1.415 -0.391,-0.391 -1.024,-0.39 -1.415,-0.001 l -2.052,2.049 0.708,0.708 c 1.322,1.322 2.051,3.081 2.051,4.951 0,1.87 -0.729,3.627 -2.051,4.949 -1.322,1.322 -3.079,2.051 -4.949,2.051 -1.87,0 -3.627,-0.729 -4.949,-2.051 -0.391,-0.391 -1.023,-0.391 -1.414,0 -0.391,0.39 -0.391,1.023 0,1.414 1.699,1.7 3.959,2.637 6.363,2.637 z m 0,-16 c 1.657,0 3.157,0.672 4.243,1.757 1.085,1.086 1.757,2.586 1.757,4.243 0,1.656 -0.672,3.156 -1.757,4.242 -1.086,1.086 -2.586,1.758 -4.243,1.758 -1.658,0 -3.157,-0.672 -4.242,-1.757 -1.085,-1.086 -1.756,-2.586 -1.756,-4.243 0,-1.657 0.671,-3.157 1.756,-4.243 1.085,-1.085 2.584,-1.757 4.242,-1.757 z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,31 +0,0 @@
<?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"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg5204">
<metadata
id="metadata5210">
<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="defs5208" />
<path
id="rect928"
d="M 7.75 2 C 6.7805 2 6 2.7805 6 3.75 L 6 5.5 L 4.1621094 5.5 C 3.2410844 5.5 2.5 6.2805 2.5 7.25 C 2.5 8.2195 3.2410844 9 4.1621094 9 L 6 9 L 6 15 L 4.1621094 15 C 3.2410844 15 2.5 15.7805 2.5 16.75 C 2.5 17.7195 3.2410844 18.5 4.1621094 18.5 L 6 18.5 L 6 20.25 C 6 21.2195 6.7805 22 7.75 22 C 8.7195 22 9.5 21.2195 9.5 20.25 L 9.5 18.5 L 14.5 18.5 L 14.5 20.25 C 14.5 21.2195 15.2805 22 16.25 22 C 17.2195 22 18 21.2195 18 20.25 L 18 18.5 L 19.837891 18.5 C 20.758916 18.5 21.5 17.7195 21.5 16.75 C 21.5 15.7805 20.758916 15 19.837891 15 L 18 15 L 18 9 L 19.837891 9 C 20.758916 9 21.5 8.2195 21.5 7.25 C 21.5 6.2805 20.758916 5.5 19.837891 5.5 L 18 5.5 L 18 3.75 C 18 2.7805 17.2195 2 16.25 2 C 15.2805 2 14.5 2.7805 14.5 3.75 L 14.5 5.5 L 9.5 5.5 L 9.5 3.75 C 9.5 2.7805 8.7195 2 7.75 2 z M 9.5 9 L 14.5 9 L 14.5 15 L 9.5 15 L 9.5 9 z "
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.7544229;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,35 +0,0 @@
<?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"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg11367">
<metadata
id="metadata11373">
<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="defs11371" />
<path
d="m 13.499143,15.642429 c -0.45,0 -0.885857,-0.178715 -1.209857,-0.502715 L 8.859,11.710714 c -0.668571,-0.669857 -0.668571,-1.755 0,-2.423571 0.669857,-0.669857 1.755,-0.669857 2.426143,0 L 13.105714,11.109 17.573571,4.0954286 c 0.459,-0.828 1.504286,-1.1275715 2.332286,-0.6672857 0.826714,0.4602857 1.125,1.5042857 0.666,2.331 l -5.572286,9.0000001 c -0.261,0.470571 -0.727714,0.790714 -1.26,0.865286 z"
id="path11365"
style="display:inline;fill:#718c00;fill-opacity:1;stroke-width:1.28570998" />
<path
style="display:inline;fill:#333333;fill-opacity:1;stroke-width:1.28570998"
id="path16196"
d="M 17.142857,21 H 6.8571429 C 4.7305714,21 3,19.269429 3,17.142857 V 6.8571429 C 3,4.7305714 4.7305714,3 6.8571429,3 h 6.4285711 c 0.711,0 1.285715,0.576 1.285715,1.2857143 0,0.7097143 -0.574715,1.2857143 -1.285715,1.2857143 H 6.8571429 c -0.7097143,0 -1.2857143,0.5772857 -1.2857143,1.2857143 V 17.142857 c 0,0.708429 0.576,1.285714 1.2857143,1.285714 H 17.142857 c 0.709714,0 1.285714,-0.577285 1.285714,-1.285714 V 13.285714 C 18.428571,12.576 19.003286,12 19.714286,12 20.425286,12 21,12.576 21,13.285714 v 3.857143 C 21,19.269429 19.269429,21 17.142857,21 Z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

@@ -1,36 +0,0 @@
<?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"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg7030">
<metadata
id="metadata7036">
<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="defs7034" />
<g
id="g7028"
transform="matrix(0.95238095,0,0,0.95238095,0.5714286,0.09523812)"
style="fill:#333333;fill-opacity:1">
<path
d="M 19.414,8.902 C 19.518,8.854 19.62,8.794 19.707,8.707 l 0.5,-0.5 c 0.391,-0.391 0.391,-1.023 0,-1.414 -0.391,-0.391 -1.023,-0.391 -1.414,0 l -0.5,0.5 -0.115,0.173 C 16.791,6.154 14.99,5.276 12.989,5.056 L 13,5 V 4 h 1 C 14.55,4 15,3.55 15,3 15,2.45 14.55,2 14,2 H 10 C 9.45,2 9,2.45 9,3 9,3.55 9.45,4 10,4 h 1 v 1 l 0.012,0.057 C 6.506,5.549 3,9.364 3,14 c 0,4.971 4.029,9 9,9 4.971,0 9,-4.029 9,-9 0,-1.894 -0.588,-3.648 -1.586,-5.098 z M 12,21 C 8.141,21 5,17.86 5,14 5,10.14 8.141,7 12,7 c 3.859,0 7,3.14 7,7 0,3.86 -3.141,7 -7,7 z m 1,-8 v -2 c 0,-0.55 -0.45,-1 -1,-1 -0.55,0 -1,0.45 -1,1 v 3 c 0,0.55 0.45,1 1,1 h 3 c 0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1 z M 12,8 c -3.312,0 -6,2.688 -6,6 0,3.312 2.688,6 6,6 3.312,0 6,-2.688 6,-6 0,-3.312 -2.688,-6 -6,-6 z m 0,11 C 9.243,19 7,16.757 7,14 7,11.243 9.243,9 12,9 c 2.757,0 5,2.243 5,5 0,2.757 -2.243,5 -5,5 z"
id="path7026"
style="fill:#333333;fill-opacity:1" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

+4 -4
View File
@@ -2,11 +2,11 @@
<html>
<body>
<h2>Release Notes for 1.7 Beta 1</h2>
<p><i>Released on 17 May 2022</i></p>
<h2>Release Notes for 2.0 RC 1</h2>
<p><i>Released on 17 October 2022</i></p>
<p>This is a beta release of the next release version, and is intended for testing purposes. Please
be careful when using this version on live writing projects, and make sure you take frequent
<p>This is a release candidate of the next release version, and is intended for testing purposes.
Please be careful when using this version on live writing projects, and make sure you take frequent
backups.</p>
<p>Please check the changelog for an overview of changes. The full release notes will be added to
the final release.</p>
+133 -151
View File
@@ -35,7 +35,7 @@ from PyQt5.QtCore import QCoreApplication
from PyQt5.QtWidgets import qApp
from novelwriter.enum import nwItemClass, nwItemType, nwItemLayout
from novelwriter.error import logException
from novelwriter.error import formatException, logException
from novelwriter.constants import nwConst, nwUnicode
logger = logging.getLogger(__name__)
@@ -45,52 +45,54 @@ logger = logging.getLogger(__name__)
# Checker Functions
# =============================================================================================== #
def checkString(value, default, allowNone=False):
def checkStringNone(value, default):
"""Check if a variable is a string or a None.
"""
if allowNone and (value is None or value == "None"):
if value is None or value == "None":
return None
if isinstance(value, str):
return str(value)
return default
def checkInt(value, default, allowNone=False):
"""Check if a variable is an integer or a None.
def checkString(value, default):
"""Check if a variable is a string.
"""
if isinstance(value, str):
return str(value)
return default
def checkInt(value, default):
"""Check if a variable is an integer.
"""
if allowNone and (value is None or value == "None"):
return None
try:
return int(value)
except Exception:
return default
def checkFloat(value, default, allowNone=False):
"""Check if a variable is a float or a None.
def checkFloat(value, default):
"""Check if a variable is a float.
"""
if allowNone and (value is None or value == "None"):
return None
try:
return float(value)
except Exception:
return default
def checkBool(value, default, allowNone=False):
"""Check if a variable is a boolean or a None.
def checkBool(value, default):
"""Check if a variable is a boolean.
"""
if allowNone and (value is None or value == "None"):
return None
if isinstance(value, str):
if isinstance(value, bool):
return value
elif isinstance(value, str):
if value == "True":
return True
elif value == "False":
return False
else:
return default
elif isinstance(value, int):
if value == 1:
return True
@@ -98,7 +100,6 @@ def checkBool(value, default, allowNone=False):
return False
else:
return default
return default
@@ -174,16 +175,6 @@ def hexToInt(value, default=0):
return default
def checkIntRange(value, first, last, default):
"""Check that an int is in a given range. If it isn't, return the
default value.
"""
if isinstance(value, int):
if value >= first and value <= last:
return value
return default
def minmax(value, minVal, maxVal):
"""Make sure an integer is between min and max value (inclusive).
"""
@@ -225,25 +216,25 @@ def formatInt(value):
return str(value)
def formatTimeStamp(theTime, fileSafe=False):
def formatTimeStamp(value, fileSafe=False):
"""Take a number (on the format returned by time.time()) and convert
it to a timestamp string.
"""
if fileSafe:
return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_FSTAMP)
return datetime.fromtimestamp(value).strftime(nwConst.FMT_FSTAMP)
else:
return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_TSTAMP)
return datetime.fromtimestamp(value).strftime(nwConst.FMT_TSTAMP)
def formatTime(tS):
def formatTime(t):
"""Format a time in seconds in HH:MM:SS format or d-HH:MM:SS format
if a full day or longer.
"""
if isinstance(tS, int):
if tS >= 86400:
return f"{tS//86400:d}-{tS%86400//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}"
if isinstance(t, int):
if t >= 86400:
return f"{t//86400:d}-{t%86400//3600:02d}:{t%3600//60:02d}:{t%60:02d}"
else:
return f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}"
return f"{t//3600:02d}:{t%3600//60:02d}:{t%60:02d}"
return "ERROR"
@@ -263,7 +254,7 @@ def splitVersionNumber(value):
and patch, and computes an integer value aabbcc.
"""
if not isinstance(value, str):
return [0, 0, 0, 0]
return 0, 0, 0, 0
vMajor = 0
vMinor = 0
@@ -282,114 +273,114 @@ def splitVersionNumber(value):
vInt = vMajor*10000 + vMinor*100 + vPatch
return [vMajor, vMinor, vPatch, vInt]
return vMajor, vMinor, vPatch, vInt
def transferCase(theSource, theTarget):
def transferCase(source, target):
"""Transfers the case of the source word to the target word. This
will consider all upper or lower, and first char capitalisation.
"""
theResult = theTarget
theResult = target
if not isinstance(theSource, str) or not isinstance(theTarget, str):
if not isinstance(source, str) or not isinstance(target, str):
return theResult
if len(theTarget) < 1 or len(theSource) < 1:
if len(target) < 1 or len(source) < 1:
return theResult
if theSource.istitle():
theResult = theTarget.title()
if source.istitle():
theResult = target.title()
if theSource.isupper():
theResult = theTarget.upper()
elif theSource.islower():
theResult = theTarget.lower()
if source.isupper():
theResult = target.upper()
elif source.islower():
theResult = target.lower()
return theResult
def fuzzyTime(secDiff):
def fuzzyTime(seconds):
"""Converts a time difference in seconds into a fuzzy time string.
"""
if secDiff < 0:
if seconds < 0:
return QCoreApplication.translate(
"Common", "in the future"
)
elif secDiff < 30:
elif seconds < 30:
return QCoreApplication.translate(
"Common", "just now"
)
elif secDiff < 90:
elif seconds < 90:
return QCoreApplication.translate(
"Common", "a minute ago"
)
elif secDiff < 3300: # 55 minutes
elif seconds < 3300: # 55 minutes
return QCoreApplication.translate(
"Common", "{0} minutes ago"
).format(int(round(secDiff/60)))
elif secDiff < 5400: # 90 minutes
).format(int(round(seconds/60)))
elif seconds < 5400: # 90 minutes
return QCoreApplication.translate(
"Common", "an hour ago"
)
elif secDiff < 84600: # 23.5 hours
elif seconds < 84600: # 23.5 hours
return QCoreApplication.translate(
"Common", "{0} hours ago"
).format(int(round(secDiff/3600)))
elif secDiff < 129600: # 1.5 days
).format(int(round(seconds/3600)))
elif seconds < 129600: # 1.5 days
return QCoreApplication.translate(
"Common", "a day ago"
)
elif secDiff < 561600: # 6.5 days
elif seconds < 561600: # 6.5 days
return QCoreApplication.translate(
"Common", "{0} days ago"
).format(int(round(secDiff/86400)))
elif secDiff < 907200: # 10.5 days
).format(int(round(seconds/86400)))
elif seconds < 907200: # 10.5 days
return QCoreApplication.translate(
"Common", "a week ago"
)
elif secDiff < 2419200: # 28 days
elif seconds < 2419200: # 28 days
return QCoreApplication.translate(
"Common", "{0} weeks ago"
).format(int(round(secDiff/604800)))
elif secDiff < 3888000: # 45 days
).format(int(round(seconds/604800)))
elif seconds < 3888000: # 45 days
return QCoreApplication.translate(
"Common", "a month ago"
)
elif secDiff < 29808000: # 345 days
elif seconds < 29808000: # 345 days
return QCoreApplication.translate(
"Common", "{0} months ago"
).format(int(round(secDiff/2592000)))
elif secDiff < 47336400: # 1.5 years
).format(int(round(seconds/2592000)))
elif seconds < 47336400: # 1.5 years
return QCoreApplication.translate(
"Common", "a year ago"
)
else:
return QCoreApplication.translate(
"Common", "{0} years ago"
).format(int(round(secDiff/31557600)))
).format(int(round(seconds/31557600)))
def numberToRoman(numVal, toLower=False):
def numberToRoman(value, toLower=False):
"""Convert an integer to a Roman number.
"""
if not isinstance(numVal, int):
if not isinstance(value, int):
return "NAN"
if numVal < 1 or numVal > 4999:
if value < 1 or value > 4999:
return "OOR"
theValues = [
lookup = [
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"),
(50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
]
romNum = ""
for theDiv, theSym in theValues:
n = numVal//theDiv
romNum += n*theSym
numVal -= n*theDiv
if numVal <= 0:
roman = ""
for divisor, symbol in lookup:
n = value//divisor
roman += n*symbol
value -= n*divisor
if value <= 0:
break
return romNum.lower() if toLower else romNum
return roman.lower() if toLower else roman
# =============================================================================================== #
@@ -447,51 +438,70 @@ def jsonEncode(data, n=0, nmax=0):
# File and File System Functions
# =============================================================================================== #
def readTextFile(filePath):
def readTextFile(path):
"""Read the content of a text file in a robust manner.
"""
if not os.path.isfile(filePath):
if not os.path.isfile(path):
return ""
fileText = ""
text = ""
try:
with open(filePath, mode="r", encoding="utf-8") as inFile:
fileText = inFile.read()
with open(path, mode="r", encoding="utf-8") as inFile:
text = inFile.read()
except Exception:
logger.error("Could not read file: %s", filePath)
logger.error("Could not read file: %s", path)
logException()
return ""
return fileText
return text
def makeFileNameSafe(value):
"""Returns a filename safe string of the value.
"""
cleanName = ""
clean = ""
for c in str(value).strip():
if c.isalpha() or c.isdigit() or c == " ":
cleanName += c
return cleanName
clean += c
return clean
def sha256sum(filePath):
def ensureFolder(path, parent=None, errLog=None):
"""Make sure a folder exists, and if it doesn't, create it.
"""
try:
if parent:
path = os.path.join(parent, path)
if not os.path.isdir(path):
os.mkdir(path)
except Exception as exc:
logger.error("Could not create folder: %s", path)
logException()
if isinstance(errLog, list):
errLog.append(f"Could not create folder: {path}")
errLog.append(formatException(exc))
return False
return True
def sha256sum(path):
"""Make a shasum of a file using a buffer.
Based on: https://stackoverflow.com/a/44873382/5825851
"""
hDigest = hashlib.sha256()
digest = hashlib.sha256()
bData = bytearray(65536)
mData = memoryview(bData)
try:
with open(filePath, mode="rb", buffering=0) as inFile:
with open(path, mode="rb", buffering=0) as inFile:
for n in iter(lambda: inFile.readinto(mData), 0):
hDigest.update(mData[:n])
digest.update(mData[:n])
except Exception:
logger.error("Could not create sha256sum of: %s", filePath)
logger.error("Could not create sha256sum of: %s", path)
logException()
return None
return hDigest.hexdigest()
return digest.hexdigest()
# =============================================================================================== #
@@ -513,87 +523,59 @@ def getGuiItem(objName):
class NWConfigParser(ConfigParser):
CNF_STR = 0
CNF_INT = 1
CNF_FLOAT = 2
CNF_BOOL = 3
CNF_S_LST = 4
CNF_I_LST = 5
def __init__(self):
super().__init__()
def rdStr(self, section, option, default):
"""Read string value.
"""
return self._parseLine(section, option, default, self.CNF_STR)
return self.get(section, option, fallback=default)
def rdInt(self, section, option, default):
"""Read integer value.
"""
return self._parseLine(section, option, default, self.CNF_INT)
try:
return self.getint(section, option, fallback=default)
except ValueError:
logger.error("Could not read '%s':'%s' from config", section, option)
return default
def rdFlt(self, section, option, default):
"""Read float value.
"""
return self._parseLine(section, option, default, self.CNF_FLOAT)
try:
return self.getfloat(section, option, fallback=default)
except ValueError:
logger.error("Could not read '%s':'%s' from config", section, option)
return default
def rdBool(self, section, option, default):
"""Read boolean value.
"""
return self._parseLine(section, option, default, self.CNF_BOOL)
try:
return self.getboolean(section, option, fallback=default)
except ValueError:
logger.error("Could not read '%s':'%s' from config", section, option)
return default
def rdStrList(self, section, option, default):
"""Read string list.
"""
return self._parseLine(section, option, default, self.CNF_S_LST)
result = default.copy() if isinstance(default, list) else []
if self.has_option(section, option):
data = self.get(section, option, fallback="").split(",")
for i in range(min(len(data), len(result))):
result[i] = data[i].strip()
return result
def rdIntList(self, section, option, default):
"""Read integer list.
"""
return self._parseLine(section, option, default, self.CNF_I_LST)
##
# Internal Functions
##
def _unpackList(self, value, default, type):
"""Unpack a comma-separated string of items into a list.
"""
inList = value.split(",")
outList = []
if isinstance(default, list):
outList = default.copy()
for i in range(min(len(inList), len(outList))):
try:
if type == self.CNF_S_LST:
outList[i] = inList[i].strip()
elif type == self.CNF_I_LST:
outList[i] = int(inList[i].strip())
except Exception:
continue
return outList
def _parseLine(self, section, option, default, type):
"""Parse a line and return the correct datatype.
"""
result = default.copy() if isinstance(default, list) else []
if self.has_option(section, option):
try:
if type == self.CNF_STR:
return self.get(section, option)
elif type == self.CNF_INT:
return self.getint(section, option)
elif type == self.CNF_FLOAT:
return self.getfloat(section, option)
elif type == self.CNF_BOOL:
return self.getboolean(section, option)
elif type in (self.CNF_I_LST, self.CNF_S_LST):
return self._unpackList(self.get(section, option), default, type)
except ValueError:
logger.error("Could not read '%s':'%s' from config", str(section), str(option))
logException()
return default
return default
data = self.get(section, option, fallback="").split(",")
for i in range(min(len(data), len(result))):
result[i] = checkInt(data[i].strip(), result[i])
return result
# END Class NWConfigParser
+47 -106
View File
@@ -37,7 +37,7 @@ from PyQt5.QtCore import (
)
from novelwriter.error import logException, formatException
from novelwriter.common import splitVersionNumber, formatTimeStamp, NWConfigParser
from novelwriter.common import ensureFolder, splitVersionNumber, formatTimeStamp, NWConfigParser
from novelwriter.constants import nwFiles, nwUnicode
logger = logging.getLogger(__name__)
@@ -52,25 +52,22 @@ class Config:
# Set Application Variables
self.appName = "novelWriter"
self.appHandle = self.appName.lower()
# Config Error Handling
self.hasError = False # True if the config class encountered an error
self.errData = [] # List of error messages
self.appHandle = "novelwriter"
# Set Paths
self.cmdOpen = None # Path from command line for project to be opened on launch
self.confPath = None # Folder where the config is saved
self.confFile = None # The config file name
self.dataPath = None # Folder where app data is stored
self.lastPath = None # The last user-selected folder (browse dialogs)
self.appPath = None # The full path to the novelwriter package folder
self.appRoot = None # The full path to the novelwriter root folder
self.appIcon = None # The full path to the novelwriter icon file
self.assetPath = None # The full path to the novelwriter/assets folder
self.pdfDocs = None # The location of the PDF manual, if it exists
self.cmdOpen = None # Path from command line for project to be opened on launch
self.confPath = None # Folder where the config is saved
self.dataPath = None # Folder where app data is stored
self.lastPath = None # The last user-selected folder (browse dialogs)
self.appPath = None # The full path to the novelwriter package folder
self.appRoot = None # The full path to the novelwriter root folder
self.appIcon = None # The full path to the novelwriter icon file
self.assetPath = None # The full path to the novelwriter/assets folder
self.pdfDocs = None # The location of the PDF manual, if it exists
# Runtime Settings and Variables
self.hasError = False # True if the config class encountered an error
self.errData = [] # List of error messages
self.confChanged = False # True whenever the config has chenged, false after save
# General
@@ -94,14 +91,14 @@ class Config:
self.qtTrans = {}
# Sizes
self.winGeometry = [1200, 650]
self.prefGeometry = [700, 615]
self.projColWidth = [200, 60, 140]
self.mainPanePos = [300, 800]
self.docPanePos = [400, 400]
self.viewPanePos = [500, 150]
self.outlnPanePos = [500, 150]
self.isFullScreen = False
self.winGeometry = [1200, 650]
self.prefGeometry = [700, 615]
self.projColWidth = [200, 60, 140]
self.mainPanePos = [300, 800]
self.docPanePos = [400, 400]
self.viewPanePos = [500, 150]
self.outlnPanePos = [500, 150]
self.isFullScreen = False
# Features
self.hideVScroll = False # Hide vertical scroll bars on main widgets
@@ -220,8 +217,8 @@ class Config:
self.osUnknown = True
# Other System Info
self.hostName = "Unknown"
self.kernelVer = "Unknown"
self.hostName = QSysInfo.machineHostName()
self.kernelVer = QSysInfo.kernelVersion()
# Packages
self.hasEnchant = False # The pyenchant package
@@ -264,31 +261,15 @@ class Config:
self.confPath = confPath
if dataPath is None:
if self.verQtValue >= 50400:
dataRoot = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
else:
dataRoot = QStandardPaths.writableLocation(QStandardPaths.DataLocation)
dataRoot = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation)
self.dataPath = os.path.join(os.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)
logger.debug("Config path: %s", self.confPath)
logger.debug("Data path: %s", self.dataPath)
# Check Data Path Subdirs
dataDirs = ["syntax", "themes"]
for dataDir in dataDirs:
dirPath = os.path.join(self.dataPath, dataDir)
if not os.path.isdir(dirPath):
try:
os.mkdir(dirPath)
logger.info("Created folder: %s", dirPath)
except Exception:
logger.error("Could not create folder: %s", dirPath)
logException()
self.confFile = self.appHandle+".conf"
self.lastPath = os.path.expanduser("~")
self.appPath = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__)))
self.appRoot = os.path.abspath(os.path.join(self.appPath, os.path.pardir))
@@ -308,50 +289,33 @@ class Config:
self.nwLangPath = os.path.join(self.assetPath, "i18n")
logger.debug("Assets: %s", self.assetPath)
logger.verbose("App path: %s", self.appPath)
logger.verbose("Last path: %s", self.lastPath)
logger.debug("App path: %s", self.appPath)
logger.debug("Last path: %s", self.lastPath)
# If the config folder does not exist, create it.
# This assumes that the os config folder itself exists.
if not os.path.isdir(self.confPath):
try:
os.mkdir(self.confPath)
except Exception as exc:
logger.error("Could not create folder: %s", self.confPath)
logException()
self.hasError = True
self.errData.append("Could not create folder: %s" % self.confPath)
self.errData.append(formatException(exc))
self.confPath = None
# If the config and data folders don't not exist, create them
# This assumes that the os config and data folders exist
if not ensureFolder(self.confPath, errLog=self.errData):
self.hasError = True
self.confPath = None
if not ensureFolder(self.dataPath, errLog=self.errData):
self.hasError = True
self.dataPath = None
# We don't error on these failing since they are not essential
if self.dataPath is not None:
ensureFolder("syntax", parent=self.dataPath)
ensureFolder("themes", parent=self.dataPath)
# Check if config file exists
if self.confPath is not None:
if os.path.isfile(os.path.join(self.confPath, self.confFile)):
if os.path.isfile(os.path.join(self.confPath, nwFiles.CONF_FILE)):
# If it exists, load it
self.loadConfig()
else:
# If it does not exist, save a copy of the default values
self.saveConfig()
# If the data folder does not exist, create it.
# This assumes that the os data folder itself exists.
if self.dataPath is not None:
if not os.path.isdir(self.dataPath):
try:
os.mkdir(self.dataPath)
except Exception as exc:
logger.error("Could not create folder: %s", self.dataPath)
logException()
self.hasError = True
self.errData.append("Could not create folder: %s" % self.dataPath)
self.errData.append(formatException(exc))
self.dataPath = None
# Host and Kernel
if self.verQtValue >= 50600:
self.hostName = QSysInfo.machineHostName()
self.kernelVer = QSysInfo.kernelVersion()
# Load recent projects cache
self.loadRecentCache()
@@ -397,7 +361,7 @@ class Config:
def listLanguages(self, lngSet):
"""List localisation files in the i18n folder. The default GUI
language 'en_GB' is British English.
language is British English (en_GB).
"""
if lngSet == self.LANG_NW:
fPre = "nw_"
@@ -430,7 +394,7 @@ class Config:
return False
theConf = NWConfigParser()
cnfPath = os.path.join(self.confPath, self.confFile)
cnfPath = os.path.join(self.confPath, nwFiles.CONF_FILE)
try:
with open(cnfPath, mode="r", encoding="utf-8") as inFile:
theConf.read_file(inFile)
@@ -653,7 +617,7 @@ class Config:
}
# Write config file
cnfPath = os.path.join(self.confPath, self.confFile)
cnfPath = os.path.join(self.confPath, nwFiles.CONF_FILE)
try:
with open(cnfPath, mode="w", encoding="utf-8") as outFile:
theConf.write(outFile)
@@ -738,7 +702,7 @@ class Config:
"""
if thePath in self.recentProj:
del self.recentProj[thePath]
logger.verbose("Removed recent: %s", thePath)
logger.debug("Removed recent: %s", thePath)
self.saveRecentCache()
else:
logger.error("Unknown recent: %s", thePath)
@@ -749,29 +713,6 @@ class Config:
# Setters
##
def setConfPath(self, newPath):
"""Set the path and filename to the config file.
"""
if newPath is None:
return True
if not os.path.isfile(newPath):
logger.error("File not found, using default config path instead")
return False
self.confPath = os.path.dirname(newPath)
self.confFile = os.path.basename(newPath)
return True
def setDataPath(self, newPath):
"""Set the data path.
"""
if newPath is None:
return True
if not os.path.isdir(newPath):
logger.error("Path not found, using default data path instead")
return False
self.dataPath = os.path.abspath(newPath)
return True
def setLastPath(self, lastPath):
"""Set the last used path (by the user).
"""
+2
View File
@@ -68,6 +68,7 @@ class nwHeaders:
class nwFiles:
CONF_FILE = "novelwriter.conf"
PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt"
PROJ_LOCK = "nwProject.lock"
@@ -157,6 +158,7 @@ class nwLabels:
"doc_h1": QT_TRANSLATE_NOOP("Constant", "Novel Title Page"),
"doc_h2": QT_TRANSLATE_NOOP("Constant", "Novel Chapter"),
"doc_h3": QT_TRANSLATE_NOOP("Constant", "Novel Scene"),
"doc_h4": QT_TRANSLATE_NOOP("Constant", "Novel Section"),
"note": QT_TRANSLATE_NOOP("Constant", "Project Note"),
}
KEY_NAME = {
+3
View File
@@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from novelwriter.core.doctools import DocMerger, DocSplitter
from novelwriter.core.document import NWDoc
from novelwriter.core.index import countWords
from novelwriter.core.project import NWProject
@@ -28,6 +29,8 @@ from novelwriter.core.toodt import ToOdt
from novelwriter.core.tomd import ToMarkdown
__all__ = [
"DocMerger",
"DocSplitter",
"countWords",
"NWDoc",
"NWProject",
+240
View File
@@ -0,0 +1,240 @@
"""
novelWriter Project Document Tools
====================================
A collection of tools to create and manipulate documents
File History:
Created: 2022-10-02 [2.0b1] DocMerger
Created: 2022-10-11 [2.0b1] DocSplitter
This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
from novelwriter.common import minmax
from novelwriter.core.document import NWDoc
logger = logging.getLogger(__name__)
class DocMerger:
def __init__(self, theProject):
self.theProject = theProject
self._error = ""
self._targetDoc = None
self._targetText = []
return
##
# Methods
##
def getError(self):
"""Return any collected errors.
"""
return self._error
def setTargetDoc(self, tHandle):
"""Set the target document for the merging. Calling this
function resets the class.
"""
self._targetDoc = tHandle
self._targetText = []
return
def newTargetDoc(self, srcHandle, docLabel):
"""Create a barnd new target document based on a source handle
and a new doc label. Calling this function resets the class.
"""
srcItem = self.theProject.tree[srcHandle]
if srcItem is None:
return None
newHandle = self.theProject.newFile(docLabel, srcItem.itemParent)
newItem = self.theProject.tree[newHandle]
newItem.setLayout(srcItem.itemLayout)
newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport)
self._targetDoc = newHandle
self._targetText = []
return newHandle
def appendText(self, srcHandle, addComment, cmtPrefix):
"""Append text from an existing document to the text buffer.
"""
srcItem = self.theProject.tree[srcHandle]
if srcItem is None:
return False
inDoc = NWDoc(self.theProject, srcHandle)
docText = (inDoc.readDocument() or "").rstrip("\n")
if addComment:
docInfo = srcItem.describeMe()
docSt, _ = srcItem.getImportStatus(incIcon=False)
cmtLine = f"% {cmtPrefix} {docInfo}: {srcItem.itemName} [{docSt}]\n\n"
docText = cmtLine + docText
self._targetText.append(docText)
return True
def writeTargetDoc(self):
"""Write the accumulated text into the designated target
document, appending any existing text.
"""
if self._targetDoc is None:
return False
outDoc = NWDoc(self.theProject, self._targetDoc)
docText = (outDoc.readDocument() or "").rstrip("\n")
if docText:
self._targetText.insert(0, docText)
status = outDoc.writeDocument("\n\n".join(self._targetText) + "\n\n")
if not status:
self._error = outDoc.getError()
return status
# END Class DocMerger
class DocSplitter:
def __init__(self, theProject, sHandle):
self.theProject = theProject
self._error = ""
self._parHandle = None
self._srcHandle = None
self._srcItem = None
self._inFolder = False
self._rawData = []
srcItem = self.theProject.tree[sHandle]
if srcItem is not None and srcItem.isFileType():
self._srcHandle = sHandle
self._srcItem = srcItem
return
##
# Methods
##
def getError(self):
"""Return any collected errors.
"""
return self._error
def setParentItem(self, pHandle):
"""Set the item that will be the top level parent item for the
new documents.
"""
self._parHandle = pHandle
self._inFolder = False
return
def newParentFolder(self, pHandle, folderLabel):
"""Create a new folder that will be the top level parent item
for the new documents.
"""
if self._srcItem is None:
return None
newHandle = self.theProject.newFolder(folderLabel, pHandle)
newItem = self.theProject.tree[newHandle]
newItem.setStatus(self._srcItem.itemStatus)
newItem.setImport(self._srcItem.itemImport)
self._parHandle = newHandle
self._inFolder = True
return newHandle
def splitDocument(self, splitData, splitText):
"""Loop through the split data record and perform the split job.
"""
self._rawData = []
buffer = splitText.copy()
for lineNo, hLevel, hLabel in reversed(splitData):
chunk = buffer[lineNo:]
buffer = buffer[:lineNo]
self._rawData.insert(0, (chunk, hLevel, hLabel))
return True
def writeDocuments(self, docHierarchy):
"""An iterator that will write each document in the buffer, and
return its new handle, parent handle, and sibling handle.
"""
if self._srcHandle is None:
return
pHandle = self._parHandle
nHandle = self._parHandle if self._inFolder else self._srcHandle
hHandle = [self._parHandle, None, None, None, None]
pLevel = 0
for docText, hLevel, docLabel in self._rawData:
hLevel = minmax(hLevel, 1, 4)
if pLevel == 0:
pLevel = hLevel
if docHierarchy:
if hLevel == 1:
pHandle = self._parHandle
elif hLevel == 2:
pHandle = hHandle[1] or hHandle[0]
elif hLevel == 3:
pHandle = hHandle[2] or hHandle[1] or hHandle[0]
elif hLevel == 4:
pHandle = hHandle[3] or hHandle[2] or hHandle[1] or hHandle[0]
if hLevel < pLevel:
nHandle = hHandle[hLevel] or hHandle[0]
elif hLevel > pLevel:
nHandle = pHandle
dHandle = self.theProject.newFile(docLabel, pHandle)
hHandle[hLevel] = dHandle
outDoc = NWDoc(self.theProject, dHandle)
status = outDoc.writeDocument("\n".join(docText))
if not status:
self._error = outDoc.getError()
yield status, dHandle, nHandle
hHandle[hLevel] = dHandle
nHandle = dHandle
pLevel = hLevel
return
# END Class DocSplitter
+1 -1
View File
@@ -33,7 +33,7 @@ from novelwriter.common import isHandle, sha256sum
logger = logging.getLogger(__name__)
class NWDoc():
class NWDoc:
def __init__(self, theProject, theHandle):
+69 -74
View File
@@ -176,7 +176,7 @@ class NWIndex:
self._indexChange = round(time())
logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000)
logger.debug("Index loaded in %.3f ms", (time() - tStart)*1000)
return True
@@ -202,7 +202,7 @@ class NWIndex:
logException()
return False
logger.verbose("Index saved in %.3f ms", (time() - tStart)*1000)
logger.debug("Index saved in %.3f ms", (time() - tStart)*1000)
return True
@@ -243,20 +243,43 @@ class NWIndex:
if theItem.itemParent is None:
logger.info("Not indexing orphaned item '%s'", tHandle)
return False
if theItem.isInactive():
logger.debug("Not indexing inactive item '%s'", tHandle)
return False
logger.debug("Indexing item with handle '%s'", tHandle)
if theItem.isInactive():
self._scanInactive(theItem, theText)
else:
self._scanActive(tHandle, theItem, theText, itemTags)
# Scan the text content
# Update timestamps for index changes
nowTime = round(time())
self._indexChange = nowTime
self._rootChange[theItem.itemRoot] = nowTime
return True
##
# Internal Indexer Helpers
##
def _scanActive(self, tHandle, theItem, theText, itemTags):
"""Scan an active document for meta data.
"""
nTitle = 0
findHeader = True
theLines = theText.splitlines()
for nLine, aLine in enumerate(theLines, start=1):
if len(aLine.strip()) == 0:
continue
if aLine.startswith("#"):
if findHeader:
hDepth, _ = self._splitHeading(aLine)
if hDepth != "H0":
theItem.setMainHeading(hDepth)
findHeader = False
isTitle = self._indexTitle(tHandle, aLine, nLine)
if isTitle and nLine > 0:
if nTitle > 0:
@@ -289,48 +312,49 @@ class NWIndex:
# Prune no longer used tags
for tTag, isActive in itemTags.items():
if not isActive:
logger.verbose("Deleting removed tag '%s'", tTag)
logger.debug("Deleting removed tag '%s'", tTag)
del self._tagsIndex[tTag]
# Update timestamps for index changes
nowTime = round(time())
self._indexChange = nowTime
self._rootChange[theItem.itemRoot] = nowTime
return
return True
def _scanInactive(self, theItem, theText):
"""Scan an inactive document for meta data.
"""
for aLine in theText.splitlines():
if aLine.startswith("#"):
hDepth, _ = self._splitHeading(aLine)
if hDepth != "H0":
theItem.setMainHeading(hDepth)
break
return
##
# Internal Indexer Helpers
##
def _splitHeading(self, aLine):
"""Split a heading into its header level and text value.
"""
if aLine.startswith("# "):
return "H1", aLine[2:].strip()
elif aLine.startswith("## "):
return "H2", aLine[3:].strip()
elif aLine.startswith("### "):
return "H3", aLine[4:].strip()
elif aLine.startswith("#### "):
return "H4", aLine[5:].strip()
elif aLine.startswith("#! "):
return "H1", aLine[3:].strip()
elif aLine.startswith("##! "):
return "H2", aLine[4:].strip()
return "H0", ""
def _indexTitle(self, tHandle, aLine, nTitle):
"""Save information about the title and its location in the
file to the index.
"""
if aLine.startswith("# "):
hDepth = "H1"
hText = aLine[2:].strip()
elif aLine.startswith("## "):
hDepth = "H2"
hText = aLine[3:].strip()
elif aLine.startswith("### "):
hDepth = "H3"
hText = aLine[4:].strip()
elif aLine.startswith("#### "):
hDepth = "H4"
hText = aLine[5:].strip()
elif aLine.startswith("#! "):
hDepth = "H1"
hText = aLine[3:].strip()
elif aLine.startswith("##! "):
hDepth = "H2"
hText = aLine[4:].strip()
else:
hDepth, hText = self._splitHeading(aLine)
if hDepth == "H0":
return False
sTitle = f"T{nTitle:06d}"
self._itemIndex.addItemHeading(tHandle, sTitle, hDepth, hText)
return True
def _indexWordCounts(self, tHandle, theText, nTitle):
@@ -493,18 +517,15 @@ class NWIndex:
for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle)
]
def getHandleHeaderLevel(self, tHandle):
"""Get the header level of the first header of a handle.
"""
return self._itemIndex.mainItemHeader(tHandle)
def getTableOfContents(self, maxDepth, skipExcl=True):
def getTableOfContents(self, rootHandle, maxDepth, skipExcl=True):
"""Generate a table of contents up to a maximum depth.
"""
tOrder = []
tData = {}
pKey = None
for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl):
for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(
rootHandle=rootHandle, skipExcl=skipExcl
):
tKey = f"{tHandle}:{sTitle}"
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
if iLevel > maxDepth:
@@ -647,23 +668,17 @@ class TagsIndex:
def tagHandle(self, tagKey):
"""Get the handle of a given tag.
"""
if tagKey in self._tags:
return self._tags.get(tagKey).get("handle")
return None
return self._tags.get(tagKey, {}).get("handle", None)
def tagHeading(self, tagKey):
"""Get the heading of a given tag.
"""
if tagKey in self._tags:
return self._tags.get(tagKey).get("heading")
return nwHeaders.TT_NONE
return self._tags.get(tagKey, {}).get("heading", nwHeaders.TT_NONE)
def tagClass(self, tagKey):
"""Get the class of a given tag.
"""
if tagKey in self._tags:
return self._tags.get(tagKey).get("class")
return None
return self._tags.get(tagKey, {}).get("class", None)
##
# Pack/Unpack
@@ -755,13 +770,6 @@ class ItemIndex:
self._items[tHandle] = IndexItem(tHandle, tItem)
return
def mainItemHeader(self, tHandle):
"""Return the primary item header for an item.
"""
if tHandle in self._items:
return self._items[tHandle].level
return "H0"
def allItemTags(self, tHandle):
"""Get all tags set for headings of an item.
"""
@@ -794,7 +802,7 @@ class ItemIndex:
continue
if tItem.isNoteLayout():
continue
if skipExcl and not tItem.isExported:
if skipExcl and not tItem.isActive:
continue
tHandle = tItem.itemHandle
@@ -821,7 +829,6 @@ class ItemIndex:
"""
if tHandle in self._items:
tItem = self._items[tHandle]
tItem.updateLevel(hDepth)
tItem.addHeading(IndexHeading(sTitle, hDepth, hText))
return
@@ -897,7 +904,6 @@ class IndexItem:
def __init__(self, tHandle, tItem):
self._handle = tHandle
self._item = tItem
self._level = "H0"
self._headings = {}
self._index = 0
@@ -917,21 +923,10 @@ class IndexItem:
def item(self):
return self._item
@property
def level(self):
return self._level
##
# Setters
##
def updateLevel(self, level):
"""Set the level only if it has not already been set.
"""
if self._level == "H0":
self._level = level
return
def addHeading(self, tHeading):
"""Add a heading to the item. Also remove the placeholder entry
if it exists.
@@ -1011,7 +1006,7 @@ class IndexItem:
if hRefs:
refs[sTitle] = hRefs
data = {"level": self._level}
data = {}
data["headings"] = heads
if refs:
data["references"] = refs
@@ -1021,7 +1016,6 @@ class IndexItem:
def unpackData(self, data):
"""Unpack an item entry from the data.
"""
self._level = data.get("level", "H0")
references = data.get("references", {})
for sTitle, hData in data.get("headings", {}).items():
if not isTitleTag(sTitle):
@@ -1030,6 +1024,7 @@ class IndexItem:
tHeading.unpackData(hData)
tHeading.unpackReferences(references.get(sTitle, {}))
self.addHeading(tHeading)
return
# END Class IndexItem
+49 -29
View File
@@ -31,12 +31,12 @@ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.common import (
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified
)
from novelwriter.constants import nwLabels, trConst
from novelwriter.constants import nwHeaders, nwLabels, trConst
logger = logging.getLogger(__name__)
class NWItem():
class NWItem:
def __init__(self, theProject):
@@ -52,15 +52,16 @@ class NWItem():
self._layout = nwItemLayout.NO_LAYOUT
self._status = None
self._import = None
self._active = True
self._expanded = False
self._exported = True
# Document Meta Data
self._charCount = 0 # Current character count
self._wordCount = 0 # Current word count
self._paraCount = 0 # Current paragraph count
self._cursorPos = 0 # Last cursor position
self._initCount = 0 # Initial word count
self._heading = "H0" # The main heading
self._charCount = 0 # Current character count
self._wordCount = 0 # Current word count
self._paraCount = 0 # Current paragraph count
self._cursorPos = 0 # Last cursor position
self._initCount = 0 # Initial word count
return
@@ -114,13 +115,17 @@ class NWItem():
def itemImport(self):
return self._import
@property
def isActive(self):
return self._active
@property
def isExpanded(self):
return self._expanded
@property
def isExported(self):
return self._exported
def mainHeading(self):
return self._heading
@property
def charCount(self):
@@ -162,6 +167,7 @@ class NWItem():
metaAttrib = {}
metaAttrib["expanded"] = str(self._expanded)
if self._type == nwItemType.FILE:
metaAttrib["mainHeading"] = str(self._heading)
metaAttrib["charCount"] = str(self._charCount)
metaAttrib["wordCount"] = str(self._wordCount)
metaAttrib["paraCount"] = str(self._paraCount)
@@ -171,7 +177,7 @@ class NWItem():
nameAttrib["status"] = str(self._status)
nameAttrib["import"] = str(self._import)
if self._type == nwItemType.FILE:
nameAttrib["exported"] = str(self._exported)
nameAttrib["active"] = str(self._active)
xPack = etree.SubElement(xParent, "item", attrib=itemAttrib)
self._subPack(xPack, "meta", attrib=metaAttrib)
@@ -202,6 +208,7 @@ class NWItem():
for xValue in xItem:
if xValue.tag == "meta":
self.setExpanded(xValue.attrib.get("expanded", False))
self.setMainHeading(xValue.attrib.get("mainHeading", "H0"))
self.setCharCount(xValue.attrib.get("charCount", 0))
self.setWordCount(xValue.attrib.get("wordCount", 0))
self.setParaCount(xValue.attrib.get("paraCount", 0))
@@ -210,7 +217,11 @@ class NWItem():
self.setName(xValue.text)
self.setStatus(xValue.attrib.get("status", None))
self.setImport(xValue.attrib.get("import", None))
self.setExported(xValue.attrib.get("exported", True))
self.setActive(xValue.attrib.get("active", True))
# ToDo: Remove before 2.0 release. Only needed for 2.0 pre-releases.
if "exported" in xValue.attrib:
self.setActive(xValue.attrib.get("exported", True))
# Legacy Format (1.3 and earlier)
elif xValue.tag == "status":
@@ -224,7 +235,7 @@ class NWItem():
elif xValue.tag == "expanded":
self.setExpanded(xValue.text)
elif xValue.tag == "exported":
self.setExported(xValue.text)
self.setActive(xValue.text)
elif xValue.tag == "charCount":
self.setCharCount(xValue.text)
elif xValue.tag == "wordCount":
@@ -269,7 +280,7 @@ class NWItem():
# Lookup Methods
##
def describeMe(self, hLevel=None):
def describeMe(self):
"""Return a string description of the item.
"""
descKey = "none"
@@ -279,12 +290,14 @@ class NWItem():
descKey = "folder"
elif self._type == nwItemType.FILE:
if self._layout == nwItemLayout.DOCUMENT:
if hLevel == "H1":
if self._heading == "H1":
descKey = "doc_h1"
elif hLevel == "H2":
elif self._heading == "H2":
descKey = "doc_h2"
elif hLevel == "H3":
elif self._heading == "H3":
descKey = "doc_h3"
elif self._heading == "H4":
descKey = "doc_h4"
else:
descKey = "document"
elif self._layout == nwItemLayout.NOTE:
@@ -292,16 +305,16 @@ class NWItem():
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
def getImportStatus(self):
def getImportStatus(self, incIcon=True):
"""Return the relevant importance or status label and icon for
the current item based on its class.
"""
if self.isNovelLike():
stName = self.theProject.statusItems.name(self._status)
stIcon = self.theProject.statusItems.icon(self._status)
stIcon = self.theProject.statusItems.icon(self._status) if incIcon else None
else:
stName = self.theProject.importItems.name(self._import)
stIcon = self.theProject.importItems.icon(self._import)
stIcon = self.theProject.importItems.icon(self._import) if incIcon else None
return stName, stIcon
##
@@ -487,6 +500,15 @@ class NWItem():
self._import = self.theProject.importItems.check(value)
return
def setActive(self, state):
"""Set the export flag.
"""
if isinstance(state, str):
self._active = (state == str(True))
else:
self._active = (state is True)
return
def setExpanded(self, state):
"""Set the expanded status of an item in the project tree.
"""
@@ -496,19 +518,17 @@ class NWItem():
self._expanded = (state is True)
return
def setExported(self, state):
"""Set the export flag.
"""
if isinstance(state, str):
self._exported = (state == str(True))
else:
self._exported = (state is True)
return
##
# Set Document Meta Data
##
def setMainHeading(self, value):
"""Set the main heading level.
"""
if value in nwHeaders.H_LEVEL:
self._heading = value
return
def setCharCount(self, count):
"""Set the character count, and ensure that it is an integer.
"""
+7 -8
View File
@@ -40,30 +40,30 @@ VALID_MAP = {
"GuiWritingStats": {
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
"widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes",
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax"
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax",
},
"GuiDocSplit": {"spLevel"},
"GuiDocSplit": {"spLevel", "intoFolder", "docHierarchy"},
"GuiBuildNovel": {
"winWidth", "winHeight", "boxWidth", "docWidth", "hideScene",
"hideSection", "addNovel", "addNotes", "ignoreFlag", "justifyText",
"excludeBody", "textFont", "textSize", "lineHeight", "noStyling",
"incSynopsis", "incComments", "incKeywords", "incBodyText",
"replaceTabs", "replaceUCode"
"replaceTabs", "replaceUCode", "rootFilter",
},
"GuiOutline": {"headerOrder", "columnWidth", "columnHidden"},
"GuiProjectSettings": {
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW"
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW",
},
"GuiProjectDetails": {
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble"
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
},
"GuiWordList": {"winWidth", "winHeight"},
"GuiNovelView": {"lastCol"},
}
class OptionState():
class OptionState:
def __init__(self, theProject):
self.theProject = theProject
@@ -188,8 +188,7 @@ class OptionState():
the default value.
"""
if group in self._theState:
if name in self._theState[group]:
return checkBool(self._theState[group].get(name, default), default)
return checkBool(self._theState[group].get(name, default), default)
return default
def getEnum(self, group, name, lookup, default):
+51 -36
View File
@@ -44,7 +44,7 @@ from novelwriter.core.document import NWDoc
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
from novelwriter.error import logException
from novelwriter.common import (
checkString, checkBool, checkInt, isHandle, formatTimeStamp,
checkString, checkBool, checkInt, checkStringNone, isHandle, formatTimeStamp,
makeFileNameSafe, hexToInt, minmax, simplified
)
from novelwriter.constants import trConst, nwFiles, nwLabels
@@ -52,7 +52,7 @@ from novelwriter.constants import trConst, nwFiles, nwLabels
logger = logging.getLogger(__name__)
class NWProject():
class NWProject:
FILE_VERSION = "1.4" # The current project file format version
@@ -85,7 +85,6 @@ class NWProject():
self.projDict = None # The spell check dictionary
self.projSpell = None # The spell check language, if different than default
self.projLang = None # The project language, used for builds
self.projFile = None # The file name of the project main XML file
self.projFiles = [] # A list of all files in the content folder on load
# Project Meta
@@ -176,7 +175,7 @@ class NWProject():
self._projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def writeNewFile(self, tHandle, hLevel, isDocument):
def writeNewFile(self, tHandle, hLevel, isDocument, addText=""):
"""Write content to a new document after it is created. This
will not run if the file exists and is not empty.
"""
@@ -187,11 +186,11 @@ class NWProject():
return False
newDoc = NWDoc(self, tHandle)
if newDoc.readDocument().strip():
if (newDoc.readDocument() or "").strip():
return False
hshText = "#"*minmax(hLevel, 1, 4)
newText = f"{hshText} {tItem.itemName}\n\n"
newText = f"{hshText} {tItem.itemName}\n\n{addText}"
if tItem.isNovelLike() and isDocument:
tItem.setLayout(nwItemLayout.DOCUMENT)
else:
@@ -202,6 +201,23 @@ class NWProject():
return True
def removeItem(self, tHandle):
"""Remove an item from the project. This will delete both the
project entry and a document file if it exists.
"""
if self._projTree.checkType(tHandle, nwItemType.FILE):
delDoc = NWDoc(self, tHandle)
if not delDoc.deleteDocument():
self.mainGui.makeAlert([
self.tr("Could not delete document file."), delDoc.getError()
], nwAlert.ERROR)
return False
self._projIndex.deleteHandle(tHandle)
del self._projTree[tHandle]
return True
def trashFolder(self):
"""Add the special trash root folder to the project.
"""
@@ -243,7 +259,6 @@ class NWProject():
self.projDict = None
self.projSpell = None
self.projLang = None
self.projFile = nwFiles.PROJ_FILE
self.projFiles = []
self.projName = ""
self.bookTitle = ""
@@ -437,7 +452,7 @@ class NWProject():
legacyList = [] # Cleanup is done later
for projItem in os.listdir(self.projPath):
logger.verbose("Project contains: %s", projItem)
logger.debug("Project contains: %s", projItem)
if projItem.startswith("data_") and len(projItem) == 6:
legacyList.append(projItem)
@@ -457,7 +472,7 @@ class NWProject():
self.clearProject()
return False
else:
logger.verbose("Project is not locked")
logger.debug("Project is not locked")
# Open The Project XML File
# =========================
@@ -494,8 +509,8 @@ class NWProject():
hexVersion = xRoot.attrib.get("hexVersion", "0x0")
fileVersion = xRoot.attrib.get("fileVersion", self.tr("Unknown"))
logger.verbose("XML root is '%s'", nwxRoot)
logger.verbose("File version is '%s'", fileVersion)
logger.debug("XML root is '%s'", nwxRoot)
logger.debug("File version is '%s'", fileVersion)
# Check File Type
# ===============
@@ -576,16 +591,16 @@ class NWProject():
if xItem.text is None:
continue
if xItem.tag == "name":
self.projName = checkString(simplified(xItem.text), "")
logger.verbose("Working Title: '%s'", self.projName)
self.projName = simplified(checkString(xItem.text, ""))
logger.info("Project Name: '%s'", self.projName)
elif xItem.tag == "title":
self.bookTitle = checkString(simplified(xItem.text), "")
logger.verbose("Title is '%s'", self.bookTitle)
self.bookTitle = simplified(checkString(xItem.text, ""))
logger.info("Project Title: '%s'", self.bookTitle)
elif xItem.tag == "author":
author = checkString(simplified(xItem.text), "")
author = simplified(checkString(xItem.text, ""))
if author:
self.bookAuthors.append(author)
logger.verbose("Author: '%s'", author)
logger.debug("Author: '%s'", author)
elif xItem.tag == "saveCount":
self.saveCount = checkInt(xItem.text, 0)
elif xItem.tag == "autoCount":
@@ -601,25 +616,25 @@ class NWProject():
if xItem.tag == "doBackup":
self.doBackup = checkBool(xItem.text, False)
elif xItem.tag == "language":
self.projLang = checkString(xItem.text, None, True)
self.projLang = checkStringNone(xItem.text, None)
elif xItem.tag == "spellCheck":
self.spellCheck = checkBool(xItem.text, False)
elif xItem.tag == "spellLang":
self.projSpell = checkString(xItem.text, None, True)
self.projSpell = checkStringNone(xItem.text, None)
elif xItem.tag == "lastEdited":
self.lastEdited = checkString(xItem.text, None, True)
self.lastEdited = checkStringNone(xItem.text, None)
elif xItem.tag == "lastViewed":
self.lastViewed = checkString(xItem.text, None, True)
self.lastViewed = checkStringNone(xItem.text, None)
elif xItem.tag == "lastNovel":
self.lastNovel = checkString(xItem.text, None, True)
self.lastNovel = checkStringNone(xItem.text, None)
elif xItem.tag == "lastOutline":
self.lastOutline = checkString(xItem.text, None, True)
self.lastOutline = checkStringNone(xItem.text, None)
elif xItem.tag == "lastWordCount":
self.lastWCount = checkInt(xItem.text, 0, False)
self.lastWCount = checkInt(xItem.text, 0)
elif xItem.tag == "novelWordCount":
self.lastNovelWC = checkInt(xItem.text, 0, False)
self.lastNovelWC = checkInt(xItem.text, 0)
elif xItem.tag == "notesWordCount":
self.lastNotesWC = checkInt(xItem.text, 0, False)
self.lastNotesWC = checkInt(xItem.text, 0)
elif xItem.tag == "status":
self.statusItems.unpackXML(xItem)
elif xItem.tag == "importance":
@@ -628,12 +643,12 @@ class NWProject():
for xEntry in xItem:
if xEntry.tag == "entry" and "key" in xEntry.attrib:
self.autoReplace[xEntry.attrib["key"]] = checkString(
xEntry.text, None, False
xEntry.text, "ERROR"
)
elif xItem.tag == "titleFormat":
titleFormat = self.titleFormat.copy()
for xEntry in xItem:
titleFormat[xEntry.tag] = checkString(xEntry.text, "", False)
titleFormat[xEntry.tag] = checkString(xEntry.text, "")
self.setTitleFormat(titleFormat)
elif xChild.tag == "content":
@@ -663,7 +678,7 @@ class NWProject():
# Check the project tree consistency
for tItem in self._projTree:
tHandle = tItem.itemHandle
logger.verbose("Checking item '%s'", tHandle)
logger.debug("Checking item '%s'", tHandle)
if not self._projTree.updateItemData(tHandle):
logger.error("There was a problem item '%s', and it has been removed", tHandle)
del self._projTree[tHandle] # The file will be re-added as orphaned
@@ -757,9 +772,9 @@ class NWProject():
self._projTree.packXML(nwXML)
# Write the xml tree to file
tempFile = os.path.join(self.projPath, self.projFile+"~")
saveFile = os.path.join(self.projPath, self.projFile)
backFile = os.path.join(self.projPath, self.projFile[:-3]+"bak")
tempFile = os.path.join(self.projPath, nwFiles.PROJ_FILE+"~")
saveFile = os.path.join(self.projPath, nwFiles.PROJ_FILE)
backFile = os.path.join(self.projPath, nwFiles.PROJ_FILE[:-3]+"bak")
try:
with open(tempFile, mode="wb") as outFile:
outFile.write(etree.tostring(
@@ -799,7 +814,7 @@ class NWProject():
return True
def closeProject(self, idleTime=0):
def closeProject(self, idleTime=0.0):
"""Close the current project and clear all meta data.
"""
logger.info("Closing project: %s", self.projPath)
@@ -1074,7 +1089,7 @@ class NWProject():
def setSpellLang(self, theLang):
"""Set the project-specific spell check language.
"""
theLang = checkString(theLang, None, True)
theLang = checkStringNone(theLang, None)
if self.projSpell != theLang:
self.projSpell = theLang
self.setProjectChanged(True)
@@ -1084,7 +1099,7 @@ class NWProject():
def setProjectLang(self, theLang):
"""Set the project-specific language.
"""
theLang = checkString(theLang, None, True)
theLang = checkStringNone(theLang, None)
if self.projLang != theLang:
self.projLang = theLang
self._loadProjectLocalisation()
@@ -1168,7 +1183,7 @@ class NWProject():
information to the GUI statusbar.
"""
self.projChanged = bValue
self.mainGui.statusBar.doUpdateProjectStatus(bValue)
self.mainGui.mainStatus.doUpdateProjectStatus(bValue)
if bValue:
# If we've changed the project at all, this should be True
self.projAltered = True
+1 -1
View File
@@ -33,7 +33,7 @@ from novelwriter.error import logException
logger = logging.getLogger(__name__)
class NWSpellEnchant():
class NWSpellEnchant:
def __init__(self):
+1 -1
View File
@@ -37,7 +37,7 @@ from novelwriter.common import checkInt, minmax, simplified
logger = logging.getLogger(__name__)
class NWStatus():
class NWStatus:
STATUS = 1
IMPORT = 2
+2 -2
View File
@@ -38,7 +38,7 @@ class ToHtml(Tokenizer):
M_EBOOK = 2 # Tweak output for converting to epub
def __init__(self, theProject):
Tokenizer.__init__(self, theProject)
super().__init__(theProject)
self._genMode = self.M_EXPORT
self._cssStyles = True
@@ -107,7 +107,7 @@ class ToHtml(Tokenizer):
"""Extend the auto-replace to also properly encode some unicode
characters into their respective HTML entities.
"""
Tokenizer.doPreProcessing(self)
super().doPreProcessing()
self._theText = self._theText.translate(self._trMap)
return
+4 -10
View File
@@ -304,16 +304,10 @@ class Tokenizer(ABC):
if self._theItem is None:
return False
self._theText = ""
if theText is not None:
# If the text is set, just use that
self._theText = theText
else:
# Otherwise, load it from file
theDoc = NWDoc(self.theProject, theHandle)
theText = theDoc.readDocument()
if theText:
self._theText = theText
if theText is None:
theText = NWDoc(self.theProject, theHandle).readDocument() or ""
self._theText = theText
docSize = len(self._theText)
if docSize > nwConst.MAX_DOCSIZE:
+1 -1
View File
@@ -37,7 +37,7 @@ class ToMarkdown(Tokenizer):
M_GH = 1 # GitHub Markdown
def __init__(self, theProject):
Tokenizer.__init__(self, theProject)
super().__init__(theProject)
self._genMode = self.M_STD
self._fullMD = []
+4 -4
View File
@@ -89,7 +89,7 @@ M_DEL = ~X_DEL
class ToOdt(Tokenizer):
def __init__(self, theProject, isFlat):
Tokenizer.__init__(self, theProject)
super().__init__(theProject)
self._isFlat = isFlat # Flat: .fodt, otherwise .odt
@@ -994,7 +994,7 @@ class ToOdt(Tokenizer):
# Auto-Style Classes
# =============================================================================================== #
class ODTParagraphStyle():
class ODTParagraphStyle:
"""Wrapper class for the paragraph style setting used by the
exporter. Only the used settings are exposed here to keep the class
minimal and fast.
@@ -1208,7 +1208,7 @@ class ODTParagraphStyle():
# END Class ODTParagraphStyle
class ODTTextStyle():
class ODTTextStyle:
"""Wrapper class for the text style setting used by the exporter.
Only the used settings are exposed here to keep the class minimal
and fast.
@@ -1297,7 +1297,7 @@ X_SPAN_TEXT = 2
X_SPAN_SING = 3
class XMLParagraph():
class XMLParagraph:
"""This is a helper class to manage the text content of a single
XML element using mixed content tags.
+13 -11
View File
@@ -38,7 +38,7 @@ from novelwriter.core.item import NWItem
logger = logging.getLogger(__name__)
class NWTree():
class NWTree:
MAX_DEPTH = 1000 # Cap of tree traversing for loops
@@ -89,20 +89,20 @@ class NWTree():
logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
return False
logger.verbose("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
logger.debug("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
nwItem.setHandle(tHandle)
nwItem.setParent(pHandle)
if nwItem.isRootType():
logger.verbose("Item '%s' is a root item", str(tHandle))
logger.debug("Item '%s' is a root item", str(tHandle))
self._treeRoots[tHandle] = nwItem
if nwItem.itemClass == nwItemClass.ARCHIVE:
logger.verbose("Item '%s' is the archive folder", str(tHandle))
logger.debug("Item '%s' is the archive folder", str(tHandle))
self._archRoot = tHandle
elif nwItem.itemClass == nwItemClass.TRASH:
if self._trashRoot is None:
logger.verbose("Item '%s' is the trash folder", str(tHandle))
logger.debug("Item '%s' is the trash folder", str(tHandle))
self._trashRoot = tHandle
else:
logger.error("Only one trash folder allowed")
@@ -274,11 +274,13 @@ class NWTree():
return rootClasses
def iterRoots(self, itemClass):
"""Iterate over all items of a given class.
"""Iterate over all root items of a given class in order.
"""
for tHandle, nwItem in self._treeRoots.items():
if nwItem.itemClass == itemClass:
yield tHandle, nwItem
for tHandle in self._treeOrder:
nwItem = self.__getitem__(tHandle)
if nwItem is not None and nwItem.isRootType():
if itemClass is None or nwItem.itemClass == itemClass:
yield tHandle, nwItem
return
def isRoot(self, tHandle):
@@ -347,7 +349,7 @@ class NWTree():
# Save the temp list
self._treeOrder = tmpOrder
self._setTreeChanged(True)
logger.verbose("Project tree order updated")
logger.debug("Project tree order updated")
return
@@ -457,7 +459,7 @@ class NWTree():
"""Generate a unique item handle. In the event that the key
already exists, generate a new one.
"""
logger.verbose("Generating new handle")
logger.debug("Generating new handle")
handle = f"{random.getrandbits(52):013x}"
if handle in self._projTree:
logger.warning("Duplicate handle encountered! Retrying ...")
@@ -202,7 +202,7 @@ class QConfigLayout(QGridLayout):
class QHelpLabel(QLabel):
def __init__(self, theText, textCol, fontSize=0.9):
QLabel.__init__(self, theText)
super().__init__(theText)
if isinstance(textCol, QColor):
qCol = textCol
@@ -267,8 +267,8 @@ class QSwitch(QAbstractButton):
return self._offset
@offset.setter
def offset(self, theOffset):
self._offset = theOffset
def offset(self, offset):
self._offset = offset
self.update()
return
@@ -276,11 +276,11 @@ class QSwitch(QAbstractButton):
# Getters and Setters
##
def setChecked(self, isChecked):
def setChecked(self, checked):
"""Overload setChecked to also alter the offset.
"""
super().setChecked(isChecked)
if isChecked:
super().setChecked(checked)
if checked:
self.offset = self._xW - self._xR
else:
self.offset = self._xR
@@ -290,10 +290,10 @@ class QSwitch(QAbstractButton):
# Events
##
def resizeEvent(self, theEvent):
def resizeEvent(self, event):
"""Overload resize to ensure correct offset.
"""
super().resizeEvent(theEvent)
super().resizeEvent(event)
if self.isChecked():
self.offset = self._xW - self._xR
else:
@@ -377,7 +377,7 @@ class QSwitch(QAbstractButton):
class PagedDialog(QDialog):
def __init__(self, parent=None):
QDialog.__init__(self, parent=parent)
super().__init__(parent=parent)
self._tabBar = VerticalTabBar(self)
self._tabBar.setExpanding(False)
@@ -410,13 +410,13 @@ class PagedDialog(QDialog):
return
def addTab(self, widget, label):
"""Forwards the adding of tabs to the QTabWidget.
"""Forward the adding of tabs to the QTabWidget.
"""
self._tabBox.addTab(widget, label)
return
def addControls(self, buttonBar):
"""Adds a button bar to the dialog.
"""Add a button bar to the dialog.
"""
self._buttonBox.addWidget(buttonBar)
return
@@ -427,14 +427,14 @@ class PagedDialog(QDialog):
class VerticalTabBar(QTabBar):
def __init__(self, parent=None):
QTabBar.__init__(self, parent=parent)
super().__init__(parent=parent)
self._mW = novelwriter.CONFIG.pxInt(150)
return
def tabSizeHint(self, index):
"""Returns a transposed size hint for the rotated bar.
"""Return a transposed size hint for the rotated bar.
"""
tSize = QTabBar.tabSizeHint(self, index)
tSize = super().tabSizeHint(index)
tSize.transpose()
tSize.setWidth(min(tSize.width(), self._mW))
return tSize
+1 -1
View File
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
class GuiAbout(QDialog):
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiAbout ...")
self.setObjectName("GuiAbout")
+88 -110
View File
@@ -1,10 +1,11 @@
"""
novelWriter GUI Doc Merge Tool
================================
GUI class for merging multiple documents to one document
novelWriter GUI Doc Merge Dialog
==================================
Custom dialog class for merging documents.
File History:
Created: 2020-01-23 [0.4.3]
Created: 2020-01-23 [0.4.3]
Rewritten: 2022-10-06 [2.0b1]
This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen
@@ -26,169 +27,146 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import novelwriter
from PyQt5.QtCore import Qt
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QLabel, QListWidget, QAbstractItemView,
QListWidgetItem, QDialogButtonBox
QAbstractItemView, QDialog, QDialogButtonBox, QGridLayout, QLabel,
QListWidget, QListWidgetItem, QVBoxLayout,
)
from novelwriter.core import NWDoc
from novelwriter.enum import nwAlert, nwItemType
from novelwriter.gui.custom import QHelpLabel
from novelwriter.custom import QHelpLabel, QSwitch
logger = logging.getLogger(__name__)
class GuiDocMerge(QDialog):
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
def __init__(self, mainGui, sHandle, itemList):
super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocMerge ...")
self.setObjectName("GuiDocMerge")
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject
self.sourceItem = None
self.outerBox = QVBoxLayout()
self._data = {}
self.setWindowTitle(self.tr("Merge Documents"))
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge")))
self.helpLabel = QHelpLabel(
self.tr("Drag and drop items to change the order."), self.mainGui.mainTheme.helpText
)
self.helpLabel = QHelpLabel(self.tr(
"Drag and drop items to change the order, or uncheck to exclude."
), self.mainTheme.helpText)
iPx = self.mainTheme.baseIconSize
hSp = self.mainConf.pxInt(12)
vSp = self.mainConf.pxInt(8)
bSp = self.mainConf.pxInt(12)
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
self.listBox.setIconSize(QSize(iPx, iPx))
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
self.listBox.setMinimumHeight(self.mainConf.pxInt(180))
self.listBox.setSelectionBehavior(QAbstractItemView.SelectRows)
self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
# Merge Options
self.trashLabel = QLabel(self.tr("Move merged items to Trash"))
self.trashSwitch = QSwitch(width=2*iPx, height=iPx)
self.optBox = QGridLayout()
self.optBox.addWidget(self.trashLabel, 0, 0)
self.optBox.addWidget(self.trashSwitch, 0, 1)
self.optBox.setHorizontalSpacing(hSp)
self.optBox.setColumnStretch(2, 1)
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doMerge)
self.buttonBox.rejected.connect(self._doClose)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.resetButton = self.buttonBox.addButton(QDialogButtonBox.Reset)
self.resetButton.clicked.connect(self._resetList)
# Assemble
self.outerBox = QVBoxLayout()
self.outerBox.setSpacing(0)
self.outerBox.addWidget(self.headLabel)
self.outerBox.addWidget(self.helpLabel)
self.outerBox.addSpacing(self.mainConf.pxInt(8))
self.outerBox.addSpacing(vSp)
self.outerBox.addWidget(self.listBox)
self.outerBox.addSpacing(self.mainConf.pxInt(12))
self.outerBox.addSpacing(vSp)
self.outerBox.addLayout(self.optBox)
self.outerBox.addSpacing(bSp)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
self.rejected.connect(self._doClose)
self._populateList()
# Load Content
self._loadContent(sHandle, itemList)
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.
def getData(self):
"""Return the user's choices.
"""
logger.verbose("GuiDocMerge merge button clicked")
finalOrder = []
finalItems = []
for i in range(self.listBox.count()):
finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
item = self.listBox.item(i)
if item is not None and item.checkState() == Qt.Checked:
finalItems.append(item.data(Qt.UserRole))
if len(finalOrder) == 0:
self.mainGui.makeAlert(self.tr(
"No source documents found. Nothing to do."
), nwAlert.ERROR)
return False
self._data["moveToTrash"] = self.trashSwitch.isChecked()
self._data["finalItems"] = finalItems
theText = ""
for tHandle in finalOrder:
inDoc = NWDoc(self.theProject, tHandle)
docText = inDoc.readDocument()
docErr = inDoc.getError()
if docText is None and docErr:
self.mainGui.makeAlert([
self.tr("Failed to open document file."), docErr
], nwAlert.ERROR)
if docText:
theText += docText.rstrip("\n")+"\n\n"
return self._data
if self.sourceItem is None:
self.mainGui.makeAlert(self.tr(
"No source folder selected. Nothing to do."
), nwAlert.ERROR)
return False
##
# Slots
##
srcItem = self.theProject.tree[self.sourceItem]
if srcItem is None:
self.mainGui.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
return False
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent)
newItem = self.theProject.tree[nHandle]
newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport)
outDoc = NWDoc(self.theProject, nHandle)
if not outDoc.writeDocument(theText):
self.mainGui.makeAlert([
self.tr("Could not save document."), outDoc.getError()
], nwAlert.ERROR)
return False
self.mainGui.projView.revealNewTreeItem(nHandle)
self.mainGui.openDocument(nHandle, doScroll=True)
self._doClose()
return True
def _doClose(self):
"""Close the dialog window without doing anything.
def _resetList(self):
"""Reset the content of the list box to its original state.
"""
self.close()
logger.debug("Resetting list box content")
sHandle = self._data.get("sHandle", None)
itemList = self._data.get("origItems", [])
self._loadContent(sHandle, itemList)
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.
def _loadContent(self, sHandle, itemList):
"""Load content from a given list of items.
"""
tHandle = self.mainGui.projView.getSelectedHandle()
self.sourceItem = tHandle
if tHandle is None:
return False
self._data = {}
self._data["sHandle"] = sHandle
self._data["origItems"] = itemList
nwItem = self.theProject.tree[tHandle]
if nwItem is None:
return False
if nwItem.itemType is not nwItemType.FOLDER:
self.mainGui.makeAlert(self.tr(
"Element selected in the project tree must be a folder."
), nwAlert.ERROR)
return False
for sHandle in self.mainGui.projView.getTreeFromHandle(tHandle):
newItem = QListWidgetItem()
nwItem = self.theProject.tree[sHandle]
if not nwItem.isFileType():
self.listBox.clear()
for tHandle in itemList:
nwItem = self.theProject.tree[tHandle]
if nwItem is None or not nwItem.isFileType():
continue
itemIcon = self.mainTheme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading
)
newItem = QListWidgetItem()
newItem.setIcon(itemIcon)
newItem.setText(nwItem.itemName)
newItem.setData(Qt.UserRole, sHandle)
newItem.setData(Qt.UserRole, tHandle)
newItem.setCheckState(Qt.Checked)
self.listBox.addItem(newItem)
return True
return
# END Class GuiDocMerge
+124 -159
View File
@@ -1,10 +1,11 @@
"""
novelWriter GUI Doc Split Tool
================================
GUI class for splitting a single document into multiple documents
novelWriter GUI Doc Split Dialog
==================================
Custom dialog class for splitting documents.
File History:
Created: 2020-02-01 [0.4.3]
Created: 2020-02-01 [0.4.3]
Rewritten: 2022-10-12 [2.0b1]
This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen
@@ -29,32 +30,35 @@ import novelwriter
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QComboBox, QListWidget, QAbstractItemView,
QListWidgetItem, QDialogButtonBox, QLabel
QListWidgetItem, QDialogButtonBox, QLabel, QGridLayout
)
from novelwriter.core import NWDoc
from novelwriter.enum import nwAlert
from novelwriter.gui.custom import QHelpLabel
from novelwriter.custom import QHelpLabel, QSwitch
logger = logging.getLogger(__name__)
class GuiDocSplit(QDialog):
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
LINE_ROLE = Qt.UserRole
LEVEL_ROLE = Qt.UserRole + 1
LABEL_ROLE = Qt.UserRole + 2
def __init__(self, mainGui, sHandle):
super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocSplit ...")
self.setObjectName("GuiDocSplit")
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject
self.sourceItem = None
self.sourceText = []
self._data = {}
self._text = []
self.outerBox = QVBoxLayout()
self.setWindowTitle(self.tr("Split Document"))
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Document Headers")))
@@ -63,6 +67,18 @@ class GuiDocSplit(QDialog):
self.mainGui.mainTheme.helpText
)
# Values
iPx = self.mainTheme.baseIconSize
hSp = self.mainConf.pxInt(12)
vSp = self.mainConf.pxInt(8)
bSp = self.mainConf.pxInt(12)
pOptions = self.theProject.options
spLevel = pOptions.getInt("GuiDocSplit", "spLevel", 3)
intoFolder = pOptions.getBool("GuiDocSplit", "intoFolder", True)
docHierarchy = pOptions.getBool("GuiDocSplit", "docHierarchy", True)
# Header Selection
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
@@ -73,206 +89,155 @@ class GuiDocSplit(QDialog):
self.splitLevel.addItem(self.tr("Split up to Header Level 2 (Chapter)"), 2)
self.splitLevel.addItem(self.tr("Split up to Header Level 3 (Scene)"), 3)
self.splitLevel.addItem(self.tr("Split up to Header Level 4 (Section)"), 4)
spIndex = self.splitLevel.findData(
self.theProject.options.getInt("GuiDocSplit", "spLevel", 3)
)
spIndex = self.splitLevel.findData(spLevel)
if spIndex != -1:
self.splitLevel.setCurrentIndex(spIndex)
self.splitLevel.currentIndexChanged.connect(self._populateList)
self.splitLevel.currentIndexChanged.connect(self._reloadList)
# Split Options
self.folderLabel = QLabel(self.tr("Split into a new folder"))
self.folderSwitch = QSwitch(width=2*iPx, height=iPx)
self.folderSwitch.setChecked(intoFolder)
self.hierarchyLabel = QLabel(self.tr("Create document hierarchy"))
self.hierarchySwitch = QSwitch(width=2*iPx, height=iPx)
self.hierarchySwitch.setChecked(docHierarchy)
self.optBox = QGridLayout()
self.optBox.addWidget(self.folderLabel, 0, 0)
self.optBox.addWidget(self.folderSwitch, 0, 1)
self.optBox.addWidget(self.hierarchyLabel, 1, 0)
self.optBox.addWidget(self.hierarchySwitch, 1, 1)
self.optBox.setVerticalSpacing(vSp)
self.optBox.setHorizontalSpacing(hSp)
self.optBox.setColumnStretch(2, 1)
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doSplit)
self.buttonBox.rejected.connect(self._doClose)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
# Assemble
self.outerBox = QVBoxLayout()
self.outerBox.setSpacing(0)
self.outerBox.addWidget(self.headLabel)
self.outerBox.addWidget(self.helpLabel)
self.outerBox.addSpacing(self.mainConf.pxInt(8))
self.outerBox.addSpacing(vSp)
self.outerBox.addWidget(self.listBox)
self.outerBox.addWidget(self.splitLevel)
self.outerBox.addSpacing(self.mainConf.pxInt(12))
self.outerBox.addSpacing(vSp)
self.outerBox.addLayout(self.optBox)
self.outerBox.addSpacing(bSp)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
self.rejected.connect(self._doClose)
self._populateList()
# Load Content
self._loadContent(sHandle)
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 split process, and
must be deleted manually.
def getData(self):
"""Return the user's choices. Also save the users options for
the next time the dialog is used.
"""
logger.verbose("GuiDocSplit split button clicked")
if self.sourceItem is None:
self.mainGui.makeAlert(self.tr(
"No source document selected. Nothing to do."
), nwAlert.ERROR)
return False
srcItem = self.theProject.tree[self.sourceItem]
if srcItem is None:
self.mainGui.makeAlert(self.tr(
"Could not parse source document."
), nwAlert.ERROR)
return False
inDoc = NWDoc(self.theProject, self.sourceItem)
theText = inDoc.readDocument()
docErr = inDoc.getError()
if theText is None and docErr:
self.mainGui.makeAlert([
self.tr("Failed to open document file."), docErr
], nwAlert.ERROR)
if theText is None:
theText = ""
nLines = len(self.sourceText)
logger.debug("Splitting document %s with %d lines", self.sourceItem, nLines)
finalOrder = []
headerList = []
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
item = self.listBox.item(i)
if item is not None:
headerList.append((
item.data(self.LINE_ROLE),
item.data(self.LEVEL_ROLE),
item.data(self.LABEL_ROLE),
))
nFiles = len(finalOrder)
if nFiles == 0:
self.mainGui.makeAlert(self.tr(
"No headers found. Nothing to do."
), nwAlert.ERROR)
return False
spLevel = self.splitLevel.currentData()
intoFolder = self.folderSwitch.isChecked()
docHierarchy = self.hierarchySwitch.isChecked()
msgYes = self.mainGui.askQuestion(
self.tr("Split Document"),
"{0}<br><br>{1}".format(
self.tr(
"The document will be split into {0} file(s) in a new folder. "
"The original document will remain intact."
).format(nFiles),
self.tr(
"Continue with the splitting process?"
)
)
)
if not msgYes:
return False
self._data["spLevel"] = spLevel
self._data["headerList"] = headerList
self._data["intoFolder"] = intoFolder
self._data["docHierarchy"] = docHierarchy
# Create the folder
fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent)
self.mainGui.projView.revealNewTreeItem(fHandle)
logger.verbose("Creating folder '%s'", fHandle)
pOptions = self.theProject.options
pOptions.setValue("GuiDocSplit", "spLevel", spLevel)
pOptions.setValue("GuiDocSplit", "intoFolder", intoFolder)
pOptions.setValue("GuiDocSplit", "docHierarchy", docHierarchy)
# Loop through, and create the files
for wTitle, iStart, iEnd in finalOrder:
return self._data, self._text
wTitle = wTitle.lstrip("#").strip()
nHandle = self.theProject.newFile(wTitle, fHandle)
newItem = self.theProject.tree[nHandle]
newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport)
logger.verbose(
"Creating new document '%s' with text from line %d to %d",
nHandle, iStart+1, iEnd
)
##
# Slots
##
theText = "\n".join(self.sourceText[iStart:iEnd])
theText = theText.rstrip("\n") + "\n\n"
outDoc = NWDoc(self.theProject, nHandle)
if not outDoc.writeDocument(theText):
self.mainGui.makeAlert([
self.tr("Could not save document."), outDoc.getError()
], nwAlert.ERROR)
return False
self.mainGui.projView.revealNewTreeItem(nHandle)
self._doClose()
return True
def _doClose(self):
"""Close the dialog window without doing anything.
def _reloadList(self):
"""Reload the content of the list box.
"""
self.theProject.options.saveSettings()
self.close()
sHandle = self._data.get("sHandle", None)
self._loadContent(sHandle)
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.
def _loadContent(self, sHandle):
"""Load content from a given source item.
"""
self._data = {}
self._data["sHandle"] = sHandle
self.listBox.clear()
if self.sourceItem is None:
self.sourceItem = self.mainGui.projView.getSelectedHandle()
if self.sourceItem is None:
return False
nwItem = self.theProject.tree[self.sourceItem]
if nwItem is None:
return False
if not nwItem.isFileType():
self.mainGui.makeAlert(self.tr(
"Element selected in the project tree must be a file."
), nwAlert.ERROR)
return False
inDoc = NWDoc(self.theProject, self.sourceItem)
theText = inDoc.readDocument()
if theText is None:
theText = ""
return False
nwItem = self.theProject.tree[sHandle]
if nwItem is None or not nwItem.isFileType():
return
spLevel = self.splitLevel.currentData()
self.theProject.options.setValue("GuiDocSplit", "spLevel", spLevel)
logger.debug(
"Scanning document '%s' for headings level <= %d",
self.sourceItem, spLevel
)
if not self._text:
inDoc = NWDoc(self.theProject, sHandle)
self._text = (inDoc.readDocument() or "").splitlines()
self.sourceText = theText.splitlines()
for lineNo, aLine in enumerate(self.sourceText):
for lineNo, aLine in enumerate(self._text):
onLine = -1
hLevel = 0
hLabel = aLine.strip()
if aLine.startswith("# ") and spLevel >= 1:
onLine = lineNo
hLevel = 1
hLabel = aLine[2:].strip()
elif aLine.startswith("## ") and spLevel >= 2:
onLine = lineNo
hLevel = 2
hLabel = aLine[3:].strip()
elif aLine.startswith("### ") and spLevel >= 3:
onLine = lineNo
hLevel = 3
hLabel = aLine[4:].strip()
elif aLine.startswith("#### ") and spLevel >= 4:
onLine = lineNo
hLevel = 4
hLabel = aLine[5:].strip()
elif aLine.startswith("#! ") and spLevel >= 1:
onLine = lineNo
hLevel = 1
hLabel = aLine[3:].strip()
elif aLine.startswith("##! ") and spLevel >= 2:
onLine = lineNo
hLevel = 2
hLabel = aLine[4:].strip()
if onLine >= 0:
if onLine >= 0 and hLevel > 0:
newItem = QListWidgetItem()
newItem.setText(aLine.strip())
newItem.setData(Qt.UserRole, onLine)
newItem.setData(self.LINE_ROLE, onLine)
newItem.setData(self.LEVEL_ROLE, hLevel)
newItem.setData(self.LABEL_ROLE, hLabel)
self.listBox.addItem(newItem)
return True
return
# END Class GuiDocSplit
+1 -1
View File
@@ -36,7 +36,7 @@ logger = logging.getLogger(__name__)
class GuiEditLabel(QDialog):
def __init__(self, parent, text=""):
QDialog.__init__(self, parent=parent)
super().__init__(parent=parent)
self.setObjectName("GuiEditLabel")
self.setWindowTitle(self.tr("Item Label"))
+38 -40
View File
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter.enum import nwAlert
from novelwriter.gui.custom import QSwitch, QConfigLayout, PagedDialog
from novelwriter.custom import QSwitch, QConfigLayout, PagedDialog
from novelwriter.dialogs.quotes import GuiQuoteSelect
logger = logging.getLogger(__name__)
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
class GuiPreferences(PagedDialog):
def __init__(self, mainGui):
PagedDialog.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiPreferences ...")
self.setObjectName("GuiPreferences")
@@ -55,13 +55,13 @@ class GuiPreferences(PagedDialog):
self.setWindowTitle(self.tr("Preferences"))
self.tabGeneral = GuiPreferencesGeneral(self.mainGui)
self.tabProjects = GuiPreferencesProjects(self.mainGui)
self.tabDocs = GuiPreferencesDocuments(self.mainGui)
self.tabEditor = GuiPreferencesEditor(self.mainGui)
self.tabSyntax = GuiPreferencesSyntax(self.mainGui)
self.tabAuto = GuiPreferencesAutomation(self.mainGui)
self.tabQuote = GuiPreferencesQuotes(self.mainGui)
self.tabGeneral = GuiPreferencesGeneral(self)
self.tabProjects = GuiPreferencesProjects(self)
self.tabDocs = GuiPreferencesDocuments(self)
self.tabEditor = GuiPreferencesEditor(self)
self.tabSyntax = GuiPreferencesSyntax(self)
self.tabAuto = GuiPreferencesAutomation(self)
self.tabQuote = GuiPreferencesQuotes(self)
self.addTab(self.tabGeneral, self.tr("General"))
self.addTab(self.tabProjects, self.tr("Projects"))
@@ -128,9 +128,7 @@ class GuiPreferences(PagedDialog):
def _saveWindowSize(self):
"""Save the dialog window size.
"""
winWidth = self.mainConf.rpxInt(self.width())
winHeight = self.mainConf.rpxInt(self.height())
self.mainConf.setPreferencesSize(winWidth, winHeight)
self.mainConf.setPreferencesSize(self.width(), self.height())
return
# END Class GuiPreferences
@@ -138,12 +136,12 @@ class GuiPreferences(PagedDialog):
class GuiPreferencesGeneral(QWidget):
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
def __init__(self, prefsGui):
super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.mainGui = prefsGui.mainGui
self.mainTheme = prefsGui.mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
@@ -344,12 +342,12 @@ class GuiPreferencesGeneral(QWidget):
class GuiPreferencesProjects(QWidget):
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
def __init__(self, prefsGui):
super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.mainGui = prefsGui.mainGui
self.mainTheme = prefsGui.mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
@@ -505,12 +503,12 @@ class GuiPreferencesProjects(QWidget):
class GuiPreferencesDocuments(QWidget):
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
def __init__(self, prefsGui):
super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.mainGui = prefsGui.mainGui
self.mainTheme = prefsGui.mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
@@ -666,12 +664,12 @@ class GuiPreferencesDocuments(QWidget):
class GuiPreferencesEditor(QWidget):
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
def __init__(self, prefsGui):
super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.mainGui = prefsGui.mainGui
self.mainTheme = prefsGui.mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
@@ -840,12 +838,12 @@ class GuiPreferencesEditor(QWidget):
class GuiPreferencesSyntax(QWidget):
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
def __init__(self, prefsGui):
super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.mainGui = prefsGui.mainGui
self.mainTheme = prefsGui.mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
@@ -943,12 +941,12 @@ class GuiPreferencesSyntax(QWidget):
class GuiPreferencesAutomation(QWidget):
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
def __init__(self, prefsGui):
super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.mainGui = prefsGui.mainGui
self.mainTheme = prefsGui.mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
@@ -1100,12 +1098,12 @@ class GuiPreferencesAutomation(QWidget):
class GuiPreferencesQuotes(QWidget):
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
def __init__(self, prefsGui):
super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.mainGui = prefsGui.mainGui
self.mainTheme = prefsGui.mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
+60 -15
View File
@@ -27,16 +27,18 @@ import math
import logging
import novelwriter
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtCore import Qt, QSize, pyqtSlot
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import (
QWidget, QDialogButtonBox, QVBoxLayout, QTreeWidget, QTreeWidgetItem,
QLabel, QSpinBox, QGridLayout, QHBoxLayout, QLineEdit, QAbstractItemView
QAbstractItemView, QComboBox, QDialogButtonBox, QGridLayout, QHBoxLayout,
QLabel, QLineEdit, QSpinBox, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
QWidget
)
from novelwriter.enum import nwItemClass
from novelwriter.common import numberToRoman
from novelwriter.constants import nwUnicode
from novelwriter.gui.custom import PagedDialog, QSwitch
from novelwriter.custom import PagedDialog, QSwitch
from novelwriter.constants import nwLabels, nwUnicode
logger = logging.getLogger(__name__)
@@ -44,7 +46,7 @@ logger = logging.getLogger(__name__)
class GuiProjectDetails(PagedDialog):
def __init__(self, mainGui):
PagedDialog.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiProjectDetails ...")
self.setObjectName("GuiProjectDetails")
@@ -140,7 +142,7 @@ class GuiProjectDetails(PagedDialog):
class GuiProjectDetailsMain(QWidget):
def __init__(self, mainGui, theProject):
QWidget.__init__(self, mainGui)
super().__init__(parent=mainGui)
self.mainConf = novelwriter.CONFIG
self.theProject = theProject
@@ -272,7 +274,7 @@ class GuiProjectDetailsContents(QWidget):
C_PROG = 4
def __init__(self, mainGui, theProject):
QWidget.__init__(self, mainGui)
super().__init__(parent=mainGui)
self.mainConf = novelwriter.CONFIG
self.theProject = theProject
@@ -281,12 +283,26 @@ class GuiProjectDetailsContents(QWidget):
# Internal
self._theToC = []
self._currentRoot = None
iPx = self.mainTheme.baseIconSize
hPx = self.mainConf.pxInt(12)
vPx = self.mainConf.pxInt(4)
pOptions = self.theProject.options
# Header
# ======
self.tocLabel = QLabel("<b>%s</b>" % self.tr("Table of Contents"))
self.novelValue = QComboBox(self)
self.novelValue.setMinimumWidth(self.mainConf.pxInt(200))
self.novelValue.currentIndexChanged.connect(self._novelValueChanged)
self.headBox = QHBoxLayout()
self.headBox.addWidget(self.tocLabel)
self.headBox.addWidget(self.novelValue)
# Contents Tree
# =============
@@ -389,7 +405,7 @@ class GuiProjectDetailsContents(QWidget):
# ========
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(QLabel("<b>%s</b>" % self.tr("Table of Contents")))
self.outerBox.addLayout(self.headBox)
self.outerBox.addWidget(self.tocTree)
self.outerBox.addLayout(self.optionsBox)
@@ -412,19 +428,35 @@ class GuiProjectDetailsContents(QWidget):
def updateValues(self):
"""Populate the tree.
"""
self._prepareData()
self._currentRoot = None
self._populateNovelList()
rootHandle = self.novelValue.currentData()
self._prepareData(rootHandle)
self._populateTree()
return
##
# Internal Functions
##
def _prepareData(self):
"""Extract the data for the tree.
def _populateNovelList(self):
"""Fill the novel combo box with a list of all novel folders.
"""
self._theToC = []
self._theToC = self.theProject.index.getTableOfContents(2)
self.novelValue.clear()
tIcon = self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])
for tHandle, nwItem in self.theProject.tree.iterRoots(nwItemClass.NOVEL):
self.novelValue.addItem(tIcon, nwItem.itemName, tHandle)
return
def _prepareData(self, rootHandle):
"""Extract the information from the project index.
"""
logger.debug("Populating ToC from handle '%s'", rootHandle)
self._theToC = self.theProject.index.getTableOfContents(rootHandle, 2)
self._theToC.append(("", 0, self.tr("END"), 0))
return
@@ -432,6 +464,18 @@ class GuiProjectDetailsContents(QWidget):
# Slots
##
@pyqtSlot()
def _novelValueChanged(self):
"""Refresh the tree with another root item.
"""
rootHandle = self.novelValue.currentData()
if rootHandle != self._currentRoot:
self._prepareData(rootHandle)
self._populateTree()
self._currentRoot = rootHandle
return
@pyqtSlot()
def _populateTree(self):
"""Set the content of the chapter/page tree.
"""
@@ -466,10 +510,11 @@ class GuiProjectDetailsContents(QWidget):
progPage = f"{cPage:n}"
progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%"
hDec = self.mainTheme.getHeaderDecoration(tLevel)
if tTitle.strip() == "":
tTitle = self.tr("Untitled")
newItem.setIcon(self.C_TITLE, self.mainTheme.getIcon("doc_h%d" % tLevel))
newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
newItem.setText(self.C_TITLE, tTitle)
newItem.setText(self.C_WORDS, f"{wCount:n}")
newItem.setText(self.C_PAGES, f"{pCount:n}")
+1 -5
View File
@@ -54,7 +54,7 @@ class GuiProjectLoad(QDialog):
C_TIME = 2
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiProjectLoad ...")
self.setObjectName("GuiProjectLoad")
@@ -158,7 +158,6 @@ class GuiProjectLoad(QDialog):
def _doOpenRecent(self):
"""Close the dialog window with a recent project selected.
"""
logger.verbose("GuiProjectLoad open button clicked")
self._saveSettings()
self.openPath = None
@@ -183,7 +182,6 @@ class GuiProjectLoad(QDialog):
def _doBrowse(self):
"""Browse for a folder path.
"""
logger.verbose("GuiProjectLoad browse button clicked")
extFilter = [
self.tr("novelWriter Project File ({0})").format(nwFiles.PROJ_FILE),
self.tr("All files ({0})").format("*"),
@@ -203,7 +201,6 @@ class GuiProjectLoad(QDialog):
def _doCancel(self):
"""Close the dialog window without doing anything.
"""
logger.verbose("GuiProjectLoad close button clicked")
self.openPath = None
self.openState = self.NONE_STATE
self.close()
@@ -212,7 +209,6 @@ class GuiProjectLoad(QDialog):
def _doNewProject(self):
"""Create a new project.
"""
logger.verbose("GuiProjectLoad new project button clicked")
self._saveSettings()
self.openPath = None
self.openState = self.NEW_STATE
+33 -32
View File
@@ -36,7 +36,7 @@ from PyQt5.QtWidgets import (
from novelwriter.enum import nwAlert
from novelwriter.common import simplified
from novelwriter.gui.custom import QSwitch, PagedDialog, QConfigLayout
from novelwriter.custom import QSwitch, PagedDialog, QConfigLayout
logger = logging.getLogger(__name__)
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
class GuiProjectSettings(PagedDialog):
def __init__(self, mainGui):
PagedDialog.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiProjectSettings ...")
self.setObjectName("GuiProjectSettings")
@@ -67,10 +67,10 @@ class GuiProjectSettings(PagedDialog):
self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH))
)
self.tabMain = GuiProjectEditMain(self.mainGui, self.theProject)
self.tabStatus = GuiProjectEditStatus(self.mainGui, self.theProject, True)
self.tabImport = GuiProjectEditStatus(self.mainGui, self.theProject, False)
self.tabReplace = GuiProjectEditReplace(self.mainGui, self.theProject)
self.tabMain = GuiProjectEditMain(self)
self.tabStatus = GuiProjectEditStatus(self, True)
self.tabImport = GuiProjectEditStatus(self, False)
self.tabReplace = GuiProjectEditReplace(self)
self.addTab(self.tabMain, self.tr("Settings"))
self.addTab(self.tabStatus, self.tr("Status"))
@@ -96,8 +96,6 @@ class GuiProjectSettings(PagedDialog):
def _doSave(self):
"""Save settings and close dialog.
"""
logger.verbose("GuiProjectSettings save button clicked")
projName = self.tabMain.editName.text()
bookTitle = self.tabMain.editTitle.text()
bookAuthors = self.tabMain.editAuthors.toPlainText()
@@ -166,12 +164,12 @@ class GuiProjectSettings(PagedDialog):
class GuiProjectEditMain(QWidget):
def __init__(self, mainGui, theProject):
QWidget.__init__(self, mainGui)
def __init__(self, projGui):
super().__init__(parent=projGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.theProject = theProject
self.mainGui = projGui.mainGui
self.theProject = projGui.theProject
# The Form
self.mainForm = QConfigLayout()
@@ -256,13 +254,13 @@ class GuiProjectEditStatus(QWidget):
COL_ROLE = Qt.UserRole + 1
NUM_ROLE = Qt.UserRole + 2
def __init__(self, mainGui, theProject, isStatus):
QWidget.__init__(self, mainGui)
def __init__(self, projGui, isStatus):
super().__init__(parent=projGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.theProject = theProject
self.mainTheme = mainGui.mainTheme
self.mainGui = projGui.mainGui
self.theProject = projGui.theProject
self.mainTheme = projGui.mainGui.mainTheme
if isStatus:
self.theStatus = self.theProject.statusItems
@@ -367,11 +365,12 @@ class GuiProjectEditStatus(QWidget):
newList = []
for n in range(self.listBox.topLevelItemCount()):
item = self.listBox.topLevelItem(n)
newList.append({
"key": item.data(self.COL_LABEL, self.KEY_ROLE),
"name": item.text(self.COL_LABEL),
"cols": item.data(self.COL_LABEL, self.COL_ROLE),
})
if item is not None:
newList.append({
"key": item.data(self.COL_LABEL, self.KEY_ROLE),
"name": item.text(self.COL_LABEL),
"cols": item.data(self.COL_LABEL, self.COL_ROLE),
})
return newList, self.colDeleted
return [], []
@@ -470,7 +469,8 @@ class GuiProjectEditStatus(QWidget):
self.listBox.insertTopLevelItem(nIndex, cItem)
self.listBox.clearSelection()
cItem.setSelected(True)
if cItem is not None:
cItem.setSelected(True)
self.colChanged = True
return
@@ -527,13 +527,13 @@ class GuiProjectEditReplace(QWidget):
COL_KEY = 0
COL_REPL = 1
def __init__(self, mainGui, theProject):
QWidget.__init__(self, mainGui)
def __init__(self, projGui):
super().__init__(parent=projGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.theProject = theProject
self.mainGui = projGui.mainGui
self.mainTheme = projGui.mainGui.mainTheme
self.theProject = projGui.theProject
self.arChanged = False
wCol0 = self.mainConf.pxInt(
@@ -619,10 +619,11 @@ class GuiProjectEditReplace(QWidget):
newList = {}
for n in range(self.listBox.topLevelItemCount()):
tItem = self.listBox.topLevelItem(n)
aKey = self._stripNotAllowed(tItem.text(0))
aVal = tItem.text(1)
if len(aKey) > 0:
newList[aKey] = aVal
if tItem is not None:
aKey = self._stripNotAllowed(tItem.text(0))
aVal = tItem.text(1)
if len(aKey) > 0:
newList[aKey] = aVal
return newList
+1 -1
View File
@@ -43,7 +43,7 @@ class GuiQuoteSelect(QDialog):
selectedQuote = ""
def __init__(self, parent=None, currentQuote='"'):
QDialog.__init__(self, parent=parent)
super().__init__(parent=parent)
self.mainConf = novelwriter.CONFIG
+2 -2
View File
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
class GuiUpdates(QDialog):
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiUpdates ...")
self.setObjectName("GuiUpdates")
@@ -135,7 +135,7 @@ class GuiUpdates(QDialog):
logException()
relVersion = rawData.get("tag_name", "Unknown")
relDate = rawData.get("created_at", None)
relDate = rawData.get("created_at", "")
try:
relDate = datetime.strptime(relDate[:10], "%Y-%m-%d").strftime("%x")
+4 -2
View File
@@ -43,7 +43,7 @@ logger = logging.getLogger(__name__)
class GuiWordList(QDialog):
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiWordList ...")
self.setObjectName("GuiWordList")
@@ -156,7 +156,9 @@ class GuiWordList(QDialog):
try:
with open(tmpFile, mode="w", encoding="utf-8") as outFile:
for i in range(self.listBox.count()):
outFile.write(self.listBox.item(i).text() + "\n")
item = self.listBox.item(i)
if item is not None:
outFile.write(item.text() + "\n")
except Exception:
logger.error("Could not save new word list")
+6 -5
View File
@@ -44,7 +44,8 @@ def logException():
"""Log the content of an exception message.
"""
exType, exValue, _ = sys.exc_info()
logger.error("%s: %s", exType.__name__, str(exValue))
if exType is not None:
logger.error("%s: %s", exType.__name__, str(exValue))
def formatException(exc):
@@ -61,7 +62,7 @@ def formatException(exc):
class NWErrorMessage(QDialog):
def __init__(self, parent):
QDialog.__init__(self, parent=parent)
super().__init__(parent=parent)
self.setObjectName("NWErrorMessage")
# Widgets
@@ -131,7 +132,7 @@ class NWErrorMessage(QDialog):
try:
import lxml
lxmlVersion = lxml.__version__
lxmlVersion = lxml.__version__ # type: ignore
except Exception:
lxmlVersion = "Unknown"
@@ -198,8 +199,8 @@ def exceptionHandler(exType, exValue, exTrace):
try:
# Try a controlled shutdown
nwGUI.closeProject(isYes=True)
nwGUI.closeMain()
nwGUI.closeProject(isYes=True) # type: ignore
nwGUI.closeMain() # type: ignore
logger.info("Emergency shutdown successful")
except Exception as exc:
+57 -65
View File
@@ -73,7 +73,7 @@ class GuiDocEditor(QTextEdit):
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, mainGui):
QTextEdit.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocEditor ...")
@@ -303,10 +303,7 @@ class GuiDocEditor(QTextEdit):
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
# Refresh the tab stops
if self.mainConf.verQtValue >= 51000:
self.setTabStopDistance(self.mainConf.getTabWidth())
else: # pragma: no cover
self.setTabStopWidth(self.mainConf.getTabWidth())
self.setTabStopDistance(self.mainConf.getTabWidth())
# Initialise the syntax highlighter
self.highLight.initHighlighter()
@@ -511,12 +508,12 @@ class GuiDocEditor(QTextEdit):
self.setDocumentChanged(False)
oldHeader = self.theProject.index.getHandleHeaderLevel(tHandle)
oldHeader = self._nwItem.mainHeading
self.theProject.index.scanText(tHandle, docText)
newHeader = self.theProject.index.getHandleHeaderLevel(tHandle)
newHeader = self._nwItem.mainHeading
# ToDo: This should be a signal
if self._updateHeaders(checkLevel=True):
if self._updateHeaders():
self.mainGui.requestNovelTreeRefresh()
else:
self.mainGui.novelView.updateWordCounts(tHandle)
@@ -604,19 +601,15 @@ class GuiDocEditor(QTextEdit):
##
def getText(self):
"""Get the text content of the current document. This method
uses QTextEdit->toPlainText for Qt versions lower than 5.9, and
the QTextDocument->toRawText for higher version. The latter
preserves non-breaking spaces, which the former does not.
We still want to get rid of page and line separators though.
"""Get the text content of the current document. This method uses
QTextDocument->toRawText instead of toPlainText(). The former preserves
non-breaking spaces, the latter does not. We still want to get rid of
page and line separators though.
See: https://doc.qt.io/qt-5/qtextdocument.html#toPlainText
"""
if self.mainConf.verQtValue >= 50900:
theText = self.document().toRawText()
theText = theText.replace(nwUnicode.U_LSEP, "\n") # Line separators
theText = theText.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
else:
theText = self.toPlainText()
theText = self.document().toRawText()
theText = theText.replace(nwUnicode.U_LSEP, "\n") # Line separators
theText = theText.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
return theText
def getCursorPosition(self):
@@ -671,7 +664,7 @@ class GuiDocEditor(QTextEdit):
if theBlock:
self.setCursorPosition(theBlock.position())
self.docFooter.updateLineCount()
logger.verbose("Cursor moved to line %d", theLine)
logger.debug("Cursor moved to line %d", theLine)
return True
@@ -725,7 +718,7 @@ class GuiDocEditor(QTextEdit):
if not self._bigDoc:
self.spellCheckDocument()
logger.verbose("Spell check is set to '%s'", str(theMode))
logger.debug("Spell check is set to '%s'", str(theMode))
return True
@@ -735,7 +728,7 @@ class GuiDocEditor(QTextEdit):
of Qt 5.13, is to clear the text and put it back. This clears
the undo stack, so we only do it for big documents.
"""
logger.verbose("Running spell checker")
logger.debug("Running spell checker")
if self._spellCheck:
bfTime = time()
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
@@ -747,7 +740,7 @@ class GuiDocEditor(QTextEdit):
qApp.restoreOverrideCursor()
afTime = time()
logger.debug("Document highlighted in %.3f ms", 1000*(afTime-bfTime))
self.mainGui.statusBar.setStatus(self.tr("Spell check complete"))
self.mainGui.mainStatus.setStatus(self.tr("Spell check complete"))
return True
@@ -770,7 +763,7 @@ class GuiDocEditor(QTextEdit):
logger.error("Not a document action")
return False
logger.verbose("Requesting action: %s", theAction.name)
logger.debug("Requesting action: %s", theAction.name)
self._allowAutoReplace(False)
if theAction == nwDocAction.UNDO:
@@ -951,7 +944,7 @@ class GuiDocEditor(QTextEdit):
logger.error("Invalid keyword '%s'", keyWord)
return False
logger.verbose("Inserting keyword '%s'", keyWord)
logger.debug("Inserting keyword '%s'", keyWord)
theState = self.insertNewBlock("%s: " % keyWord)
return theState
@@ -1003,7 +996,7 @@ class GuiDocEditor(QTextEdit):
if self.mainConf.autoScroll:
cOld = self.cursorRect().center().y()
QTextEdit.keyPressEvent(self, keyEvent)
super().keyPressEvent(keyEvent)
kMod = keyEvent.modifiers()
okMod = kMod == Qt.NoModifier or kMod == Qt.ShiftModifier
@@ -1022,7 +1015,7 @@ class GuiDocEditor(QTextEdit):
doAnim.start()
else:
QTextEdit.keyPressEvent(self, keyEvent)
super().keyPressEvent(keyEvent)
self.docFooter.updateLineCount()
@@ -1049,7 +1042,7 @@ class GuiDocEditor(QTextEdit):
theCursor = self.cursorForPosition(theEvent.pos())
self._followTag(theCursor)
QTextEdit.mouseReleaseEvent(self, theEvent)
super().mouseReleaseEvent(theEvent)
self.docFooter.updateLineCount()
return
@@ -1059,7 +1052,7 @@ class GuiDocEditor(QTextEdit):
has its margins adjusted according to user preferences.
"""
self.updateDocMargins()
QTextEdit.resizeEvent(self, theEvent)
super().resizeEvent(theEvent)
return
##
@@ -1179,7 +1172,7 @@ class GuiDocEditor(QTextEdit):
spellCheck &= theWord != ""
if spellCheck:
logger.verbose("Looking up '%s' in the dictionary", theWord)
logger.debug("Looking up '%s' in the dictionary", theWord)
spellCheck &= not self.spEnchant.checkWord(theWord)
if spellCheck:
@@ -1245,11 +1238,11 @@ class GuiDocEditor(QTextEdit):
return
if self.wCounterDoc.isRunning():
logger.verbose("Word counter is busy")
logger.debug("Word counter is busy")
return
if time() - self._lastEdit < 5 * self.wcInterval:
logger.verbose("Running word counter")
logger.debug("Running word counter")
self.mainGui.threadPool.start(self.wCounterDoc)
return
@@ -1261,7 +1254,7 @@ class GuiDocEditor(QTextEdit):
if self._docHandle is None or self._nwItem is None:
return
logger.verbose("Updating word count")
logger.debug("Updating word count")
self._charCount = cCount
self._wordCount = wCount
@@ -1304,7 +1297,7 @@ class GuiDocEditor(QTextEdit):
return
if self.wCounterSel.isRunning():
logger.verbose("Selection word counter is busy")
logger.debug("Selection word counter is busy")
return
self.mainGui.threadPool.start(self.wCounterSel)
@@ -1318,7 +1311,7 @@ class GuiDocEditor(QTextEdit):
if self._docHandle is None or self._nwItem is None:
return
logger.verbose("User selectee %d words", wCount)
logger.debug("User selectee %d words", wCount)
self.docFooter.updateCounts(wCount=wCount, cCount=cCount)
self.wcTimerSel.stop()
@@ -1336,11 +1329,11 @@ class GuiDocEditor(QTextEdit):
QPointF(theSize.width(), theSize.height()), Qt.FuzzyHit
)
if self._queuePos <= thePos:
logger.verbose("Allowed cursor move to %d <= %d", self._queuePos, thePos)
logger.debug("Allowed cursor move to %d <= %d", self._queuePos, thePos)
self.setCursorPosition(self._queuePos)
self._queuePos = None
else:
logger.verbose("Denied cursor move to %d > %d", self._queuePos, thePos)
logger.debug("Denied cursor move to %d > %d", self._queuePos, thePos)
return
@@ -1527,7 +1520,7 @@ class GuiDocEditor(QTextEdit):
theCursor.endEditBlock()
theCursor.setPosition(theCursor.selectionEnd())
self.setTextCursor(theCursor)
logger.verbose(
logger.debug(
"Replaced occurrence of '%s' with '%s' on line %d",
searchFor, replWith, theCursor.blockNumber()
)
@@ -1905,10 +1898,10 @@ class GuiDocEditor(QTextEdit):
return False
if loadTag:
logger.verbose("Attempting to follow tag '%s'", theWord)
logger.debug("Attempting to follow tag '%s'", theWord)
self.loadDocumentTagRequest.emit(theWord, nwDocMode.VIEW)
else:
logger.verbose("Potential tag '%s'", theWord)
logger.debug("Potential tag '%s'", theWord)
return True
@@ -1992,13 +1985,22 @@ class GuiDocEditor(QTextEdit):
nDelete = 3
tInsert = nwUnicode.U_HELLIP
elif theOne == nwUnicode.U_LSEP:
# This resolves issue #1150
nDelete = 1
tInsert = nwUnicode.U_PSEP
tCheck = tInsert
if tCheck in self.mainConf.fmtPadBefore:
if self.mainConf.fmtPadBefore and tCheck in self.mainConf.fmtPadBefore:
if self._allowSpaceBeforeColon(theText, tCheck):
nDelete = max(nDelete, 1)
chkPos = thePos - nDelete - 1
if chkPos >= 0 and theText[chkPos].isspace():
# Strip existing space before inserting a new (#1061)
nDelete += 1
tInsert = self._typPadChar + tInsert
if tCheck in self.mainConf.fmtPadAfter:
if self.mainConf.fmtPadAfter and tCheck in self.mainConf.fmtPadAfter:
if self._allowSpaceBeforeColon(theText, tCheck):
nDelete = max(nDelete, 1)
tInsert = tInsert + self._typPadChar
@@ -2023,7 +2025,7 @@ class GuiDocEditor(QTextEdit):
return False
return True
def _updateHeaders(self, checkPos=False, checkLevel=False):
def _updateHeaders(self):
"""Update the headers record and return True if anything
changed, if a check flag was provided.
"""
@@ -2031,21 +2033,12 @@ class GuiDocEditor(QTextEdit):
return False
newHeaders = self.theProject.index.getHandleHeaders(self._docHandle)
if checkPos:
newPos = [x[0] for x in newHeaders]
oldPos = [x[0] for x in self._docHeaders]
if checkLevel:
newLev = [x[1] for x in newHeaders]
oldLev = [x[1] for x in self._docHeaders]
newLev = [x[1] for x in newHeaders]
oldLev = [x[1] for x in self._docHeaders]
self._docHeaders = newHeaders
if checkPos:
return newPos != oldPos
if checkLevel:
return newLev != oldLev
return False
return newLev != oldLev
def _checkDocSize(self, theSize):
"""Check if document size crosses the big document limit set in
@@ -2152,7 +2145,7 @@ class GuiDocEditor(QTextEdit):
class BackgroundWordCounter(QRunnable):
def __init__(self, docEditor, forSelection=False):
QRunnable.__init__(self)
super().__init__()
self._docEditor = docEditor
self._forSelection = forSelection
@@ -2202,7 +2195,7 @@ class BackgroundWordCounterSignals(QObject):
class GuiDocEditSearch(QFrame):
def __init__(self, docEditor):
QFrame.__init__(self, docEditor)
super().__init__(parent=docEditor)
logger.debug("Initialising GuiDocEditSearch ...")
@@ -2429,7 +2422,6 @@ class GuiDocEditSearch(QFrame):
self.searchBox.selectAll()
if self.isRegEx:
self._alertSearchValid(True)
logger.verbose("Setting search text to '%s'", theText)
return True
def setReplaceText(self, theText):
@@ -2469,7 +2461,8 @@ class GuiDocEditSearch(QFrame):
self._alertSearchValid(theRegEx.isValid())
return theRegEx
else: # >= 50300 to < 51300
else: # pragma: no cover
# >= 50300 to < 51300
if self.isCaseSense:
rxOpt = Qt.CaseSensitive
else:
@@ -2590,7 +2583,7 @@ class GuiDocEditSearch(QFrame):
class GuiDocEditHeader(QWidget):
def __init__(self, docEditor):
QWidget.__init__(self, docEditor)
super().__init__(parent=docEditor)
logger.debug("Initialising GuiDocEditHeader ...")
@@ -2636,7 +2629,7 @@ class GuiDocEditHeader(QWidget):
self.editButton.setStyleSheet(buttonStyle)
self.editButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.editButton.setVisible(False)
self.editButton.setToolTip(self.tr("Edit document meta"))
self.editButton.setToolTip(self.tr("Edit document label"))
self.editButton.clicked.connect(self._editDocument)
self.searchButton = QToolButton(self)
@@ -2814,7 +2807,7 @@ class GuiDocEditHeader(QWidget):
class GuiDocEditFooter(QWidget):
def __init__(self, docEditor):
QWidget.__init__(self, docEditor)
super().__init__(parent=docEditor)
logger.debug("Initialising GuiDocEditFooter ...")
@@ -2942,7 +2935,7 @@ class GuiDocEditFooter(QWidget):
"""
self._docHandle = tHandle
if self._docHandle is None:
logger.verbose("No handle set, so clearing the editor footer")
logger.debug("No handle set, so clearing the editor footer")
self._theItem = None
else:
self._theItem = self.theProject.tree[self._docHandle]
@@ -2969,8 +2962,7 @@ class GuiDocEditFooter(QWidget):
else:
theStatus, theIcon = self._theItem.getImportStatus()
sIcon = theIcon.pixmap(self.sPx, self.sPx)
hLevel = self.theProject.index.getHandleHeaderLevel(self._docHandle)
sText = f"{theStatus} / {self._theItem.describeMe(hLevel)}"
sText = f"{theStatus} / {self._theItem.describeMe()}"
self.statusIcon.setPixmap(sIcon)
self.statusText.setText(sText)
+1 -1
View File
@@ -47,7 +47,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
BLOCK_TITLE = 4
def __init__(self, theDoc, mainGui, spEnchant):
QSyntaxHighlighter.__init__(self, theDoc)
super().__init__(theDoc)
logger.debug("Initialising GuiDocHighlighter ...")
self.mainConf = novelwriter.CONFIG
+23 -29
View File
@@ -54,7 +54,7 @@ class GuiDocViewer(QTextBrowser):
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, mainGui):
QTextBrowser.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocViewer ...")
@@ -150,10 +150,7 @@ class GuiDocViewer(QTextBrowser):
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
# Refresh the tab stops
if self.mainConf.verQtValue >= 51000:
self.setTabStopDistance(self.mainConf.getTabWidth())
else:
self.setTabStopWidth(self.mainConf.getTabWidth())
self.setTabStopDistance(self.mainConf.getTabWidth())
# If we have a document open, we should reload it in case the font changed
if self._docHandle is not None:
@@ -193,10 +190,7 @@ class GuiDocViewer(QTextBrowser):
return False
# Refresh the tab stops
if self.mainConf.verQtValue >= 51000:
self.setTabStopDistance(self.mainConf.getTabWidth())
else:
self.setTabStopWidth(self.mainConf.getTabWidth())
self.setTabStopDistance(self.mainConf.getTabWidth())
# Must be before setHtml
if updateHistory:
@@ -247,7 +241,7 @@ class GuiDocViewer(QTextBrowser):
"""Wrapper function for various document actions on the current
document.
"""
logger.verbose("Requesting action: '%s'", theAction.name)
logger.debug("Requesting action: '%s'", theAction.name)
if self._docHandle is None:
logger.error("No document open")
return False
@@ -270,7 +264,7 @@ class GuiDocViewer(QTextBrowser):
if not isinstance(tAnchor, str):
return False
if tAnchor.startswith("#"):
logger.verbose("Moving to anchor '%s'", tAnchor)
logger.debug("Moving to anchor '%s'", tAnchor)
self.setSource(QUrl(tAnchor))
return True
@@ -356,7 +350,7 @@ class GuiDocViewer(QTextBrowser):
theBlock = self.document().findBlockByLineNumber(theLine)
if theBlock:
self.setCursorPosition(theBlock.position())
logger.verbose("Cursor moved to line %d", theLine)
logger.debug("Cursor moved to line %d", theLine)
return True
def setScrollPosition(self, thePos):
@@ -402,7 +396,7 @@ class GuiDocViewer(QTextBrowser):
"""Process a clicked link internally in the document.
"""
theLink = theURL.url()
logger.verbose("Clicked link: '%s'", theLink)
logger.debug("Clicked link: '%s'", theLink)
if len(theLink) > 0:
theBits = theLink.split("=")
if len(theBits) == 2:
@@ -461,7 +455,7 @@ class GuiDocViewer(QTextBrowser):
has its margins adjusted according to user preferences.
"""
self.updateDocMargins()
QTextBrowser.resizeEvent(self, theEvent)
super().resizeEvent(theEvent)
return
def mouseReleaseEvent(self, theEvent):
@@ -472,7 +466,7 @@ class GuiDocViewer(QTextBrowser):
elif theEvent.button() == Qt.ForwardButton:
self.navForward()
else:
QTextBrowser.mouseReleaseEvent(self, theEvent)
super().mouseReleaseEvent(theEvent)
return
##
@@ -568,7 +562,7 @@ class GuiDocViewer(QTextBrowser):
# END Class GuiDocViewer
class GuiDocViewHistory():
class GuiDocViewHistory:
def __init__(self, docViewer):
@@ -584,7 +578,7 @@ class GuiDocViewHistory():
def clear(self):
"""Clear the view history.
"""
logger.verbose("View history cleared")
logger.debug("View history cleared")
self._navHistory = []
self._posHistory = []
self._currPos = -1
@@ -598,7 +592,7 @@ class GuiDocViewHistory():
"""
if self._currPos >= 0 and self._currPos < len(self._navHistory):
if tHandle == self._navHistory[self._currPos]:
logger.verbose("Not updating view hsitory")
logger.debug("Not updating view hsitory")
return False
self._truncateHistory(self._currPos)
@@ -613,7 +607,7 @@ class GuiDocViewHistory():
self._dumpHistory()
logger.verbose("Added '%s' to view history", tHandle)
logger.debug("Added '%s' to view history", tHandle)
return True
@@ -622,7 +616,7 @@ class GuiDocViewHistory():
"""
newPos = self._currPos + 1
if newPos < len(self._navHistory):
logger.verbose("Move forward in view history")
logger.debug("Move forward in view history")
self._prevPos = self._currPos
self._updateScrollBar()
@@ -640,7 +634,7 @@ class GuiDocViewHistory():
"""
newPos = self._currPos - 1
if newPos >= 0:
logger.verbose("Move backward in view history")
logger.debug("Move backward in view history")
self._prevPos = self._currPos
self._updateScrollBar()
@@ -686,11 +680,11 @@ class GuiDocViewHistory():
def _dumpHistory(self):
"""Debug function to dump history to the logger. Since it is a
for loop, it is skipped entirely if log level isn't VERBOSE.
for loop, it is skipped entirely if log level isn't DEBUG.
"""
if logger.getEffectiveLevel() < logging.DEBUG:
if logger.getEffectiveLevel() == logging.DEBUG:
for i, (h, p) in enumerate(zip(self._navHistory, self._posHistory)):
logger.verbose(
logger.debug(
"History %02d: %s %13s [x:%d]" % (
i + 1, ">" if i == self._currPos else " ", h, p
)
@@ -708,7 +702,7 @@ class GuiDocViewHistory():
class GuiDocViewHeader(QWidget):
def __init__(self, docViewer):
QWidget.__init__(self, docViewer)
super().__init__(parent=docViewer)
logger.debug("Initialising GuiDocViewHeader ...")
@@ -915,7 +909,7 @@ class GuiDocViewHeader(QWidget):
class GuiDocViewFooter(QWidget):
def __init__(self, docViewer):
QWidget.__init__(self, docViewer)
super().__init__(parent=docViewer)
logger.debug("Initialising GuiDocViewFooter ...")
@@ -1110,7 +1104,7 @@ class GuiDocViewFooter(QWidget):
def _doToggleSticky(self, theState):
"""Toggle the sticky flag for the reference panel.
"""
logger.verbose("Reference sticky is %s", str(theState))
logger.debug("Reference sticky is %s", str(theState))
self.docViewer.stickyRef = theState
if not theState and self.docViewer.docHandle() is not None:
self.viewMeta.refreshReferences(self.docViewer.docHandle())
@@ -1141,7 +1135,7 @@ class GuiDocViewFooter(QWidget):
class GuiDocViewDetails(QScrollArea):
def __init__(self, mainGui):
QScrollArea.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocViewDetails ...")
self.mainConf = novelwriter.CONFIG
@@ -1205,7 +1199,7 @@ class GuiDocViewDetails(QScrollArea):
"""Capture the link-click and forward it to the document viewer
class for handling.
"""
logger.verbose("Clicked link: '%s'", theLink)
logger.debug("Clicked link: '%s'", theLink)
if len(theLink) == 21:
tHandle = theLink[:13]
tAnchor = theLink[13:]
+4 -5
View File
@@ -38,7 +38,7 @@ logger = logging.getLogger(__name__)
class GuiItemDetails(QWidget):
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiItemDetails ...")
self.mainConf = novelwriter.CONFIG
@@ -247,7 +247,7 @@ class GuiItemDetails(QWidget):
theLabel = theLabel[:96].rstrip()+" ..."
if nwItem.isFileType():
if nwItem.isExported:
if nwItem.isActive:
self.labelIcon.setPixmap(self._expCheck)
else:
self.labelIcon.setPixmap(self._expCross)
@@ -273,12 +273,11 @@ class GuiItemDetails(QWidget):
# Layout
# ======
hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
usageIcon = self.mainTheme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading
)
self.usageIcon.setPixmap(usageIcon.pixmap(iPx, iPx))
self.usageData.setText(nwItem.describeMe(hLevel))
self.usageData.setText(nwItem.describeMe())
# Counts
# ======
+7 -21
View File
@@ -40,9 +40,13 @@ logger = logging.getLogger(__name__)
class GuiMainMenu(QMenuBar):
"""The GUI main menu. All menu actions are defined here with the
main menu as the owner. Each widget that need them elsewhere need to
add them from this class.
"""
def __init__(self, mainGui):
QMenuBar.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiMainMenu ...")
self.mainConf = novelwriter.CONFIG
@@ -79,12 +83,6 @@ class GuiMainMenu(QMenuBar):
self.aSpellCheck.setChecked(theMode)
return
def setFocusMode(self, theMode):
"""Forward focus mode check state to its action.
"""
self.aFocusMode.setChecked(theMode)
return
##
# Slots
##
@@ -164,14 +162,14 @@ class GuiMainMenu(QMenuBar):
# Project > Edit
self.aEditItem = QAction(self.tr("Rename Item"), self)
self.aEditItem.setShortcuts(["F2"])
self.aEditItem.setShortcut("F2")
self.aEditItem.triggered.connect(lambda: self.mainGui.editItemLabel(None))
self.projMenu.addAction(self.aEditItem)
# Project > Delete
self.aDeleteItem = QAction(self.tr("Delete Item"), self)
self.aDeleteItem.setShortcut("Ctrl+Shift+Del")
self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.deleteItem(None))
self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.requestDeleteItem(None))
self.projMenu.addAction(self.aDeleteItem)
# Project > Empty Trash
@@ -244,16 +242,6 @@ class GuiMainMenu(QMenuBar):
self.aImportFile.triggered.connect(lambda: self.mainGui.importDocument())
self.docuMenu.addAction(self.aImportFile)
# Document > Merge Documents
self.aMergeDocs = QAction(self.tr("Merge Folder to Document"), self)
self.aMergeDocs.triggered.connect(lambda: self.mainGui.mergeDocuments())
self.docuMenu.addAction(self.aMergeDocs)
# Document > Split Document
self.aSplitDoc = QAction(self.tr("Split Document to Folder"), self)
self.aSplitDoc.triggered.connect(lambda: self.mainGui.splitDocument())
self.docuMenu.addAction(self.aSplitDoc)
return
def _buildEditMenu(self):
@@ -375,8 +363,6 @@ class GuiMainMenu(QMenuBar):
# View > Focus Mode
self.aFocusMode = QAction(self.tr("Focus Mode"), self)
self.aFocusMode.setShortcut("F8")
self.aFocusMode.setCheckable(True)
self.aFocusMode.setChecked(self.mainGui.isFocusMode)
self.aFocusMode.triggered.connect(lambda: self.mainGui.toggleFocusMode())
self.viewMenu.addAction(self.aFocusMode)
+24 -28
View File
@@ -63,7 +63,7 @@ class GuiNovelView(QWidget):
openDocumentRequest = pyqtSignal(str, Enum, int, str)
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
super().__init__(parent=mainGui)
self.mainGui = mainGui
self.theProject = mainGui.theProject
@@ -166,7 +166,7 @@ class GuiNovelView(QWidget):
class GuiNovelToolBar(QWidget):
def __init__(self, novelView):
QTreeWidget.__init__(self, novelView)
super().__init__(parent=novelView)
logger.debug("Initialising GuiNovelToolBar ...")
@@ -333,7 +333,7 @@ class GuiNovelTree(QTreeWidget):
D_KEY = Qt.UserRole + 2
def __init__(self, novelView):
QTreeWidget.__init__(self, novelView)
super().__init__(parent=novelView)
logger.debug("Initialising GuiNovelTree ...")
@@ -390,13 +390,6 @@ class GuiNovelTree(QTreeWidget):
fH2.setBold(True)
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
self._pIndent = [
self.mainTheme.loadDecoration("deco_doc_h0", pxH=iPx),
self.mainTheme.loadDecoration("deco_doc_h1", pxH=iPx),
self.mainTheme.loadDecoration("deco_doc_h2", pxH=iPx),
self.mainTheme.loadDecoration("deco_doc_h3", pxH=iPx),
self.mainTheme.loadDecoration("deco_doc_h4", pxH=iPx),
]
self._pMore = self.mainTheme.loadDecoration("deco_doc_more", pxH=iPx)
# Connect signals
@@ -450,14 +443,14 @@ class GuiNovelTree(QTreeWidget):
def refreshTree(self, rootHandle=None, overRide=False):
"""Called whenever the Novel tab is activated.
"""
logger.verbose("Requesting refresh of the novel tree")
logger.debug("Requesting refresh of the novel tree")
if rootHandle is None:
rootHandle = self.theProject.tree.findRoot(nwItemClass.NOVEL)
treeChanged = self.mainGui.projView.changedSince(self._lastBuild)
indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
if not (treeChanged or indexChanged or overRide):
logger.verbose("No changes have been made to the novel index")
logger.debug("No changes have been made to the novel index")
return
selItem = self.selectedItems()
@@ -515,18 +508,19 @@ class GuiNovelTree(QTreeWidget):
self._actHandle = tHandle
for i in range(self.topLevelItemCount()):
tItem = self.topLevelItem(i)
if tItem.data(self.C_TITLE, self.D_HANDLE) == tHandle:
tItem.setBackground(self.C_TITLE, self.palette().alternateBase())
tItem.setBackground(self.C_WORDS, self.palette().alternateBase())
tItem.setBackground(self.C_EXTRA, self.palette().alternateBase())
tItem.setBackground(self.C_MORE, self.palette().alternateBase())
else:
tItem.setBackground(self.C_TITLE, self.palette().base())
tItem.setBackground(self.C_WORDS, self.palette().base())
tItem.setBackground(self.C_EXTRA, self.palette().base())
tItem.setBackground(self.C_MORE, self.palette().base())
if tItem is not None:
if tItem.data(self.C_TITLE, self.D_HANDLE) == tHandle:
tItem.setBackground(self.C_TITLE, self.palette().alternateBase())
tItem.setBackground(self.C_WORDS, self.palette().alternateBase())
tItem.setBackground(self.C_EXTRA, self.palette().alternateBase())
tItem.setBackground(self.C_MORE, self.palette().alternateBase())
else:
tItem.setBackground(self.C_TITLE, self.palette().base())
tItem.setBackground(self.C_WORDS, self.palette().base())
tItem.setBackground(self.C_EXTRA, self.palette().base())
tItem.setBackground(self.C_MORE, self.palette().base())
logger.verbose("Highlighted Novel Tree in %.3f ms", (time() - tStart)*1000)
logger.debug("Highlighted Novel Tree in %.3f ms", (time() - tStart)*1000)
return
@@ -539,7 +533,7 @@ class GuiNovelTree(QTreeWidget):
mouse in a blank area of the tree view, and to load a document
for viewing if the user middle-clicked.
"""
QTreeWidget.mousePressEvent(self, theEvent)
super().mousePressEvent(theEvent)
if theEvent.button() == Qt.LeftButton:
selItem = self.indexAt(theEvent.pos())
@@ -562,7 +556,7 @@ class GuiNovelTree(QTreeWidget):
def focusOutEvent(self, theEvent):
"""Clear the selection when the tree no longer has focus.
"""
QTreeWidget.focusOutEvent(self, theEvent)
super().focusOutEvent(theEvent)
self.clearSelection()
return
@@ -610,7 +604,7 @@ class GuiNovelTree(QTreeWidget):
"""
self.clearContent()
tStart = time()
logger.verbose("Building novel tree for root item '%s'", rootHandle)
logger.debug("Building novel tree for root item '%s'", rootHandle)
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
for tKey, tHandle, sTitle, novIdx in novStruct:
@@ -619,8 +613,10 @@ class GuiNovelTree(QTreeWidget):
if iLevel == 0:
continue
hDec = self.mainTheme.getHeaderDecoration(iLevel)
newItem = QTreeWidgetItem()
newItem.setData(self.C_TITLE, Qt.DecorationRole, self._pIndent[iLevel])
newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
newItem.setText(self.C_TITLE, novIdx.title)
newItem.setData(self.C_TITLE, self.D_HANDLE, tHandle)
newItem.setData(self.C_TITLE, self.D_TITLE, sTitle)
@@ -641,7 +637,7 @@ class GuiNovelTree(QTreeWidget):
self.setActiveHandle(self._actHandle)
logger.verbose("Novel Tree built in %.3f ms", (time() - tStart)*1000)
logger.debug("Novel Tree built in %.3f ms", (time() - tStart)*1000)
self._lastBuild = time()
return
+14 -22
View File
@@ -57,7 +57,7 @@ class GuiOutlineView(QWidget):
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
super().__init__(parent=mainGui)
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
@@ -198,7 +198,7 @@ class GuiOutlineToolBar(QToolBar):
viewColumnToggled = pyqtSignal(bool, Enum)
def __init__(self, theOutline):
QTreeWidget.__init__(self, theOutline)
super().__init__(parent=theOutline)
logger.debug("Initialising GuiOutlineToolBar ...")
@@ -357,7 +357,7 @@ class GuiOutlineTree(QTreeWidget):
activeItemChanged = pyqtSignal(str, str)
def __init__(self, theOutline):
QTreeWidget.__init__(self, theOutline)
super().__init__(parent=theOutline)
logger.debug("Initialising GuiOutlineTree ...")
@@ -391,13 +391,6 @@ class GuiOutlineTree(QTreeWidget):
fH2.setBold(True)
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
self._pIndent = [
self.mainTheme.loadDecoration("deco_doc_h0", pxH=iPx),
self.mainTheme.loadDecoration("deco_doc_h1", pxH=iPx),
self.mainTheme.loadDecoration("deco_doc_h2", pxH=iPx),
self.mainTheme.loadDecoration("deco_doc_h3", pxH=iPx),
self.mainTheme.loadDecoration("deco_doc_h4", pxH=iPx),
]
self._dIcon = {
"H0": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H0"),
"H1": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H1"),
@@ -491,7 +484,7 @@ class GuiOutlineTree(QTreeWidget):
# was last built, we rebuild the tree from the updated index.
indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
if not (novelChanged or indexChanged or overRide):
logger.verbose("No changes have been made to the novel index")
logger.debug("No changes have been made to the novel index")
return
self._populateTree(rootHandle)
@@ -561,11 +554,9 @@ class GuiOutlineTree(QTreeWidget):
"""Receive the changes to column visibility forwarded by the
column selection menu.
"""
logger.verbose("User toggled Outline column '%s'", theItem.name)
if theItem in self._colIdx:
self.setColumnHidden(self._colIdx[theItem], not isChecked)
self._saveHeaderState()
return
##
@@ -684,28 +675,29 @@ class GuiOutlineTree(QTreeWidget):
self.setColumnHidden(self._colIdx[nwOutline.TITLE], False)
headItem = self.headerItem()
headItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
if headItem is not None:
headItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
for _, tHandle, sTitle, novIdx in novStruct:
iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0)
dLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
if iLevel == 0:
continue
trItem = QTreeWidgetItem()
nwItem = self.theProject.tree[tHandle]
hDec = self.mainTheme.getHeaderDecoration(iLevel)
trItem.setData(self._colIdx[nwOutline.TITLE], Qt.DecorationRole, self._pIndent[iLevel])
trItem.setData(self._colIdx[nwOutline.TITLE], Qt.DecorationRole, hDec)
trItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title)
trItem.setData(self._colIdx[nwOutline.TITLE], self.D_HANDLE, tHandle)
trItem.setData(self._colIdx[nwOutline.TITLE], self.D_TITLE, sTitle)
trItem.setFont(self._colIdx[nwOutline.TITLE], self._hFonts[iLevel])
trItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level)
trItem.setIcon(self._colIdx[nwOutline.LABEL], self._dIcon[dLevel])
trItem.setIcon(self._colIdx[nwOutline.LABEL], self._dIcon[nwItem.mainHeading])
trItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName)
trItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0"))
trItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis)
@@ -741,7 +733,7 @@ class GuiOutlineHeaderMenu(QMenu):
columnToggled = pyqtSignal(bool, Enum)
def __init__(self, theOutline):
QMenu.__init__(self, theOutline)
super().__init__(parent=theOutline)
self.acceptToggle = True
@@ -792,7 +784,7 @@ class GuiOutlineDetails(QScrollArea):
itemTagClicked = pyqtSignal(str)
def __init__(self, theOutline):
QScrollArea.__init__(self, theOutline)
super().__init__(parent=theOutline)
logger.debug("Initialising GuiOutlineDetails ...")
@@ -1048,7 +1040,7 @@ class GuiOutlineDetails(QScrollArea):
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText(novIdx.title)
itemStatus, _ = nwItem.getImportStatus()
itemStatus, _ = nwItem.getImportStatus(incIcon=False)
self.fileValue.setText(nwItem.itemName)
self.itemValue.setText(itemStatus)
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -42,7 +42,7 @@ logger = logging.getLogger(__name__)
class GuiMainStatus(QStatusBar):
def __init__(self, mainGui):
QStatusBar.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiMainStatus ...")
+45 -25
View File
@@ -38,7 +38,7 @@ from PyQt5.QtGui import (
from novelwriter.enum import nwItemLayout, nwItemType
from novelwriter.error import logException
from novelwriter.common import NWConfigParser, readTextFile
from novelwriter.common import NWConfigParser, minmax, readTextFile
from novelwriter.constants import nwLabels
logger = logging.getLogger(__name__)
@@ -120,11 +120,13 @@ class GuiTheme:
self._availThemes = {}
self._availSyntax = {}
self._listConf(self._availSyntax, os.path.join(self.mainConf.dataPath, "syntax"))
self._listConf(self._availSyntax, os.path.join(self.mainConf.assetPath, "syntax"))
self._listConf(self._availThemes, os.path.join(self.mainConf.dataPath, "themes"))
self._listConf(self._availThemes, os.path.join(self.mainConf.assetPath, "themes"))
if self.mainConf.dataPath: # Not guaranteed to be set
self._listConf(self._availSyntax, os.path.join(self.mainConf.dataPath, "syntax"))
self._listConf(self._availThemes, os.path.join(self.mainConf.dataPath, "themes"))
self.updateFont()
self.updateTheme()
self.iconCache.updateTheme()
@@ -134,13 +136,14 @@ class GuiTheme:
self.getPixmap = self.iconCache.getPixmap
self.getItemIcon = self.iconCache.getItemIcon
self.loadDecoration = self.iconCache.loadDecoration
self.getHeaderDecoration = self.iconCache.getHeaderDecoration
# Extract Other Info
self.guiDPI = qApp.primaryScreen().logicalDotsPerInchX()
self.guiScale = qApp.primaryScreen().logicalDotsPerInchX()/96.0
self.mainConf.guiScale = self.guiScale
logger.verbose("GUI DPI: %.1f", self.guiDPI)
logger.verbose("GUI Scale: %.2f", self.guiScale)
logger.debug("GUI DPI: %.1f", self.guiDPI)
logger.debug("GUI Scale: %.2f", self.guiScale)
# Fonts
self.guiFont = qApp.font()
@@ -157,12 +160,12 @@ class GuiTheme:
self.guiFontFixed.setPointSizeF(0.95*self.fontPointSize)
self.guiFontFixed.setFamily(QFontDatabase.systemFont(QFontDatabase.FixedFont).family())
logger.verbose("GUI Font Family: %s", self.guiFont.family())
logger.verbose("GUI Font Point Size: %.2f", self.fontPointSize)
logger.verbose("GUI Font Pixel Size: %d", self.fontPixelSize)
logger.verbose("GUI Base Icon Size: %d", self.baseIconSize)
logger.verbose("Text 'N' Height: %d", self.textNHeight)
logger.verbose("Text 'N' Width: %d", self.textNWidth)
logger.debug("GUI Font Family: %s", self.guiFont.family())
logger.debug("GUI Font Point Size: %.2f", self.fontPointSize)
logger.debug("GUI Font Pixel Size: %d", self.fontPixelSize)
logger.debug("GUI Base Icon Size: %d", self.baseIconSize)
logger.debug("Text 'N' Height: %d", self.textNHeight)
logger.debug("Text 'N' Width: %d", self.textNWidth)
return
@@ -355,7 +358,7 @@ class GuiTheme:
confParser = NWConfigParser()
for themeKey, themePath in self._availThemes.items():
logger.verbose("Checking theme config for '%s'", themeKey)
logger.debug("Checking theme config for '%s'", themeKey)
themeName = _loadInternalName(confParser, themePath)
if themeName:
self._themeList.append((themeKey, themeName))
@@ -372,7 +375,7 @@ class GuiTheme:
confParser = NWConfigParser()
for syntaxKey, syntaxPath in self._availSyntax.items():
logger.verbose("Checking theme syntax for '%s'", syntaxKey)
logger.debug("Checking theme syntax for '%s'", syntaxKey)
syntaxName = _loadInternalName(confParser, syntaxPath)
if syntaxName:
self._syntaxList.append((syntaxKey, syntaxName))
@@ -457,17 +460,17 @@ class GuiIcons:
ICON_KEYS = {
# Project and GUI icons
"novelwriter", "cls_archive", "cls_character", "cls_custom", "cls_entity", "cls_none",
"cls_novel", "cls_object", "cls_plot", "cls_timeline", "cls_trash", "cls_world", "doc_h0",
"doc_h1", "doc_h2", "doc_h3", "doc_h4", "proj_chapter", "proj_details", "proj_document",
"proj_folder", "proj_note", "proj_nwx", "proj_scene", "proj_stats", "proj_title",
"search_cancel", "search_case", "search_loop", "search_preserve", "search_project",
"search_regex", "search_word", "status_idle", "status_lang", "status_lines",
"status_stats", "status_time", "view_build", "view_editor", "view_novel", "view_outline",
"cls_novel", "cls_object", "cls_plot", "cls_timeline", "cls_trash", "cls_world",
"proj_chapter", "proj_details", "proj_document", "proj_folder", "proj_note", "proj_nwx",
"proj_section", "proj_scene", "proj_stats", "proj_title", "search_cancel", "search_case",
"search_loop", "search_preserve", "search_project", "search_regex", "search_word",
"status_idle", "status_lang", "status_lines", "status_stats", "status_time", "view_build",
"view_editor", "view_novel", "view_outline",
# General Button Icons
"add", "backward", "check", "clear", "close", "cross", "delete", "done", "down", "edit",
"forward", "hash", "maximise", "menu", "minimise", "reference", "refresh", "remove",
"save", "search_replace", "search", "settings", "up",
"add", "backward", "bookmark", "check", "close", "cross", "down", "edit", "forward",
"maximise", "menu", "minimise", "reference", "refresh", "remove", "search_replace",
"search", "settings", "up",
# Switches
"sticky-on", "sticky-off",
@@ -490,6 +493,7 @@ class GuiIcons:
self._qIcons = {}
self._themeMap = {}
self._themeList = []
self._headerDec = []
self._confName = "icons.conf"
# Icon Theme Path
@@ -557,7 +561,7 @@ class GuiIcons:
iconPath = os.path.join(self._themePath, iconFile)
if os.path.isfile(iconPath):
self._themeMap[iconName] = iconPath
logger.verbose("Icon slot '%s' using file '%s'", iconName, iconFile)
logger.debug("Icon slot '%s' using file '%s'", iconName, iconFile)
else:
logger.error("Icon file '%s' not in theme folder", iconFile)
@@ -641,6 +645,8 @@ class GuiIcons:
iconName = "proj_chapter"
elif hLevel == "H3":
iconName = "proj_scene"
elif hLevel == "H4":
iconName = "proj_section"
elif tLayout == nwItemLayout.NOTE:
iconName = "proj_note"
if iconName is None:
@@ -648,6 +654,20 @@ class GuiIcons:
return self.getIcon(iconName)
def getHeaderDecoration(self, hLevel):
"""Get the decoration for a specific header level.
"""
if not self._headerDec:
iPx = self.mainTheme.baseIconSize
self._headerDec = [
self.loadDecoration("deco_doc_h0", pxH=iPx),
self.loadDecoration("deco_doc_h1", pxH=iPx),
self.loadDecoration("deco_doc_h2", pxH=iPx),
self.loadDecoration("deco_doc_h3", pxH=iPx),
self.loadDecoration("deco_doc_h4", pxH=iPx),
]
return self._headerDec[minmax(hLevel, 0, 4)]
def listThemes(self):
"""Scan the icons themes folder and list all themes.
"""
@@ -660,7 +680,7 @@ class GuiIcons:
if not os.path.isdir(themePath):
continue
logger.verbose("Checking icon theme config for '%s'", themeDir)
logger.debug("Checking icon theme config for '%s'", themeDir)
themeConf = os.path.join(themePath, self._confName)
themeName = _loadInternalName(confParser, themeConf)
if themeName:
@@ -708,7 +728,7 @@ class GuiIcons:
# Otherwise, we load from the theme folder
if iconKey in self._themeMap:
relPath = os.path.relpath(self._themeMap[iconKey], self._iconPath)
logger.verbose("Loading: %s", relPath)
logger.debug("Loading: %s", relPath)
return QIcon(self._themeMap[iconKey])
# If we didn't find one, give up and return an empty icon
+17 -20
View File
@@ -41,7 +41,7 @@ class GuiViewsBar(QToolBar):
viewChangeRequested = pyqtSignal(nwView)
def __init__(self, mainGui):
QToolBar.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiViewsBar ...")
@@ -67,52 +67,49 @@ class GuiViewsBar(QToolBar):
stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Actions
self.aProject = QAction(self.tr("Project"))
self.aProject = QAction(self.tr("Project"), self)
self.aProject.setFont(lblFont)
self.aProject.setToolTip(self.tr("Show project tree and editor"))
self.aProject.setToolTip(self.tr("Project Tree View"))
self.aProject.setIcon(self.mainTheme.getIcon("view_editor"))
self.aProject.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT))
self.aNovel = QAction(self.tr("Novel"))
self.aNovel = QAction(self.tr("Novel"), self)
self.aNovel.setFont(lblFont)
self.aNovel.setToolTip(self.tr("Show novel tree and editor"))
self.aNovel.setToolTip(self.tr("Novel Tree View"))
self.aNovel.setIcon(self.mainTheme.getIcon("view_novel"))
self.aNovel.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL))
self.aOutline = QAction(self.tr("Outline"))
self.aOutline = QAction(self.tr("Outline"), self)
self.aOutline.setFont(lblFont)
self.aOutline.setToolTip(self.tr("Show novel outline"))
self.aOutline.setToolTip(self.tr("Novel Outline View"))
self.aOutline.setIcon(self.mainTheme.getIcon("view_outline"))
self.aOutline.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE))
self.aBuild = QAction(self.tr("Build"))
self.aBuild = QAction(self.tr("Build"), self)
self.aBuild.setFont(lblFont)
self.aBuild.setToolTip(self.tr("Build novel project"))
self.aBuild.setToolTip(self.tr("Build Novel Project"))
self.aBuild.setIcon(self.mainTheme.getIcon("view_build"))
self.aBuild.triggered.connect(lambda: self.mainGui.showBuildProjectDialog())
self.aDetails = QAction(self.tr("Details"))
self.aDetails = QAction(self.tr("Details"), self)
self.aDetails.setFont(lblFont)
self.aDetails.setToolTip(self.tr("Show project details"))
self.aDetails.setToolTip(self.tr("Project Details"))
self.aDetails.setIcon(self.mainTheme.getIcon("proj_details"))
self.aDetails.triggered.connect(lambda: self.mainGui.showProjectDetailsDialog())
self.aStats = QAction(self.tr("Stats"))
self.aStats = QAction(self.tr("Stats"), self)
self.aStats.setFont(lblFont)
self.aStats.setToolTip(self.tr("Show project statistics"))
self.aStats.setToolTip(self.tr("Writing Statistics"))
self.aStats.setIcon(self.mainTheme.getIcon("proj_stats"))
self.aStats.triggered.connect(lambda: self.mainGui.showWritingStatsDialog())
# Settings Menu
self.mSettings = QMenu()
self.aPrjSettings = QAction(self.tr("Project Settings"))
self.aPrjSettings.triggered.connect(lambda: self.mainGui.showProjectSettingsDialog())
self.mSettings.addAction(self.aPrjSettings)
self.aPreferences = QAction(self.tr("Preferences"))
self.aPreferences.triggered.connect(lambda: self.mainGui.showPreferencesDialog())
self.mSettings.addAction(self.aPreferences)
self.mSettings.addAction(self.mainGui.mainMenu.aEditWordList)
self.mSettings.addAction(self.mainGui.mainMenu.aProjectSettings)
self.mSettings.addSeparator()
self.mSettings.addAction(self.mainGui.mainMenu.aPreferences)
self.tbSettings = QToolButton(self)
self.tbSettings.setFont(lblFont)
+47 -64
View File
@@ -44,8 +44,8 @@ from novelwriter.gui import (
GuiViewsBar
)
from novelwriter.dialogs import (
GuiAbout, GuiDocMerge, GuiDocSplit, GuiPreferences, GuiProjectDetails,
GuiProjectLoad, GuiProjectSettings, GuiUpdates, GuiWordList
GuiAbout, GuiPreferences, GuiProjectDetails, GuiProjectLoad,
GuiProjectSettings, GuiUpdates, GuiWordList
)
from novelwriter.tools import (
GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats
@@ -55,6 +55,7 @@ from novelwriter.enum import (
nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView
)
from novelwriter.common import getGuiItem, hexToInt
from novelwriter.constants import nwFiles
logger = logging.getLogger(__name__)
@@ -62,7 +63,7 @@ logger = logging.getLogger(__name__)
class GuiMain(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
super().__init__()
logger.debug("Initialising GUI ...")
self.setObjectName("GuiMain")
@@ -104,7 +105,7 @@ class GuiMain(QMainWindow):
hWd = self.mainConf.pxInt(4)
# Main GUI Elements
self.statusBar = GuiMainStatus(self)
self.mainStatus = GuiMainStatus(self)
self.projView = GuiProjectView(self)
self.novelView = GuiNovelView(self)
self.docEditor = GuiDocEditor(self)
@@ -189,8 +190,9 @@ class GuiMain(QMainWindow):
# Set Main Window Elements
self.setMenuBar(self.mainMenu)
self.setCentralWidget(self.mainStack)
self.setStatusBar(self.statusBar)
self.setStatusBar(self.mainStatus)
self.addToolBar(Qt.LeftToolBarArea, self.viewsBar)
self.setContextMenuPolicy(Qt.NoContextMenu) # Issue #1147
# Connect Signals
# ===============
@@ -205,12 +207,13 @@ class GuiMain(QMainWindow):
self.projView.treeItemChanged.connect(self.itemDetails.updateViewBox)
self.projView.rootFolderChanged.connect(self.outlineView.updateRootItem)
self.projView.rootFolderChanged.connect(self.novelView.updateRootItem)
self.projView.rootFolderChanged.connect(self.projView.updateRootItem)
self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
self.novelView.openDocumentRequest.connect(self._openDocument)
self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage)
self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus)
self.docEditor.spellDictionaryChanged.connect(self.mainStatus.setLanguage)
self.docEditor.docEditedStatusChanged.connect(self.mainStatus.doUpdateDocumentStatus)
self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts)
self.docEditor.docCountsChanged.connect(self.projView.updateCounts)
self.docEditor.loadDocumentTagRequest.connect(self._followTag)
@@ -252,7 +255,7 @@ class GuiMain(QMainWindow):
keyEscape.activated.connect(self._keyPressEscape)
# Forward Functions
self.setStatus = self.statusBar.setStatus
self.setStatus = self.mainStatus.setStatus
# Force a show of the GUI
self.show()
@@ -264,7 +267,7 @@ class GuiMain(QMainWindow):
self.initMain()
self.asProjTimer.start()
self.asDocTimer.start()
self.statusBar.clearStatus()
self.mainStatus.clearStatus()
# Handle Windows Mode
self.showNormal()
@@ -305,7 +308,7 @@ class GuiMain(QMainWindow):
self.outlineView.clearProject()
# General
self.statusBar.clearStatus()
self.mainStatus.clearStatus()
self._updateWindowTitle()
return True
@@ -351,7 +354,7 @@ class GuiMain(QMainWindow):
logger.error("No projData or projPath set")
return False
if os.path.isfile(os.path.join(projPath, self.theProject.projFile)):
if os.path.isfile(os.path.join(projPath, nwFiles.PROJ_FILE)):
self.makeAlert(self.tr(
"A project already exists in that location. "
"Please choose another folder."
@@ -360,18 +363,27 @@ class GuiMain(QMainWindow):
logger.info("Creating new project")
if self.theProject.newProject(projData):
self.hasProject = True
self.idleRefTime = time()
self.idleTime = 0.0
self.rebuildTrees()
self.saveProject()
self.docEditor.setDictionaries()
self.projView.openProjectTasks()
self.novelView.openProjectTasks()
self.outlineView.openProjectTasks()
self.rebuildIndex(beQuiet=True)
self.statusBar.setRefTime(self.theProject.projOpened)
self.statusBar.setProjectStatus(nwState.GOOD)
self.statusBar.setDocumentStatus(nwState.NONE)
self.statusBar.setStatus(self.tr("New project created ..."))
self.mainStatus.setRefTime(self.theProject.projOpened)
self.mainStatus.setProjectStatus(nwState.GOOD)
self.mainStatus.setDocumentStatus(nwState.NONE)
self.mainStatus.setStatus(self.tr("New project created ..."))
self._updateWindowTitle(self.theProject.projName)
else:
self.theProject.clearProject()
return False
@@ -512,7 +524,8 @@ class GuiMain(QMainWindow):
self.rebuildTrees()
self.docEditor.setDictionaries()
self.docEditor.toggleSpellCheck(self.theProject.spellCheck)
self.statusBar.setRefTime(self.theProject.projOpened)
self.mainStatus.setRefTime(self.theProject.projOpened)
self.projView.openProjectTasks()
self.novelView.openProjectTasks()
self.outlineView.openProjectTasks()
self._updateStatusWordCount()
@@ -547,7 +560,7 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return False
self.projView.saveProjectTree()
self.projView.saveProjectTasks()
if self.theProject.saveProject(autoSave=autoSave):
self.theProject.index.saveIndex()
@@ -613,7 +626,7 @@ class GuiMain(QMainWindow):
fHandle = None # The first file handle we encounter
foundIt = False # We've found tHandle, pick the next we see
for tItem in self.theProject.tree:
if not self.theProject.tree.checkType(tItem.itemHandle, nwItemType.FILE):
if tItem is None or not tItem.isFileType():
continue
if fHandle is None:
fHandle = tItem.itemHandle
@@ -654,21 +667,18 @@ class GuiMain(QMainWindow):
logger.debug("Viewing document, but no handle provided")
if self.docEditor.hasFocus():
logger.verbose("Trying editor document")
tHandle = self.docEditor.docHandle()
if tHandle is not None:
self.saveDocument()
else:
logger.verbose("Trying selected document")
tHandle = self.projView.getSelectedHandle()
if tHandle is None:
logger.verbose("Trying last viewed document")
tHandle = self.theProject.lastViewed
if tHandle is None:
logger.verbose("No document to view, giving up")
logger.debug("No document to view, giving up")
return False
# Make sure main tab is in Editor view
@@ -745,30 +755,6 @@ class GuiMain(QMainWindow):
return True
def mergeDocuments(self):
"""Merge multiple documents to one single new document.
"""
if not self.hasProject:
logger.error("No project open")
return False
dlgMerge = GuiDocMerge(self)
dlgMerge.exec_()
return True
def splitDocument(self):
"""Split a single document into multiple documents.
"""
if not self.hasProject:
logger.error("No project open")
return False
dlgSplit = GuiDocSplit(self)
dlgSplit.exec_()
return True
def passDocumentAction(self, theAction):
"""Pass on document action to the document viewer if it has
focus, or pass it to the document editor if it or any of
@@ -856,14 +842,14 @@ class GuiMain(QMainWindow):
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
tStart = time()
self.projView.saveProjectTree()
self.projView.saveProjectTasks()
self.theProject.index.clearIndex()
for tItem in self.theProject.tree:
if tItem is None: # pragma: no cover
continue # This is a bug trap
logger.verbose("Indexing '%s'", tItem.itemName)
logger.debug("Indexing '%s'", tItem.itemName)
if self.theProject.index.reIndexHandle(tItem.itemHandle):
# Update Word Counts
self.projView.propagateCount(tItem.itemHandle, tItem.wordCount, countChildren=True)
@@ -964,6 +950,7 @@ class GuiMain(QMainWindow):
dlgDetails = getGuiItem("GuiProjectDetails")
if dlgDetails is None:
dlgDetails = GuiProjectDetails(self)
assert isinstance(dlgDetails, GuiProjectDetails)
dlgDetails.setModal(False)
dlgDetails.show()
@@ -982,6 +969,7 @@ class GuiMain(QMainWindow):
dlgBuild = getGuiItem("GuiBuildNovel")
if dlgBuild is None:
dlgBuild = GuiBuildNovel(self)
assert isinstance(dlgBuild, GuiBuildNovel)
dlgBuild.setModal(False)
dlgBuild.show()
@@ -1001,6 +989,7 @@ class GuiMain(QMainWindow):
dlgLipsum = getGuiItem("GuiLipsum")
if dlgLipsum is None:
dlgLipsum = GuiLipsum(self)
assert isinstance(dlgLipsum, GuiLipsum)
dlgLipsum.setModal(False)
dlgLipsum.show()
@@ -1035,6 +1024,7 @@ class GuiMain(QMainWindow):
dlgStats = getGuiItem("GuiWritingStats")
if dlgStats is None:
dlgStats = GuiWritingStats(self)
assert isinstance(dlgStats, GuiWritingStats)
dlgStats.setModal(False)
dlgStats.show()
@@ -1050,6 +1040,7 @@ class GuiMain(QMainWindow):
dlgAbout = getGuiItem("GuiAbout")
if dlgAbout is None:
dlgAbout = GuiAbout(self)
assert isinstance(dlgAbout, GuiAbout)
dlgAbout.setModal(True)
dlgAbout.show()
@@ -1075,6 +1066,7 @@ class GuiMain(QMainWindow):
dlgUpdate = getGuiItem("GuiUpdates")
if dlgUpdate is None:
dlgUpdate = GuiUpdates(self)
assert isinstance(dlgUpdate, GuiUpdates)
dlgUpdate.setModal(True)
dlgUpdate.show()
@@ -1225,11 +1217,9 @@ class GuiMain(QMainWindow):
"""
if self.docEditor.docHandle() is None:
logger.error("No document open, so not activating Focus Mode")
self.mainMenu.setFocusMode(self.isFocusMode)
return False
self.isFocusMode = not self.isFocusMode
self.mainMenu.setFocusMode(self.isFocusMode)
if self.isFocusMode:
logger.debug("Activating Focus Mode")
self.switchFocus(nwWidget.EDITOR)
@@ -1238,7 +1228,7 @@ class GuiMain(QMainWindow):
isVisible = not self.isFocusMode
self.treePane.setVisible(isVisible)
self.statusBar.setVisible(isVisible)
self.mainStatus.setVisible(isVisible)
self.mainMenu.setVisible(isVisible)
self.viewsBar.setVisible(isVisible)
@@ -1522,12 +1512,12 @@ class GuiMain(QMainWindow):
if editIdle or userIdle:
self.idleTime += currTime - self.idleRefTime
self.statusBar.setUserIdle(True)
self.mainStatus.setUserIdle(True)
else:
self.statusBar.setUserIdle(False)
self.mainStatus.setUserIdle(False)
self.idleRefTime = currTime
self.statusBar.updateTime(idleTime=self.idleTime)
self.mainStatus.updateTime(idleTime=self.idleTime)
return
@@ -1536,9 +1526,8 @@ class GuiMain(QMainWindow):
"""Update the word count on the status bar.
"""
if not self.hasProject:
self.statusBar.setProjectStats(0, 0)
self.mainStatus.setProjectStats(0, 0)
logger.verbose("Updating total word count")
self.theProject.updateWordCounts()
if self.mainConf.incNotesWCount:
currWords = self.theProject.currWCount
@@ -1547,7 +1536,7 @@ class GuiMain(QMainWindow):
currWords = self.theProject.currNovelWC
diffWords = currWords - self.theProject.lastNovelWC
self.statusBar.setProjectStats(currWords, diffWords)
self.mainStatus.setProjectStats(currWords, diffWords)
return
@@ -1575,13 +1564,9 @@ class GuiMain(QMainWindow):
def _mainStackChanged(self, stIndex):
"""Activated when the main window tab is changed.
"""
if stIndex == self.idxEditorView:
logger.verbose("Editor View activated")
elif stIndex == self.idxOutlineView:
logger.verbose("Outline View activated")
if stIndex == self.idxOutlineView:
if self.hasProject:
self.outlineView.refreshTree()
return
@pyqtSlot(int)
@@ -1591,11 +1576,9 @@ class GuiMain(QMainWindow):
sHandle = None
if stIndex == self.idxProjView:
logger.verbose("Project Tree View activated")
sHandle = self.projView.getSelectedHandle()
elif stIndex == self.idxNovelView:
logger.verbose("Novel Tree View activated")
if self.hasProject:
self.novelView.refreshTree()
sHandle, _ = self.novelView.getSelectedHandle()
+52 -13
View File
@@ -47,8 +47,8 @@ from novelwriter.core import ToHtml, ToOdt, ToMarkdown
from novelwriter.enum import nwAlert, nwItemType, nwItemLayout, nwItemClass
from novelwriter.error import formatException, logException
from novelwriter.common import fuzzyTime, makeFileNameSafe
from novelwriter.custom import QSwitch
from novelwriter.constants import nwConst, nwFiles
from novelwriter.gui.custom import QSwitch
logger = logging.getLogger(__name__)
@@ -66,7 +66,7 @@ class GuiBuildNovel(QDialog):
FMT_JSON_M = 9 # nW Markdown wrapped in JSON
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiBuildNovel ...")
self.setObjectName("GuiBuildNovel")
@@ -351,6 +351,36 @@ class GuiBuildNovel(QDialog):
self.textForm.setColumnStretch(0, 1)
self.textForm.setColumnStretch(1, 0)
# Root Filter Options
# ===================
self.rootGroup = QGroupBox(self.tr("Root Filter Options"), self)
self.rootForm = QGridLayout(self)
self.rootGroup.setLayout(self.rootForm)
rootFilter = pOptions.getValue("GuiBuildNovel", "rootFilter", [])
if not isinstance(rootFilter, list):
rootFilter = []
iRow = 0
self.rootSelection = {}
for tHandle, nwItem in self.theProject.tree.iterRoots(None):
if not nwItem.isInactive():
rootLabel = QLabel(nwItem.itemName)
rootLabel.setWordWrap(True)
rootValue = QSwitch(width=wS, height=hS)
rootValue.setChecked(tHandle not in rootFilter)
self.rootSelection[tHandle] = rootValue
self.rootForm.addWidget(rootLabel, iRow, 0, 1, 1, Qt.AlignLeft)
self.rootForm.addWidget(rootValue, iRow, 1, 1, 1, Qt.AlignRight)
iRow += 1
self.rootForm.setColumnStretch(0, 1)
self.rootForm.setColumnStretch(1, 0)
# File Filter Options
# ===================
@@ -375,13 +405,13 @@ class GuiBuildNovel(QDialog):
novelLabel = QLabel(self.tr("Include novel files"))
notesLabel = QLabel(self.tr("Include note files"))
exportLabel = QLabel(self.tr("Ignore export flag"))
activeLabel = QLabel(self.tr("Include inactive files"))
self.fileForm.addWidget(novelLabel, 0, 0, 1, 1, Qt.AlignLeft)
self.fileForm.addWidget(self.novelFiles, 0, 1, 1, 1, Qt.AlignRight)
self.fileForm.addWidget(notesLabel, 1, 0, 1, 1, Qt.AlignLeft)
self.fileForm.addWidget(self.noteFiles, 1, 1, 1, 1, Qt.AlignRight)
self.fileForm.addWidget(exportLabel, 2, 0, 1, 1, Qt.AlignLeft)
self.fileForm.addWidget(activeLabel, 2, 0, 1, 1, Qt.AlignLeft)
self.fileForm.addWidget(self.ignoreFlag, 2, 1, 1, 1, Qt.AlignRight)
self.fileForm.setColumnStretch(0, 1)
@@ -503,6 +533,7 @@ class GuiBuildNovel(QDialog):
self.toolsBox.addWidget(self.fontGroup)
self.toolsBox.addWidget(self.styleGroup)
self.toolsBox.addWidget(self.textGroup)
self.toolsBox.addWidget(self.rootGroup)
self.toolsBox.addWidget(self.fileGroup)
self.toolsBox.addWidget(self.exportGroup)
self.toolsBox.addStretch(1)
@@ -716,6 +747,7 @@ class GuiBuildNovel(QDialog):
self.buildProgress.setMaximum(len(self.theProject.tree))
self.buildProgress.setValue(0)
rootFilter = set(self._generateRootFilter())
for nItt, tItem in enumerate(self.theProject.tree):
noteRoot = noteFiles
@@ -730,7 +762,7 @@ class GuiBuildNovel(QDialog):
if doConvert:
bldObj.doConvert()
elif self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag):
elif self._checkInclude(tItem, noteFiles, novelFiles, ignoreFlag, rootFilter):
bldObj.setText(tItem.itemHandle)
bldObj.doPreProcessing()
bldObj.tokenizeText()
@@ -766,7 +798,7 @@ class GuiBuildNovel(QDialog):
return
def _checkInclude(self, theItem, noteFiles, novelFiles, ignoreFlag):
def _checkInclude(self, theItem, noteFiles, novelFiles, ignoreFlag, rootFilter):
"""This function checks whether a file should be included in the
export or not. For standard note and novel files, this is
controlled by the options selected by the user. For other files
@@ -781,7 +813,10 @@ class GuiBuildNovel(QDialog):
if theItem is None:
return False
if not (theItem.isExported or ignoreFlag):
if not (theItem.isActive or ignoreFlag):
return False
if theItem.itemRoot in rootFilter:
return False
isNone = not theItem.isFileType()
@@ -1045,6 +1080,11 @@ class GuiBuildNovel(QDialog):
return
def _generateRootFilter(self):
"""Return a list of all root folders that are filtered out.
"""
return [h for h, s in self.rootSelection.items() if not s.isChecked()]
def _loadCache(self):
"""Save the current data to cache.
"""
@@ -1146,6 +1186,7 @@ class GuiBuildNovel(QDialog):
incBodyText = self.includeBody.isChecked()
replaceTabs = self.replaceTabs.isChecked()
replaceUCode = self.replaceUCode.isChecked()
rootFilter = self._generateRootFilter()
mainSplit = self.mainSplit.sizes()
boxWidth = self.mainConf.rpxInt(mainSplit[0])
@@ -1175,6 +1216,7 @@ class GuiBuildNovel(QDialog):
pOptions.setValue("GuiBuildNovel", "incBodyText", incBodyText)
pOptions.setValue("GuiBuildNovel", "replaceTabs", replaceTabs)
pOptions.setValue("GuiBuildNovel", "replaceUCode", replaceUCode)
pOptions.setValue("GuiBuildNovel", "rootFilter", rootFilter)
pOptions.saveSettings()
return
@@ -1194,7 +1236,7 @@ class GuiBuildNovel(QDialog):
class GuiBuildNovelDocView(QTextBrowser):
def __init__(self, mainGui, theProject):
QTextBrowser.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiBuildNovelDocView ...")
@@ -1223,10 +1265,7 @@ class GuiBuildNovelDocView(QTextBrowser):
self.setFont(theFont)
# Set the tab stops
if self.mainConf.verQtValue >= 51000:
self.setTabStopDistance(self.mainConf.getTabWidth())
else:
self.setTabStopWidth(self.mainConf.getTabWidth())
self.setTabStopDistance(self.mainConf.getTabWidth())
docPalette = self.palette()
docPalette.setColor(QPalette.Base, QColor(255, 255, 255))
@@ -1343,7 +1382,7 @@ class GuiBuildNovelDocView(QTextBrowser):
def resizeEvent(self, theEvent):
"""Make sure the document title is the same width as the window.
"""
QTextBrowser.resizeEvent(self, theEvent)
super().resizeEvent(theEvent)
self._updateDocMargins()
return
+2 -2
View File
@@ -34,8 +34,8 @@ from PyQt5.QtWidgets import (
QSpinBox
)
from novelwriter.gui.custom import QSwitch
from novelwriter.common import readTextFile
from novelwriter.custom import QSwitch
logger = logging.getLogger(__name__)
@@ -43,7 +43,7 @@ logger = logging.getLogger(__name__)
class GuiLipsum(QDialog):
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiLipsum ...")
self.setObjectName("GuiLipsum")
+33 -14
View File
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter.common import makeFileNameSafe
from novelwriter.gui.custom import QSwitch
from novelwriter.custom import QSwitch
logger = logging.getLogger(__name__)
@@ -49,7 +49,7 @@ PAGE_FINAL = 4
class GuiProjectWizard(QWizard):
def __init__(self, mainGui):
QWizard.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiProjectWizard ...")
self.setObjectName("GuiProjectWizard")
@@ -88,7 +88,7 @@ class GuiProjectWizard(QWizard):
class ProjWizardIntroPage(QWizardPage):
def __init__(self, theWizard):
QWizardPage.__init__(self)
super().__init__()
self.mainConf = novelwriter.CONFIG
self.theWizard = theWizard
@@ -158,7 +158,7 @@ class ProjWizardIntroPage(QWizardPage):
class ProjWizardFolderPage(QWizardPage):
def __init__(self, theWizard):
QWizardPage.__init__(self)
super().__init__()
self.mainConf = novelwriter.CONFIG
self.theWizard = theWizard
@@ -209,12 +209,12 @@ class ProjWizardFolderPage(QWizardPage):
"""Check that the selected path isn't already being used.
"""
self.errLabel.setText("")
if not QWizardPage.isComplete(self):
if not super().isComplete():
return False
setPath = os.path.abspath(os.path.expanduser(self.projPath.text()))
parPath = os.path.dirname(setPath)
logger.verbose("Path is: %s", setPath)
logger.debug("Path is: %s", setPath)
if parPath and not os.path.isdir(parPath):
self.errLabel.setText(self.tr(
"Error: A project folder cannot be created using this path."
@@ -259,7 +259,7 @@ class ProjWizardFolderPage(QWizardPage):
class ProjWizardPopulatePage(QWizardPage):
def __init__(self, theWizard):
QWizardPage.__init__(self)
super().__init__()
self.mainConf = novelwriter.CONFIG
self.theWizard = theWizard
@@ -315,7 +315,7 @@ class ProjWizardPopulatePage(QWizardPage):
class ProjWizardCustomPage(QWizardPage):
def __init__(self, theWizard):
QWizardPage.__init__(self)
super().__init__()
self.mainConf = novelwriter.CONFIG
self.theWizard = theWizard
@@ -334,13 +334,18 @@ class ProjWizardCustomPage(QWizardPage):
# Root Folders
self.addPlot = QSwitch()
self.addChar = QSwitch()
self.addWorld = QSwitch()
self.addNotes = QSwitch()
self.addPlot.setChecked(True)
self.addPlot.clicked.connect(self._syncSwitches)
self.addChar = QSwitch()
self.addChar.setChecked(True)
self.addChar.clicked.connect(self._syncSwitches)
self.addWorld = QSwitch()
self.addWorld.setChecked(False)
self.addWorld.clicked.connect(self._syncSwitches)
self.addNotes = QSwitch()
self.addNotes.setChecked(False)
# Generate Content
@@ -391,13 +396,27 @@ class ProjWizardCustomPage(QWizardPage):
return
##
# Internal Functions
##
def _syncSwitches(self):
"""Check if the add notes option should also be switched off.
"""
addPlot = self.addPlot.isChecked()
addChar = self.addChar.isChecked()
addWorld = self.addWorld.isChecked()
if not (addPlot or addChar or addWorld):
self.addNotes.setChecked(False)
return
# END Class ProjWizardCustomPage
class ProjWizardFinalPage(QWizardPage):
def __init__(self, theWizard):
QWizardPage.__init__(self)
super().__init__()
self.mainConf = novelwriter.CONFIG
self.theWizard = theWizard
@@ -418,7 +437,7 @@ class ProjWizardFinalPage(QWizardPage):
def initializePage(self):
"""Update the summary information on the final page.
"""
QWizardPage.initializePage(self)
super().initializePage()
sumList = []
sumList.append(self.tr("Project Name: {0}").format(self.field("projName")))
+9 -8
View File
@@ -39,9 +39,9 @@ from PyQt5.QtWidgets import (
from novelwriter.enum import nwAlert
from novelwriter.error import formatException
from novelwriter.common import formatTime, checkInt, checkIntRange, checkIntTuple
from novelwriter.common import formatTime, checkInt, checkIntTuple, minmax
from novelwriter.custom import QSwitch
from novelwriter.constants import nwConst, nwFiles
from novelwriter.gui.custom import QSwitch
logger = logging.getLogger(__name__)
@@ -58,7 +58,7 @@ class GuiWritingStats(QDialog):
FMT_CSV = 1
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
super().__init__(parent=mainGui)
logger.debug("Initialising GuiWritingStats ...")
self.setObjectName("GuiWritingStats")
@@ -112,11 +112,12 @@ class GuiWritingStats(QDialog):
self.listBox.setColumnWidth(self.C_COUNT, wCol3)
hHeader = self.listBox.headerItem()
hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
hHeader.setTextAlignment(self.C_IDLE, Qt.AlignRight)
hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight)
if hHeader is not None:
hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
hHeader.setTextAlignment(self.C_IDLE, Qt.AlignRight)
hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight)
sortCol = checkIntRange(pOptions.getInt("GuiWritingStats", "sortCol", 0), 0, 2, 0)
sortCol = minmax(pOptions.getInt("GuiWritingStats", "sortCol", 0), 0, 2)
sortOrder = checkIntTuple(
pOptions.getInt("GuiWritingStats", "sortOrder", Qt.DescendingOrder),
(Qt.AscendingOrder, Qt.DescendingOrder), Qt.DescendingOrder
@@ -449,7 +450,7 @@ class GuiWritingStats(QDialog):
if inLine.startswith("#"):
if inLine.startswith("# Offset"):
self.wordOffset = checkInt(inLine[9:].strip(), 0)
logger.verbose(
logger.debug(
"Initial word count when log was started is %d" % self.wordOffset
)
continue
+41 -41
View File
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:38:08">
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.4" timeStamp="2022-10-20 18:17:33">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
<author>Jay Doh</author>
<saveCount>1378</saveCount>
<saveCount>1385</saveCount>
<autoCount>236</autoCount>
<editTime>69273</editTime>
<editTime>69348</editTime>
</project>
<settings>
<doBackup>False</doBackup>
@@ -55,56 +55,56 @@
<name status="sc24b8f" import="ia857f0">Novel</name>
</item>
<item handle="53b69b83cdafc" parent="7031beac91f75" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="93" wordCount="19" paraCount="2" cursorPos="119"/>
<name status="sc24b8f" import="ia857f0" exported="True">Title Page</name>
<meta expanded="False" mainHeading="H1" charCount="93" wordCount="19" paraCount="2" cursorPos="119"/>
<name status="sc24b8f" import="ia857f0" active="True">Title Page</name>
</item>
<item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="251" wordCount="50" paraCount="2" cursorPos="277"/>
<name status="sf12341" import="ia857f0" exported="True">Page</name>
<meta expanded="False" mainHeading="H0" charCount="251" wordCount="50" paraCount="2" cursorPos="277"/>
<name status="sf12341" import="ia857f0" active="True">Page</name>
</item>
<item handle="edca4be2fcaf8" parent="7031beac91f75" root="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="26" wordCount="6" paraCount="1" cursorPos="36"/>
<name status="s90e6c9" import="ia857f0" exported="True">Part One</name>
<meta expanded="False" mainHeading="H1" charCount="26" wordCount="6" paraCount="1" cursorPos="36"/>
<name status="s90e6c9" import="ia857f0" active="True">Part One</name>
</item>
<item handle="6a2d6d5f4f401" parent="7031beac91f75" root="7031beac91f75" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="True" charCount="95" wordCount="18" paraCount="1" cursorPos="291"/>
<name status="sf24ce6" import="ia857f0" exported="True">Chapter One</name>
<meta expanded="True" mainHeading="H2" charCount="95" wordCount="18" paraCount="1" cursorPos="291"/>
<name status="sf24ce6" import="ia857f0" active="True">Chapter One</name>
</item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="2687" wordCount="479" paraCount="14" cursorPos="2183"/>
<name status="s90e6c9" import="ia857f0" exported="True">Making a Scene</name>
<meta expanded="False" mainHeading="H3" charCount="2687" wordCount="479" paraCount="14" cursorPos="67"/>
<name status="s90e6c9" import="ia857f0" active="True">Making a Scene</name>
</item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="548" wordCount="108" paraCount="3" cursorPos="465"/>
<name status="s90e6c9" import="ia857f0" exported="True">Another Scene</name>
<meta expanded="False" mainHeading="H3" charCount="548" wordCount="108" paraCount="3" cursorPos="465"/>
<name status="s90e6c9" import="ia857f0" active="True">Another Scene</name>
</item>
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="617" wordCount="101" paraCount="3" cursorPos="310"/>
<name status="s78ea90" import="ia857f0" exported="True">Interlude</name>
<meta expanded="False" mainHeading="H2" charCount="617" wordCount="101" paraCount="3" cursorPos="310"/>
<name status="s78ea90" import="ia857f0" active="True">Interlude</name>
</item>
<item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE">
<meta expanded="False" charCount="1909" wordCount="346" paraCount="7" cursorPos="0"/>
<name status="sf24ce6" import="ia857f0" exported="False">A Note on Structure</name>
<meta expanded="False" mainHeading="H1" charCount="1909" wordCount="346" paraCount="7" cursorPos="0"/>
<name status="sf24ce6" import="ia857f0" active="False">A Note on Structure</name>
</item>
<item handle="88706ddc78b1b" parent="7031beac91f75" root="7031beac91f75" order="6" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="True" charCount="139" wordCount="28" paraCount="1" cursorPos="188"/>
<name status="s90e6c9" import="ia857f0" exported="True">Chapter Two</name>
<meta expanded="True" mainHeading="H2" charCount="139" wordCount="28" paraCount="1" cursorPos="188"/>
<name status="s90e6c9" import="ia857f0" active="True">Chapter Two</name>
</item>
<item handle="ae7339df26ded" parent="88706ddc78b1b" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="189" wordCount="37" paraCount="1" cursorPos="0"/>
<name status="s90e6c9" import="ia857f0" exported="True">We Found John!</name>
<meta expanded="False" mainHeading="H3" charCount="189" wordCount="37" paraCount="1" cursorPos="0"/>
<name status="s90e6c9" import="ia857f0" active="True">We Found John!</name>
</item>
<item handle="e5e47ebf63b1c" parent="None" root="e5e47ebf63b1c" order="1" type="ROOT" class="NOVEL">
<meta expanded="True"/>
<name status="sf12341" import="ia857f0">Sequel</name>
</item>
<item handle="bacb7059e3083" parent="e5e47ebf63b1c" root="e5e47ebf63b1c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="27" wordCount="5" paraCount="1" cursorPos="100"/>
<name status="sc24b8f" import="ia857f0" exported="True">Title Page</name>
<meta expanded="False" mainHeading="H1" charCount="27" wordCount="5" paraCount="1" cursorPos="100"/>
<name status="sc24b8f" import="ia857f0" active="True">Title Page</name>
</item>
<item handle="a520879ca0b45" parent="e5e47ebf63b1c" root="e5e47ebf63b1c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="299" wordCount="55" paraCount="2" cursorPos="104"/>
<name status="s90e6c9" import="ia857f0" exported="True">Chapter One</name>
<meta expanded="False" mainHeading="H2" charCount="299" wordCount="55" paraCount="2" cursorPos="104"/>
<name status="s90e6c9" import="ia857f0" active="True">Chapter One</name>
</item>
<item handle="f6622b4617424" parent="None" root="f6622b4617424" order="2" type="ROOT" class="CHARACTER">
<meta expanded="True"/>
@@ -115,28 +115,28 @@
<name status="sf12341" import="ia857f0">Main Characters</name>
</item>
<item handle="14298de4d9524" parent="f7e2d9f330615" root="f6622b4617424" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="49" wordCount="9" paraCount="1" cursorPos="24"/>
<name status="sf12341" import="icfb3a5" exported="True">John Smith</name>
<meta expanded="False" mainHeading="H1" charCount="49" wordCount="9" paraCount="1" cursorPos="24"/>
<name status="sf12341" import="icfb3a5" active="True">John Smith</name>
</item>
<item handle="bb2c23b3c42cc" parent="f7e2d9f330615" root="f6622b4617424" order="1" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="55" wordCount="9" paraCount="1" cursorPos="25"/>
<name status="sf12341" import="i2d7a54" exported="True">Jane Smith</name>
<meta expanded="False" mainHeading="H1" charCount="55" wordCount="9" paraCount="1" cursorPos="25"/>
<name status="sf12341" import="i2d7a54" active="True">Jane Smith</name>
</item>
<item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="3" type="ROOT" class="WORLD">
<meta expanded="True"/>
<name status="sf12341" import="ia857f0">Locations</name>
</item>
<item handle="b3e74dbc1f584" parent="15c4492bd5107" root="15c4492bd5107" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="76" wordCount="15" paraCount="1" cursorPos="20"/>
<name status="sf12341" import="i56be10" exported="True">Earth</name>
<meta expanded="False" mainHeading="H1" charCount="76" wordCount="15" paraCount="1" cursorPos="20"/>
<name status="sf12341" import="i56be10" active="True">Earth</name>
</item>
<item handle="f1471bef9f2ae" parent="15c4492bd5107" root="15c4492bd5107" order="1" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="115" wordCount="24" paraCount="1" cursorPos="133"/>
<name status="sf12341" import="icfb3a5" exported="True">Space</name>
<meta expanded="False" mainHeading="H1" charCount="115" wordCount="24" paraCount="1" cursorPos="133"/>
<name status="sf12341" import="icfb3a5" active="True">Space</name>
</item>
<item handle="5eaea4e8cdee8" parent="15c4492bd5107" root="15c4492bd5107" order="2" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="28" wordCount="6" paraCount="1" cursorPos="45"/>
<name status="sf12341" import="i2d7a54" exported="True">Mars</name>
<meta expanded="False" mainHeading="H1" charCount="28" wordCount="6" paraCount="1" cursorPos="45"/>
<name status="sf12341" import="i2d7a54" active="True">Mars</name>
</item>
<item handle="6827118336ac1" parent="None" root="6827118336ac1" order="4" type="ROOT" class="ARCHIVE">
<meta expanded="True"/>
@@ -147,16 +147,16 @@
<name status="sf12341" import="ia857f0">Scenes</name>
</item>
<item handle="8a5deb88c0e97" parent="ae9bf3c3ea159" root="6827118336ac1" order="0" type="FILE" class="ARCHIVE" layout="DOCUMENT">
<meta expanded="False" charCount="232" wordCount="42" paraCount="1" cursorPos="239"/>
<name status="s90e6c9" import="ia857f0" exported="True">Old File</name>
<meta expanded="False" mainHeading="H3" charCount="232" wordCount="42" paraCount="1" cursorPos="239"/>
<name status="s90e6c9" import="ia857f0" active="True">Old File</name>
</item>
<item handle="98acd8c76c93a" parent="None" root="98acd8c76c93a" order="5" type="ROOT" class="TRASH">
<meta expanded="True"/>
<name status="sf12341" import="ia857f0">Trash</name>
</item>
<item handle="b8136a5a774a0" parent="98acd8c76c93a" root="98acd8c76c93a" order="0" type="FILE" class="TRASH" layout="DOCUMENT">
<meta expanded="False" charCount="30" wordCount="6" paraCount="1" cursorPos="36"/>
<name status="sf12341" import="ia857f0" exported="True">Delete Me!</name>
<meta expanded="False" mainHeading="H3" charCount="30" wordCount="6" paraCount="1" cursorPos="36"/>
<name status="sf12341" import="ia857f0" active="True">Delete Me!</name>
</item>
</content>
</novelWriterXML>
+12 -6
View File
@@ -190,14 +190,20 @@ def mockRnd(monkeypatch):
from 0. This one will generate status/importance flags and handles
in a predictable sequence.
"""
def rnd(n):
for x in range(n):
yield x
class MockRnd:
gen = rnd(1000)
monkeypatch.setattr("random.getrandbits", lambda *a: next(gen))
def __init__(self):
self.reset()
return
def _rnd(self, n):
for x in range(n):
yield x
def reset(self):
gen = self._rnd(1000)
monkeypatch.setattr("random.getrandbits", lambda *a: next(gen))
return MockRnd()
##
+33 -33
View File
@@ -1,12 +1,12 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-23 15:34:13">
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.4" timeStamp="2022-10-17 21:17:31">
<project>
<name>Lorem Ipsum</name>
<title>Lorem Ipsum</title>
<author>lipsum.com</author>
<saveCount>26</saveCount>
<saveCount>28</saveCount>
<autoCount>24</autoCount>
<editTime>1863</editTime>
<editTime>1874</editTime>
</project>
<settings>
<doBackup>False</doBackup>
@@ -50,84 +50,84 @@
<name status="sbaa94f" import="i613591">Novel</name>
</item>
<item handle="7a992350f3eb6" parent="b3643d0f92e32" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="230" wordCount="40" paraCount="3" cursorPos="148"/>
<name status="sedd043" import="i613591" exported="True">Lorem Ipsum</name>
<meta expanded="False" mainHeading="H1" charCount="230" wordCount="40" paraCount="3" cursorPos="148"/>
<name status="sedd043" import="i613591" active="True">Lorem Ipsum</name>
</item>
<item handle="8c58a65414c23" parent="b3643d0f92e32" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="1058" wordCount="176" paraCount="2" cursorPos="43"/>
<name status="sedd043" import="i613591" exported="True">Front Matter</name>
<meta expanded="False" mainHeading="H0" charCount="1058" wordCount="176" paraCount="2" cursorPos="43"/>
<name status="sedd043" import="i613591" active="True">Front Matter</name>
</item>
<item handle="88d59a277361b" parent="b3643d0f92e32" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="584" wordCount="92" paraCount="1" cursorPos="4"/>
<name status="s92a87b" import="i613591" exported="True">Prologue</name>
<meta expanded="False" mainHeading="H2" charCount="584" wordCount="92" paraCount="1" cursorPos="4"/>
<name status="s92a87b" import="i613591" active="True">Prologue</name>
</item>
<item handle="db7e733775d4d" parent="b3643d0f92e32" root="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="35" wordCount="6" paraCount="1" cursorPos="42"/>
<name status="sbaa94f" import="i613591" exported="True">Act One</name>
<meta expanded="False" mainHeading="H1" charCount="35" wordCount="6" paraCount="1" cursorPos="42"/>
<name status="sbaa94f" import="i613591" active="True">Act One</name>
</item>
<item handle="45e6b01ca35c1" parent="b3643d0f92e32" root="b3643d0f92e32" order="4" type="FOLDER" class="NOVEL">
<meta expanded="True"/>
<name status="s92a87b" import="i613591">Chapter One</name>
</item>
<item handle="fb609cd8319dc" parent="45e6b01ca35c1" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="419" wordCount="67" paraCount="1" cursorPos="56"/>
<name status="s92a87b" import="i613591" exported="True">Chapter One</name>
<meta expanded="False" mainHeading="H2" charCount="419" wordCount="67" paraCount="1" cursorPos="56"/>
<name status="s92a87b" import="i613591" active="True">Chapter One</name>
</item>
<item handle="88243afbe5ed8" parent="45e6b01ca35c1" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="2758" wordCount="404" paraCount="4" cursorPos="1528"/>
<name status="sedd043" import="i613591" exported="True">Scene One</name>
<meta expanded="False" mainHeading="H3" charCount="2758" wordCount="404" paraCount="4" cursorPos="1528"/>
<name status="sedd043" import="i613591" active="True">Scene One</name>
</item>
<item handle="f96ec11c6a3da" parent="45e6b01ca35c1" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="4043" wordCount="600" paraCount="6" cursorPos="2335"/>
<name status="sedd043" import="i613591" exported="True">Scene Two</name>
<meta expanded="False" mainHeading="H3" charCount="4043" wordCount="600" paraCount="6" cursorPos="2335"/>
<name status="sedd043" import="i613591" active="True">Scene Two</name>
</item>
<item handle="846352075de7d" parent="b3643d0f92e32" root="b3643d0f92e32" order="5" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="631" wordCount="109" paraCount="3" cursorPos="376"/>
<name status="sbaa94f" import="i613591" exported="False">Interlude</name>
<meta expanded="False" mainHeading="H2" charCount="631" wordCount="109" paraCount="3" cursorPos="376"/>
<name status="sbaa94f" import="i613591" active="False">Interlude</name>
</item>
<item handle="6bd935d2490cd" parent="b3643d0f92e32" root="b3643d0f92e32" order="6" type="FOLDER" class="NOVEL">
<meta expanded="True"/>
<name status="s92a87b" import="i613591">Chapter Two</name>
</item>
<item handle="441420a886d82" parent="6bd935d2490cd" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="477" wordCount="70" paraCount="1" cursorPos="56"/>
<name status="s92a87b" import="i613591" exported="True">Chapter Two</name>
<meta expanded="False" mainHeading="H2" charCount="477" wordCount="70" paraCount="1" cursorPos="56"/>
<name status="s92a87b" import="i613591" active="True">Chapter Two</name>
</item>
<item handle="eb103bc70c90c" parent="6bd935d2490cd" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="3006" wordCount="439" paraCount="4" cursorPos="57"/>
<name status="sedd043" import="i613591" exported="True">Scene Three</name>
<meta expanded="False" mainHeading="H3" charCount="3006" wordCount="439" paraCount="4" cursorPos="57"/>
<name status="sedd043" import="i613591" active="True">Scene Three</name>
</item>
<item handle="f8c0562e50f1b" parent="6bd935d2490cd" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="3839" wordCount="563" paraCount="6" cursorPos="56"/>
<name status="sedd043" import="i613591" exported="True">Scene Four</name>
<meta expanded="False" mainHeading="H3" charCount="3839" wordCount="563" paraCount="6" cursorPos="56"/>
<name status="sedd043" import="i613591" active="True">Scene Four</name>
</item>
<item handle="47666c91c7ccf" parent="6bd935d2490cd" root="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="3644" wordCount="543" paraCount="5" cursorPos="351"/>
<name status="sedd043" import="i613591" exported="True">Scene Five</name>
<meta expanded="False" mainHeading="H3" charCount="3644" wordCount="543" paraCount="5" cursorPos="351"/>
<name status="sedd043" import="i613591" active="True">Scene Five</name>
</item>
<item handle="67a8707f2f249" parent="None" root="67a8707f2f249" order="1" type="ROOT" class="CHARACTER">
<meta expanded="True"/>
<name status="sbaa94f" import="i613591">Characters</name>
</item>
<item handle="4c4f28287af27" parent="67a8707f2f249" root="67a8707f2f249" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="1864" wordCount="284" paraCount="3" cursorPos="1883"/>
<name status="sbaa94f" import="i613591" exported="True">Mr. Nobody</name>
<meta expanded="False" mainHeading="H1" charCount="1864" wordCount="284" paraCount="3" cursorPos="1883"/>
<name status="sbaa94f" import="i613591" active="True">Mr. Nobody</name>
</item>
<item handle="6c6afb1247750" parent="None" root="6c6afb1247750" order="2" type="ROOT" class="PLOT">
<meta expanded="True"/>
<name status="sbaa94f" import="i613591">Plot</name>
</item>
<item handle="2426c6f0ca922" parent="6c6afb1247750" root="6c6afb1247750" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta expanded="False" charCount="1369" wordCount="195" paraCount="2" cursorPos="1387"/>
<name status="sbaa94f" import="i613591" exported="True">Main</name>
<meta expanded="False" mainHeading="H1" charCount="1369" wordCount="195" paraCount="2" cursorPos="1387"/>
<name status="sbaa94f" import="i613591" active="True">Main</name>
</item>
<item handle="60bdf227455cc" parent="None" root="60bdf227455cc" order="3" type="ROOT" class="WORLD">
<meta expanded="True"/>
<name status="sbaa94f" import="i613591">World</name>
</item>
<item handle="04468803b92e1" parent="60bdf227455cc" root="60bdf227455cc" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="1770" wordCount="259" paraCount="3" cursorPos="1792"/>
<name status="sbaa94f" import="i613591" exported="True">Ancient Europe</name>
<meta expanded="False" mainHeading="H1" charCount="1770" wordCount="259" paraCount="3" cursorPos="1792"/>
<name status="sbaa94f" import="i613591" active="True">Ancient Europe</name>
</item>
</content>
</novelWriterXML>
+10 -10
View File
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-23 15:34:21">
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.4" timeStamp="2022-10-17 20:32:06">
<project>
<name>Test Minimal</name>
<title>Minimal</title>
<author>Jane Doe</author>
<author>John Doh</author>
<saveCount>17</saveCount>
<saveCount>19</saveCount>
<autoCount>2</autoCount>
<editTime>150</editTime>
<editTime>167</editTime>
</project>
<settings>
<doBackup>True</doBackup>
@@ -16,7 +16,7 @@
<spellLang>None</spellLang>
<lastEdited>None</lastEdited>
<lastViewed>None</lastViewed>
<lastNovel>None</lastNovel>
<lastNovel>a508bb932959c</lastNovel>
<lastOutline>None</lastOutline>
<lastWordCount>10</lastWordCount>
<novelWordCount>10</novelWordCount>
@@ -48,20 +48,20 @@
<name status="s72322d" import="iffcacb">Novel</name>
</item>
<item handle="a35baf2e93843" parent="a508bb932959c" root="a508bb932959c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="28" wordCount="6" paraCount="1" cursorPos="33"/>
<name status="s72322d" import="iffcacb" exported="True">Title Page</name>
<meta expanded="False" mainHeading="H1" charCount="28" wordCount="6" paraCount="1" cursorPos="33"/>
<name status="s72322d" import="iffcacb" active="True">Title Page</name>
</item>
<item handle="a6d311a93600a" parent="a508bb932959c" root="a508bb932959c" order="1" type="FOLDER" class="NOVEL">
<meta expanded="True"/>
<name status="s72322d" import="iffcacb">New Chapter</name>
</item>
<item handle="f5ab3e30151e1" parent="a6d311a93600a" root="a508bb932959c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="11" wordCount="2" paraCount="0" cursorPos="16"/>
<name status="s72322d" import="iffcacb" exported="True">New Chapter</name>
<meta expanded="False" mainHeading="H2" charCount="11" wordCount="2" paraCount="0" cursorPos="16"/>
<name status="s72322d" import="iffcacb" active="True">New Chapter</name>
</item>
<item handle="8c659a11cd429" parent="a6d311a93600a" root="a508bb932959c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="9" wordCount="2" paraCount="0" cursorPos="15"/>
<name status="s72322d" import="iffcacb" exported="True">New Scene</name>
<meta expanded="False" mainHeading="H3" charCount="9" wordCount="2" paraCount="0" cursorPos="15"/>
<name status="s72322d" import="iffcacb" active="True">New Scene</name>
</item>
<item handle="7695ce551d265" parent="None" root="7695ce551d265" order="1" type="ROOT" class="PLOT">
<meta expanded="False"/>
+3 -3
View File
@@ -24,13 +24,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
# Mock GUI
# =========================================================================== #
class MockGuiMain():
class MockGuiMain:
def __init__(self):
self.mainConf = None
self.hasProject = True
self.theProject = None
self.statusBar = MockStatusBar()
self.mainStatus = MockStatusBar()
# Test Variables
self.askResponse = True
@@ -81,7 +81,7 @@ class MockGuiMain():
# END Class MockGuiMain
class MockStatusBar():
class MockStatusBar:
def __init__(self):
return
@@ -0,0 +1,33 @@
%%~name: Chapter 1
%%~path: 0000000000008/0000000000010
%%~kind: NOVEL/DOCUMENT
## Chapter 1
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum commodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, eget euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, vel semper lacus aliquam sit amet. Vestibulum vulputate neque ligula, rhoncus blandit turpis consequat id. Mauris sagittis vehicula imperdiet. Duis sed nunc pretium, ornare purus vel, sodales augue. Maecenas a suscipit risus. Quisque volutpat justo eleifend est ullamcorper fermentum. Donec ullamcorper et tortor a laoreet. Nam id risus nisi. Vivamus non imperdiet erat, sit amet imperdiet felis. Mauris vitae neque et est aliquam scelerisque non non ipsum.
Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis.
% Merge Novel Scene: Scene 1.1 [New]
### Scene 1.1
Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis.
Nullam a nisl magna. Praesent commodo nec diam aliquet vestibulum. In sapien velit, sodales feugiat porta ut, rhoncus a elit. Quisque egestas nisi eu eros laoreet, quis facilisis est pretium. Nullam bibendum sed tellus nec lobortis. Duis elit massa, volutpat a lacinia a, ullamcorper in dui. Suspendisse ac laoreet dui. Curabitur elementum, tortor elementum ultricies laoreet, nunc massa vulputate augue, vitae tincidunt nunc enim eget nisl.
% Merge Novel Scene: Scene 1.2 [New]
### Scene 1.2
Nullam a nisl magna. Praesent commodo nec diam aliquet vestibulum. In sapien velit, sodales feugiat porta ut, rhoncus a elit. Quisque egestas nisi eu eros laoreet, quis facilisis est pretium. Nullam bibendum sed tellus nec lobortis. Duis elit massa, volutpat a lacinia a, ullamcorper in dui. Suspendisse ac laoreet dui. Curabitur elementum, tortor elementum ultricies laoreet, nunc massa vulputate augue, vitae tincidunt nunc enim eget nisl.
Pellentesque nibh urna, volutpat et feugiat porta, rutrum sed lectus. Aliquam eget risus id orci tincidunt condimentum et sit amet purus. Curabitur tincidunt odio vel ante feugiat feugiat. Proin nunc lorem, molestie a sapien et, varius elementum nunc. Donec non fermentum nisl. In et massa placerat, faucibus felis eu, congue nisi. Proin sed tortor non lorem mattis cursus. Vestibulum magna neque, bibendum vel nibh et, tincidunt rhoncus nisi. Duis pulvinar mi a quam rutrum maximus. Nunc sollicitudin, urna in cursus facilisis, augue neque imperdiet metus, ac finibus lorem ante id nulla. Sed maximus eleifend justo id feugiat. Cras eget diam vel est blandit tempor nec a leo. Mauris risus est, fringilla in aliquam a, sagittis vel enim. Nullam sodales id erat placerat lobortis.
% Merge Novel Scene: Scene 1.3 [New]
### Scene 1.3
Pellentesque nibh urna, volutpat et feugiat porta, rutrum sed lectus. Aliquam eget risus id orci tincidunt condimentum et sit amet purus. Curabitur tincidunt odio vel ante feugiat feugiat. Proin nunc lorem, molestie a sapien et, varius elementum nunc. Donec non fermentum nisl. In et massa placerat, faucibus felis eu, congue nisi. Proin sed tortor non lorem mattis cursus. Vestibulum magna neque, bibendum vel nibh et, tincidunt rhoncus nisi. Duis pulvinar mi a quam rutrum maximus. Nunc sollicitudin, urna in cursus facilisis, augue neque imperdiet metus, ac finibus lorem ante id nulla. Sed maximus eleifend justo id feugiat. Cras eget diam vel est blandit tempor nec a leo. Mauris risus est, fringilla in aliquam a, sagittis vel enim. Nullam sodales id erat placerat lobortis.
Integer ac gravida quam. Quisque eleifend nisl nec pretium tincidunt. Quisque sollicitudin nisi in hendrerit scelerisque. Sed ornare nisl lacus, sit amet consectetur lectus egestas et. Vivamus nec arcu lorem. Donec rhoncus, purus a porta accumsan, nunc lectus iaculis libero, et fringilla tellus augue et velit. Integer varius felis scelerisque, vulputate tellus eu, laoreet justo. Suspendisse sit amet sem vehicula, auctor odio sed, aliquet enim. In ac tortor sed tortor fringilla elementum. Nulla non odio at magna vulputate scelerisque. Nam elementum diam eu rutrum scelerisque. Sed fermentum, felis quis vulputate fermentum, libero metus sollicitudin est, in faucibus purus nulla non dolor. Ut vitae felis porta, feugiat nunc et, bibendum neque. Nullam nec lorem nec metus ullamcorper malesuada ut a nisl. Etiam eget tristique dui. Nulla sed mi finibus, venenatis tellus non, maximus enim.
@@ -0,0 +1,35 @@
%%~name: All of Chapter 1
%%~path: 0000000000008/0000000000014
%%~kind: NOVEL/DOCUMENT
% Merge Novel Chapter: Chapter 1 [New]
## Chapter 1
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum commodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, eget euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, vel semper lacus aliquam sit amet. Vestibulum vulputate neque ligula, rhoncus blandit turpis consequat id. Mauris sagittis vehicula imperdiet. Duis sed nunc pretium, ornare purus vel, sodales augue. Maecenas a suscipit risus. Quisque volutpat justo eleifend est ullamcorper fermentum. Donec ullamcorper et tortor a laoreet. Nam id risus nisi. Vivamus non imperdiet erat, sit amet imperdiet felis. Mauris vitae neque et est aliquam scelerisque non non ipsum.
Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis.
% Merge Novel Scene: Scene 1.1 [New]
### Scene 1.1
Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis.
Nullam a nisl magna. Praesent commodo nec diam aliquet vestibulum. In sapien velit, sodales feugiat porta ut, rhoncus a elit. Quisque egestas nisi eu eros laoreet, quis facilisis est pretium. Nullam bibendum sed tellus nec lobortis. Duis elit massa, volutpat a lacinia a, ullamcorper in dui. Suspendisse ac laoreet dui. Curabitur elementum, tortor elementum ultricies laoreet, nunc massa vulputate augue, vitae tincidunt nunc enim eget nisl.
% Merge Novel Scene: Scene 1.2 [New]
### Scene 1.2
Nullam a nisl magna. Praesent commodo nec diam aliquet vestibulum. In sapien velit, sodales feugiat porta ut, rhoncus a elit. Quisque egestas nisi eu eros laoreet, quis facilisis est pretium. Nullam bibendum sed tellus nec lobortis. Duis elit massa, volutpat a lacinia a, ullamcorper in dui. Suspendisse ac laoreet dui. Curabitur elementum, tortor elementum ultricies laoreet, nunc massa vulputate augue, vitae tincidunt nunc enim eget nisl.
Pellentesque nibh urna, volutpat et feugiat porta, rutrum sed lectus. Aliquam eget risus id orci tincidunt condimentum et sit amet purus. Curabitur tincidunt odio vel ante feugiat feugiat. Proin nunc lorem, molestie a sapien et, varius elementum nunc. Donec non fermentum nisl. In et massa placerat, faucibus felis eu, congue nisi. Proin sed tortor non lorem mattis cursus. Vestibulum magna neque, bibendum vel nibh et, tincidunt rhoncus nisi. Duis pulvinar mi a quam rutrum maximus. Nunc sollicitudin, urna in cursus facilisis, augue neque imperdiet metus, ac finibus lorem ante id nulla. Sed maximus eleifend justo id feugiat. Cras eget diam vel est blandit tempor nec a leo. Mauris risus est, fringilla in aliquam a, sagittis vel enim. Nullam sodales id erat placerat lobortis.
% Merge Novel Scene: Scene 1.3 [New]
### Scene 1.3
Pellentesque nibh urna, volutpat et feugiat porta, rutrum sed lectus. Aliquam eget risus id orci tincidunt condimentum et sit amet purus. Curabitur tincidunt odio vel ante feugiat feugiat. Proin nunc lorem, molestie a sapien et, varius elementum nunc. Donec non fermentum nisl. In et massa placerat, faucibus felis eu, congue nisi. Proin sed tortor non lorem mattis cursus. Vestibulum magna neque, bibendum vel nibh et, tincidunt rhoncus nisi. Duis pulvinar mi a quam rutrum maximus. Nunc sollicitudin, urna in cursus facilisis, augue neque imperdiet metus, ac finibus lorem ante id nulla. Sed maximus eleifend justo id feugiat. Cras eget diam vel est blandit tempor nec a leo. Mauris risus est, fringilla in aliquam a, sagittis vel enim. Nullam sodales id erat placerat lobortis.
Integer ac gravida quam. Quisque eleifend nisl nec pretium tincidunt. Quisque sollicitudin nisi in hendrerit scelerisque. Sed ornare nisl lacus, sit amet consectetur lectus egestas et. Vivamus nec arcu lorem. Donec rhoncus, purus a porta accumsan, nunc lectus iaculis libero, et fringilla tellus augue et velit. Integer varius felis scelerisque, vulputate tellus eu, laoreet justo. Suspendisse sit amet sem vehicula, auctor odio sed, aliquet enim. In ac tortor sed tortor fringilla elementum. Nulla non odio at magna vulputate scelerisque. Nam elementum diam eu rutrum scelerisque. Sed fermentum, felis quis vulputate fermentum, libero metus sollicitudin est, in faucibus purus nulla non dolor. Ut vitae felis porta, feugiat nunc et, bibendum neque. Nullam nec lorem nec metus ullamcorper malesuada ut a nisl. Etiam eget tristique dui. Nulla sed mi finibus, venenatis tellus non, maximus enim.
@@ -6,31 +6,26 @@
},
"itemIndex": {
"7a992350f3eb6": {
"level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Lorem Ipsum", "tag": "", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""}
}
},
"8c58a65414c23": {
"level": "H0",
"headings": {
"T000000": {"level": "H0", "title": "", "tag": "", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""}
}
},
"88d59a277361b": {
"level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Prologue", "tag": "", "cCount": 584, "wCount": 92, "pCount": 1, "synopsis": "Explanation from the lipsum.com website."}
}
},
"db7e733775d4d": {
"level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Act One", "tag": "", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""}
}
},
"fb609cd8319dc": {
"level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Chapter One", "tag": "", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."}
},
@@ -39,7 +34,6 @@
}
},
"88243afbe5ed8": {
"level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene One", "tag": "", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."},
"T000013": {"level": "H4", "title": "Scene One, Section Two", "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""}
@@ -49,7 +43,6 @@
}
},
"f96ec11c6a3da": {
"level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene Two", "tag": "", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."},
"T000015": {"level": "H4", "title": "Scene Two, Section Two", "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""}
@@ -59,13 +52,11 @@
}
},
"846352075de7d": {
"level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Why do we use it?", "tag": "", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""}
}
},
"441420a886d82": {
"level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Chapter Two", "tag": "", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."}
},
@@ -74,7 +65,6 @@
}
},
"eb103bc70c90c": {
"level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene Three", "tag": "", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."}
},
@@ -83,7 +73,6 @@
}
},
"f8c0562e50f1b": {
"level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene Four", "tag": "", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."}
},
@@ -92,7 +81,6 @@
}
},
"47666c91c7ccf": {
"level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene Five", "tag": "", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."}
},
@@ -101,7 +89,6 @@
}
},
"4c4f28287af27": {
"level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Nobody Owens", "tag": "Bod", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""}
},
@@ -110,13 +97,11 @@
}
},
"2426c6f0ca922": {
"level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Main Plot", "tag": "Main", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""}
}
},
"04468803b92e1": {
"level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Ancient Europe", "tag": "Europe", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""}
}
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:28">
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.4" timeStamp="2022-10-20 18:21:56">
<project>
<name>Test Custom</name>
<title>Test Novel</title>
@@ -48,80 +48,80 @@
<name status="s000008" import="i00000c">Novel</name>
</item>
<item handle="0000000000011" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Title Page</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Title Page</name>
</item>
<item handle="0000000000012" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Chapter 1</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Chapter 1</name>
</item>
<item handle="0000000000013" parent="0000000000012" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 1.1</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Scene 1.1</name>
</item>
<item handle="0000000000014" parent="0000000000012" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 1.2</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Scene 1.2</name>
</item>
<item handle="0000000000015" parent="0000000000012" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 1.3</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Scene 1.3</name>
</item>
<item handle="0000000000016" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Chapter 2</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Chapter 2</name>
</item>
<item handle="0000000000017" parent="0000000000016" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 2.1</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Scene 2.1</name>
</item>
<item handle="0000000000018" parent="0000000000016" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 2.2</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Scene 2.2</name>
</item>
<item handle="0000000000019" parent="0000000000016" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 2.3</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Scene 2.3</name>
</item>
<item handle="000000000001a" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Chapter 3</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Chapter 3</name>
</item>
<item handle="000000000001b" parent="000000000001a" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 3.1</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Scene 3.1</name>
</item>
<item handle="000000000001c" parent="000000000001a" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 3.2</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Scene 3.2</name>
</item>
<item handle="000000000001d" parent="000000000001a" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 3.3</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Scene 3.3</name>
</item>
<item handle="000000000001e" parent="None" root="000000000001e" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Plot</name>
</item>
<item handle="000000000001f" parent="000000000001e" root="000000000001e" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Main Plot</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Main Plot</name>
</item>
<item handle="0000000000020" parent="None" root="0000000000020" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Characters</name>
</item>
<item handle="0000000000021" parent="0000000000020" root="0000000000020" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Protagonist</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Protagonist</name>
</item>
<item handle="0000000000022" parent="None" root="0000000000022" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Locations</name>
</item>
<item handle="0000000000023" parent="0000000000022" root="0000000000022" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Main Location</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Main Location</name>
</item>
<item handle="0000000000024" parent="None" root="0000000000024" order="0" type="ROOT" class="ARCHIVE">
<meta expanded="False"/>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-06-12 17:45:51">
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.4" timeStamp="2022-10-20 18:21:56">
<project>
<name>Test Custom</name>
<title>Test Novel</title>
@@ -48,56 +48,56 @@
<name status="s000008" import="i00000c">Novel</name>
</item>
<item handle="0000000000011" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Title Page</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Title Page</name>
</item>
<item handle="0000000000012" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 1</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Scene 1</name>
</item>
<item handle="0000000000013" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 2</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Scene 2</name>
</item>
<item handle="0000000000014" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 3</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Scene 3</name>
</item>
<item handle="0000000000015" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 4</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Scene 4</name>
</item>
<item handle="0000000000016" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 5</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Scene 5</name>
</item>
<item handle="0000000000017" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 6</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Scene 6</name>
</item>
<item handle="0000000000018" parent="None" root="0000000000018" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Plot</name>
</item>
<item handle="0000000000019" parent="0000000000018" root="0000000000018" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Main Plot</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Main Plot</name>
</item>
<item handle="000000000001a" parent="None" root="000000000001a" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Characters</name>
</item>
<item handle="000000000001b" parent="000000000001a" root="000000000001a" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Protagonist</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Protagonist</name>
</item>
<item handle="000000000001c" parent="None" root="000000000001c" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Locations</name>
</item>
<item handle="000000000001d" parent="000000000001c" root="000000000001c" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Main Location</name>
<meta expanded="False" mainHeading="H0" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" active="True">Main Location</name>
</item>
<item handle="000000000001e" parent="None" root="000000000001e" order="0" type="ROOT" class="ARCHIVE">
<meta expanded="False"/>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-07-31 18:40:28">
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.4" timeStamp="2022-10-20 18:21:56">
<project>
<name>New Project</name>
<title>New Novel</title>
@@ -17,9 +17,9 @@
<lastViewed>None</lastViewed>
<lastNovel>None</lastNovel>
<lastOutline>None</lastOutline>
<lastWordCount>2</lastWordCount>
<novelWordCount>1</novelWordCount>
<notesWordCount>1</notesWordCount>
<lastWordCount>13</lastWordCount>
<novelWordCount>10</novelWordCount>
<notesWordCount>3</notesWordCount>
<autoReplace/>
<titleFormat>
<title>%title%</title>
@@ -29,62 +29,62 @@
<section></section>
</titleFormat>
<status>
<entry key="s000008" count="7" red="100" green="100" blue="100">New</entry>
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
<entry key="s000000" count="7" red="100" green="100" blue="100">New</entry>
<entry key="s000001" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s000002" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s000003" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry key="i00000c" count="4" red="100" green="100" blue="100">New</entry>
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
<entry key="i000004" count="4" red="100" green="100" blue="100">New</entry>
<entry key="i000005" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i000006" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="11">
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Novel</name>
<name status="s000000" import="i000004">Novel</name>
</item>
<item handle="0000000000011" parent="None" root="0000000000011" order="0" type="ROOT" class="PLOT">
<item handle="0000000000009" parent="None" root="0000000000009" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Plot</name>
<name status="s000000" import="i000004">Plot</name>
</item>
<item handle="0000000000012" parent="None" root="0000000000012" order="0" type="ROOT" class="CHARACTER">
<item handle="000000000000a" parent="None" root="000000000000a" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Characters</name>
<name status="s000000" import="i000004">Characters</name>
</item>
<item handle="0000000000013" parent="None" root="0000000000013" order="0" type="ROOT" class="WORLD">
<item handle="000000000000b" parent="None" root="000000000000b" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/>
<name status="s000008" import="i00000c">World</name>
<name status="s000000" import="i000004">World</name>
</item>
<item handle="0000000000014" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Title Page</name>
<item handle="000000000000c" parent="0000000000008" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" mainHeading="H1" charCount="20" wordCount="5" paraCount="1" cursorPos="0"/>
<name status="s000000" import="i000004" active="True">Title Page</name>
</item>
<item handle="0000000000015" parent="0000000000010" root="0000000000010" order="0" type="FOLDER" class="NOVEL">
<item handle="000000000000d" parent="0000000000008" root="0000000000008" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">New Chapter</name>
<name status="s000000" import="i000004">New Chapter</name>
</item>
<item handle="0000000000016" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Chapter</name>
<item handle="000000000000e" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" mainHeading="H2" charCount="11" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="s000000" import="i000004" active="True">New Chapter</name>
</item>
<item handle="0000000000017" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Scene</name>
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" mainHeading="H3" charCount="9" wordCount="2" paraCount="0" cursorPos="0"/>
<name status="s000000" import="i000004" active="True">New Scene</name>
</item>
<item handle="0000000000028" parent="0000000000015" root="0000000000010" order="0" type="FOLDER" class="NOVEL">
<item handle="0000000000020" parent="0000000000008" root="0000000000008" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Stuff</name>
<name status="s000000" import="i000004">Stuff</name>
</item>
<item handle="0000000000029" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="5" wordCount="1" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Hello</name>
<item handle="0000000000021" parent="0000000000020" root="0000000000008" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" mainHeading="H2" charCount="5" wordCount="1" paraCount="0" cursorPos="0"/>
<name status="s000000" import="i000004" active="True">Hello</name>
</item>
<item handle="000000000002a" parent="0000000000012" root="0000000000012" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="4" wordCount="1" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Jane</name>
<item handle="0000000000022" parent="000000000000a" root="000000000000a" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" mainHeading="H1" charCount="11" wordCount="3" paraCount="1" cursorPos="0"/>
<name status="s000000" import="i000004" active="True">Jane</name>
</item>
</content>
</novelWriterXML>

Some files were not shown because too many files have changed in this diff Show More