Merge branch 'main' into i18n-de_DE-created

This commit is contained in:
Veronica Berglyd Olsen
2022-11-13 10:52:13 +01:00
committed by GitHub
214 changed files with 23012 additions and 14496 deletions
+2 -2
View File
@@ -14,7 +14,7 @@ jobs:
testLinux: testLinux:
strategy: strategy:
matrix: matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"] python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Python Setup - name: Python Setup
@@ -25,7 +25,7 @@ jobs:
- name: Install Packages (apt) - name: Install Packages (apt)
run: | run: |
sudo apt update sudo apt update
sudo apt install libenchant-dev qttools5-dev-tools sudo apt install libenchant-dev qttools5-dev-tools aspell-en
- name: Checkout Source - name: Checkout Source
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Install Dependencies (pip) - name: Install Dependencies (pip)
+333 -4
View File
@@ -1,7 +1,237 @@
# novelWriter Changelog # novelWriter Changelog
## Version 2.0 RC 2 [2022-11-13]
### 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.
### Detailed Changelog
Note: This release introduces a new Project XML format with version number 1.5. When the project is
opened, a request to update the file format will show up.
**Bugfixes**
* The custom folders for user defined themes and syntax were not created properly when the app was
first launched on a new computer. The folders were successfully created on a second launch. The
error was handled, but reported. This was caused by the folder creation process being in the
wrong order. Issue #1180. PR #1184.
* Fixed context menu entries for split and merge having inconsistent labels. Issue #1199. PR #1197.
**User Interface**
* The exported status for document items have been renamed to active/inactive. Their icons have
also been updated. Issues #1196 and #1198. PRs #1200 and #1216.
* The status/importance context menu now shows which label is the current. Issue #1202. PR #1207.
* The status/importance context menu now has a "Manage Labels" action that opens the Project
Settings dialog at the correct place. Issue #1203. PR #1207.
* Both GUI theme and syntax theme can now be updated without restarting the app. Issue #1171.
PR #1212.
* The GUI theme now determines which icon theme is to be loaded. It is no longer a separate
setting. The icon theme can also be reloaded without restart. Issue #1172. PR #1212.
* The block formating features in the Format menu now also works on empty lines. Issue #1178.
PR #1214.
* There is now a Format menu entry and shortcut code for synopsis comments. Issue #1177. PR #1214.
* The split document dialog now has the option to move teh source document to trash. Issue #1179.
PR #1217.
**Other Changes**
* Archived documents are now partially indexed. This mainly means that the item will have the
correct document icon in the project tree corresponding to its main heading. Issue #1176.
PR #1183.
* The option to add notes files in the Project Wizard now automatically switches off if there are
no notes categories enabled. Issue #1192. PR #1201.
* When a project is opened for the first time, the first document in the project is also opened.
Issue #1219. PR #1223.
* When there is no project open, the toolbars on the Project Tree, Novel View and Outline View are
disabled. They are enabled only when a project is loaded. Issue #1220. PR #1230.
**Installation and Packaging**
* The AppImage release now has version information in the package name. Issue #1182. PR #1218.
**Code Improvements**
* The main heading of a document is now stored in the item class instead of the index. PR #1183.
* The common module checker functions no longer allow None values. The only one needing it was the
string checker. A new string checker that allows None has been added for those cases. This makes
type discovery in the code editor easier. Issue #1185. PR #1188.
* Verbose logging has been removed. The lowest severity level is now DEBUG. Issue #1186. PR #1191.
* Added a number of None checks in the code where especially Qt calls could potentially return
None, even if they were unlikely to do so. PR #1197.
* Renamed the status bar attribute in the main GUI class as it conflicts with a Qt method.
Issue #1190. PR #1197.
* The data access methods for the custom config file parser have been improved to better report
correct type information. PR #1197.
* Saving and loading of XML data is now handled by a separate set of reader and writer classes. The
reader class is capable of reading all file formats that have been used thus far. The various
data classes have been improved, and a new XML file formart version 1.5 added. Issue #1189.
PRs #1221 and #1232.
* The index is now automatically rebuilt when the project file format is updated. Issue #1235.
PR #1236.
* The project folder on disk is now wrapped in a storage class that the project accesses files
through. It also handles lock files and archiving used for backup. The change is in preparation
for adding a potential single file format. Issue #1222. PR #1225.
* The Project Wizard now creates the project on disk, and then opens it. This replaces the old
method where the new project was built directly into the current session. This caused a few
inconsistencies from time to time, and was a duplicate way of getting a project into the session.
Issue #1152. PR #1225.
* The Config class has been refactored extensively and now also uses pathlib for all paths. Tests
are also switched to using pathlib. Issue #1224. PRs #1228 and #1229.
* The updating of tree order method of the project tree class has been updated for better
performance. PR #1236.
----
## 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] ## 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 ### Release Notes
This is a beta release of the next release version, and is intended for testing purposes. Please be This is a beta release of the next release version, and is intended for testing purposes. Please be
@@ -89,6 +319,105 @@ final release.
---- ----
## Version 1.6.6 [2022-10-25]
### Release Notes
This is a bugfix release that fixes a minor issues with following tags in the editor. It is now
possible to also follow tags that contain spaces.
### Detailed Changelog
**Bugfixes**
* Fix a bug where only the word under the cursor would be looked up when the user tried to follow a
tag in the editor. The lookup function now uses the same parser for the `@`-line as the syntax
highlighter does, so they should behave consistently. Issue #1195. PR #1209.
----
## Version 1.6.5 [2022-10-13]
### Release Notes
This is a bugfix release that fixes 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
This is a bugfix release that fixes a critical bug in the insert non-breaking spaces feature. It
basically no longer worked in the 1.6.3 release. This release also fixes a minor issue where the
text cursor sometimes disappears when reaching the right-hand edge of the text editor window.
### Detailed Changelog
**Bugfixes**
* Fixed a bug in the auto-replace feature of the editor that caused a crash when using the insert
non-breaking spaces feature was used. Issue #1118. PR #1120.
* Back ported a bugfix from 1.7 RC1 that resolves an issue with the text cursor sometimes
disappearing at the right-hand edge of the text editor. Issues #1112 and #1119. PR #1120.
----
## Version 1.6.3 [2022-08-18]
### Release Notes
This is a bugfix release that fixes a rare problem causing novelWriter to crash if the spell
checker language setting was configured to an empty value.
A few other minor issues have also been fixed: The project language setting is now properly
exported to ODT documents. Spaces are no longer inserted automatically in front of colons in
certain meta data settings when the feature is enabled (it is primarily used for French). Lastly,
the slider splitting the editor and viewer panels can no longer be dragged until the viewer
disappears. It was not necessarily obvious how the viewer panel could be restored in such cases.
### Detailed Changelog
**Bugfixes**
* Fixed an issue where the project language setting was not exported when building Open Document
files. Issue #1073. PR #1087.
* Fixed an issue where the splitter in the main window could be dragged until it hid the document
viewer panel. This is no longer possible. Issue #1085. PR #1087.
* Fixed an issue where an empty spell check language setting would crash novelWriter. Issue #1096.
PR #1098.
* Added a checker that blocks the automatic insertion of spaces in front of special characters in
the cases where the character is a colon in either a meta tag, or as part of the synopsis
keyword. This feature is used for certain languages like French and Spanish. Issue #1090.
PR #1099.
----
## Version 1.6.2 [2022-03-20] ## Version 1.6.2 [2022-03-20]
### Release Notes ### Release Notes
@@ -3468,8 +3797,8 @@ helpful feedback and issue reports for the new features added in this, and previ
**User Interface** **User Interface**
* Added a preferences dialog for the program settings. No longer necessary to edit the config file. * Added a preferences dialog for the program settings. It is no longer necessary to edit the config
PR #30. file. PR #30.
* The document viewer remembers scroll bar position when pressing `Ctrl+R` on a document already * The document viewer remembers scroll bar position when pressing `Ctrl+R` on a document already
being viewed. PR #28. being viewed. PR #28.
* Removed version number from windows title. PR #28. * Removed version number from windows title. PR #28.
@@ -3510,8 +3839,8 @@ helpful feedback and issue reports for the new features added in this, and previ
**Status Bar** **Status Bar**
* Redesign of the status bar adding project and session stats as well as a session timer. PR #21. * Redesign of the status bar adding project and session stats as well as a session timer. PR #21.
* Project word count is written to the project file, which is needed for the session word count. PR * Project word count is written to the project file, which is needed for the session word count.
#21. PR #21.
* Closing a project now clears the status bar. PR #21. * Closing a project now clears the status bar. PR #21.
**Editor** **Editor**
+1 -1
View File
@@ -33,7 +33,7 @@ You can also follow novelWriter on Mastodon at [fosstodon.org/@novelwriter](http
## Implementation ## 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 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. 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. extension, and don't end up with two.
The QSS files are Qt Style Sheet files. See Qt's 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. details.
+1 -1
View File
@@ -34,7 +34,7 @@ The following Python packages are needed to run novelWriter:
* ``lxml`` needed for full XML support. * ``lxml`` needed for full XML support.
* ``PyEnchant`` needed for spell checking (optional). * ``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 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 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. 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 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 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 here. The system paths are provided by the Qt QStandardPaths_ class and its AppDataLocation.
on Qt 5.4 or greater, or DataLocation for earlier versions.
The standard paths are: The standard paths are:
+2
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:`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:`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:`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:`N`", "Create new project item."
":kbd:`Ctrl`:kbd:`O`", "Open selected document." ":kbd:`Ctrl`:kbd:`O`", "Open selected document."
":kbd:`Ctrl`:kbd:`Q`", "Exit novelWriter." ":kbd:`Ctrl`:kbd:`Q`", "Exit novelWriter."
@@ -136,6 +137,7 @@ a key or key combination for the inserted content.
":kbd:`Ctrl`:kbd:`K`, :kbd:`F`", "Insert a ``@focus`` keyword." ":kbd:`Ctrl`:kbd:`K`, :kbd:`F`", "Insert a ``@focus`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`C`", "Insert a ``@char`` keyword." ":kbd:`Ctrl`:kbd:`K`, :kbd:`C`", "Insert a ``@char`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`P`", "Insert a ``@plot`` keyword." ":kbd:`Ctrl`:kbd:`K`, :kbd:`P`", "Insert a ``@plot`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`S`", "Insert a synopsis comment."
":kbd:`Ctrl`:kbd:`K`, :kbd:`T`", "Insert a ``@time`` keyword." ":kbd:`Ctrl`:kbd:`K`, :kbd:`T`", "Insert a ``@time`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`L`", "Insert a ``@location`` keyword." ":kbd:`Ctrl`:kbd:`K`, :kbd:`L`", "Insert a ``@location`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`O`", "Insert an ``@object`` keyword." ":kbd:`Ctrl`:kbd:`K`, :kbd:`O`", "Insert an ``@object`` keyword."
+1336 -1343
View File
File diff suppressed because it is too large Load Diff
+1338 -1345
View File
File diff suppressed because it is too large Load Diff
+1342 -1349
View File
File diff suppressed because it is too large Load Diff
+8 -46
View File
@@ -27,7 +27,6 @@ import sys
import getopt import getopt
import logging import logging
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QErrorMessage from PyQt5.QtWidgets import QApplication, QErrorMessage
from novelwriter.error import exceptionHandler, logException from novelwriter.error import exceptionHandler, logException
@@ -60,9 +59,9 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen" __author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net" __email__ = "code@vkbo.net"
__version__ = "1.7-beta1" __version__ = "2.0-rc2"
__hexversion__ = "0x010700b1" __hexversion__ = "0x020000c2"
__date__ = "2022-05-17" __date__ = "2022-11-13"
__status__ = "Stable" __status__ = "Stable"
__domain__ = "novelwriter.io" __domain__ = "novelwriter.io"
__url__ = "https://novelwriter.io" __url__ = "https://novelwriter.io"
@@ -72,32 +71,6 @@ __helpurl__ = "https://github.com/vkbo/novelWriter/discussions"
__releaseurl__ = "https://github.com/vkbo/novelWriter/releases/latest" __releaseurl__ = "https://github.com/vkbo/novelWriter/releases/latest"
__docurl__ = "https://novelwriter.readthedocs.io" __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__) logger = logging.getLogger(__name__)
@@ -122,7 +95,6 @@ def main(sysArgs=None):
"version", "version",
"info", "info",
"debug", "debug",
"verbose",
"style=", "style=",
"config=", "config=",
"data=", "data=",
@@ -143,7 +115,6 @@ def main(sysArgs=None):
" -v, --version Print program version and exit.\n" " -v, --version Print program version and exit.\n"
" --info Print additional runtime information.\n" " --info Print additional runtime information.\n"
" --debug Print debug output. Includes --info.\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" " --style= Sets Qt5 style flag. Defaults to 'Fusion'.\n"
" --config= Alternative config file.\n" " --config= Alternative config file.\n"
" --data= Alternative user data path.\n" " --data= Alternative user data path.\n"
@@ -181,9 +152,6 @@ def main(sysArgs=None):
elif inOpt == "--debug": elif inOpt == "--debug":
logLevel = logging.DEBUG logLevel = logging.DEBUG
logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}" 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": elif inOpt == "--style":
qtStyle = inArg qtStyle = inArg
elif inOpt == "--config": elif inOpt == "--config":
@@ -193,9 +161,6 @@ def main(sysArgs=None):
elif inOpt == "--testmode": elif inOpt == "--testmode":
testMode = True testMode = True
# Set Config Options
CONFIG.cmdOpen = cmdOpen
# Set Logging # Set Logging
cHandle = logging.StreamHandler() cHandle = logging.StreamHandler()
cHandle.setFormatter(logging.Formatter(fmt=logFormat, style="{")) cHandle.setFormatter(logging.Formatter(fmt=logFormat, style="{"))
@@ -214,14 +179,14 @@ def main(sysArgs=None):
"At least Python 3.7 is required, found %s" % CONFIG.verPyString "At least Python 3.7 is required, found %s" % CONFIG.verPyString
) )
errorCode |= 0x04 errorCode |= 0x04
if CONFIG.verQtValue < 50300: if CONFIG.verQtValue < 51000:
errorData.append( 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 errorCode |= 0x08
if CONFIG.verPyQtValue < 50300: if CONFIG.verPyQtValue < 51000:
errorData.append( 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 errorCode |= 0x10
@@ -280,7 +245,6 @@ def main(sysArgs=None):
nwApp = QApplication([CONFIG.appName, (f"-style={qtStyle}")]) nwApp = QApplication([CONFIG.appName, (f"-style={qtStyle}")])
nwApp.setApplicationName(CONFIG.appName) nwApp.setApplicationName(CONFIG.appName)
nwApp.setApplicationVersion(__version__) nwApp.setApplicationVersion(__version__)
nwApp.setWindowIcon(QIcon(CONFIG.appIcon))
nwApp.setOrganizationDomain(__domain__) nwApp.setOrganizationDomain(__domain__)
# Connect the exception handler before making the main GUI # Connect the exception handler before making the main GUI
@@ -289,9 +253,7 @@ def main(sysArgs=None):
# Launch main GUI # Launch main GUI
CONFIG.initLocalisation(nwApp) CONFIG.initLocalisation(nwApp)
nwGUI = GuiMain() nwGUI = GuiMain()
if not nwGUI.hasProject: nwGUI.postLaunchTasks(cmdOpen)
nwGUI.showProjectLoadDialog()
nwGUI.releaseNotes()
sys.exit(nwApp.exec_()) sys.exit(nwApp.exec_())
@@ -18,10 +18,10 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Map] [Map]
add = typ_plus.svg add = typ_plus.svg
backward = typ_chevron-left.svg backward = typ_chevron-left.svg
bookmark = typ_bookmark.svg
bullet-off = typ_media-record-outline.svg bullet-off = typ_media-record-outline.svg
bullet-on = typ_media-record.svg bullet-on = typ_media-record.svg
check = typ_tick.svg checked = mixed_input-checked.svg
clear = typ_backspace.svg
close = typ_times.svg close = typ_times.svg
cls_archive = typ_delete.svg cls_archive = typ_delete.svg
cls_character = typ_user.svg cls_character = typ_user.svg
@@ -35,32 +35,25 @@ cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg cls_trash = typ_trash.svg
cls_world = typ_location.svg cls_world = typ_location.svg
cross = typ_times.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 down = typ_chevron-down.svg
edit = typ_pencil.svg edit = typ_pencil.svg
forward = typ_chevron-right.svg forward = typ_chevron-right.svg
hash = typ_hash.svg
maximise = typ_arrow-maximise.svg maximise = typ_arrow-maximise.svg
menu = typ_th-menu.svg menu = typ_th-menu.svg
minimise = typ_arrow-minimise.svg minimise = typ_arrow-minimise.svg
noncheckable = mixed_input-none.svg
proj_chapter = mixed_document-chapter.svg proj_chapter = mixed_document-chapter.svg
proj_details = typ_th-list-grey.svg proj_details = typ_th-list-grey.svg
proj_document = typ_document-text.svg proj_document = typ_document-text.svg
proj_folder = typ_folder.svg proj_folder = typ_folder.svg
proj_note = mixed_document-note.svg proj_note = mixed_document-note.svg
proj_scene = mixed_document-scene.svg proj_scene = mixed_document-scene.svg
proj_section = mixed_document-section.svg
proj_stats = typ_chart-bar-grey.svg proj_stats = typ_chart-bar-grey.svg
proj_title = mixed_document-title.svg proj_title = mixed_document-title.svg
reference = typ_at.svg reference = typ_at.svg
refresh = typ_refresh.svg refresh = typ_refresh.svg
remove = typ_minus.svg remove = typ_minus.svg
save = typ_download.svg
search = typ_search.svg search = typ_search.svg
search_cancel = typ_cancel-grey.svg search_cancel = typ_cancel-grey.svg
search_case = nw_search-case.svg search_case = nw_search-case.svg
@@ -78,13 +71,16 @@ status_stats = typ_chart-bar-grey.svg
status_time = typ_stopwatch-grey.svg status_time = typ_stopwatch-grey.svg
sticky-off = typ_pin-outline.svg sticky-off = typ_pin-outline.svg
sticky-on = typ_pin.svg sticky-on = typ_pin.svg
unchecked = mixed_input-unchecked.svg
up = typ_chevron-up.svg up = typ_chevron-up.svg
view_build = typ_export.svg view_build = typ_export.svg
view_editor = mixed_edit.svg view_editor = mixed_edit.svg
view_novel = typ_book-grey.svg view_novel = typ_book-grey.svg
view_outline = typ_puzzle-outline.svg view_outline = typ_puzzle-outline.svg
deco_doc_h0 = nw_deco-h0.svg
deco_doc_h1 = nw_deco-h1.svg deco_doc_h1 = nw_deco-h1.svg
deco_doc_h2 = nw_deco-h2.svg deco_doc_h2 = nw_deco-h2.svg
deco_doc_h3 = nw_deco-h3.svg deco_doc_h3 = nw_deco-h3.svg
deco_doc_h4 = nw_deco-h4.svg deco_doc_h4 = nw_deco-h4.svg
deco_doc_more = nw_deco-noveltree-more.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

@@ -0,0 +1,39 @@
<?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"
width="24"
height="24"
version="1.2"
viewBox="0 0 24 24"
id="svg2287">
<defs
id="defs2291" />
<metadata
id="metadata2281">
<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>
<path
d="m17.714 22h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
fill="#848484"
stroke-width="1.4286"
id="path2283" />
<path
d="m19.02 3.3503c-0.60933 0.010623-1.2003 0.31545-1.5586 0.86719l-5.0977 7.8477-2.3672-3.6445c-0.57329-0.88278-1.7442-1.1319-2.627-0.55859-0.88278 0.57329-1.1319 1.7461-0.55859 2.6289l3.9199 6.0391c0.38514 0.59307 1.0409 0.88965 1.6973 0.85352 0.03023-2.78e-4 0.05972-0.0041 0.08984-0.0059 0.0556-0.0057 0.11074-0.0088 0.16602-0.01953 0.52104-0.07621 1.0077-0.36147 1.3184-0.83984l6.6445-10.23c0.57329-0.88278 0.32419-2.0556-0.55859-2.6289-0.33104-0.21498-0.70276-0.31497-1.0684-0.30859z"
fill="#9c9"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.111"
id="path2285" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,44 @@
<?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"
width="24"
height="24"
version="1.2"
viewBox="0 0 24 24"
id="svg2931">
<defs
id="defs2935" />
<metadata
id="metadata2925">
<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>
<path
d="m2 17.714v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h11.429c2.3629 0 4.2857 1.9229 4.2857 4.2857v7.1429c0 0.79-0.64 1.4286-1.4286 1.4286-0.78857 0-1.4286-0.63857-1.4286-1.4286v-7.1429c0-0.78857-0.64143-1.4286-1.4286-1.4286h-11.429c-0.78714 0-1.4286 0.64-1.4286 1.4286v11.429c0 0.78857 0.64143 1.4286 1.4286 1.4286h4.2857c0.78857 0 1.4286 0.63857 1.4286 1.4286s-0.64 1.4286-1.4286 1.4286h-4.2857c-2.3629 0-4.2857-1.9229-4.2857-4.2857zm15.714 4.2857h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
fill="#848484"
stroke-width="1.4286"
id="path2927" />
<rect
x="7"
y="10.1"
width="10"
height="3.8"
rx="1.9"
ry="1.9"
fill="#f99157"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.111"
id="rect2929" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,39 @@
<?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"
width="24"
height="24"
version="1.2"
viewBox="0 0 24 24"
id="svg3574">
<defs
id="defs3578" />
<metadata
id="metadata3568">
<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>
<path
d="m17.714 22h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
fill="#848484"
stroke-width="1.4286"
id="path3570" />
<path
d="m17.99 3.3379c-0.48501 0.025418-0.96034 0.23584-1.3125 0.62695l-4.6777 5.1953-1.332-1.4785c-0.70433-0.78223-1.9014-0.84495-2.6836-0.14062-0.78223 0.70433-0.84495 1.9014-0.14062 2.6836l1.5996 1.7754-1.5977 1.7754c-0.70433 0.78223-0.64161 1.9793 0.14062 2.6836 0.78223 0.70433 1.9773 0.64161 2.6816-0.14062l1.332-1.4785 1.332 1.4785c0.70433 0.78224 1.9014 0.84495 2.6836 0.14062 0.78223-0.70433 0.84495-1.9014 0.14062-2.6836l-1.5996-1.7754 4.9453-5.4922c0.70433-0.78223 0.64161-1.9793-0.14062-2.6836-0.39112-0.35216-0.88608-0.51175-1.3711-0.48633z"
fill="#d64848"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.111"
id="path3572" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg2161"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<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" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs2165" />
<path
d="M 9.7,18.2 16,12 9.7,5.8 C 9.5,5.6 9.2,5.5 9,5.5 8.8,5.5 8.5,5.6 8.3,5.8 8.1,6 8,6.2 8,6.5 v 11 c 0,0.3 0.1,0.5 0.3,0.7 0.2,0.2 0.5,0.3 0.7,0.3 0.2,0 0.5,-0.1 0.7,-0.3 z"
id="path3096"
style="fill:#848484;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 928 B

@@ -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" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
id="svg2608"> id="svg901">
<metadata <metadata
id="metadata2614"> id="metadata907">
<rdf:RDF> <rdf:RDF>
<cc:Work <cc:Work
rdf:about=""> rdf:about="">
@@ -23,9 +23,9 @@
</rdf:RDF> </rdf:RDF>
</metadata> </metadata>
<defs <defs
id="defs2612" /> id="defs905" />
<path <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" 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" id="path899"
style="stroke-width:1.05081;fill:#6699cc;fill-opacity:1" /> style="fill:#6699cc;fill-opacity:1;stroke-width:1.05081" />
</svg> </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

@@ -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="svg896">
<metadata
id="metadata902">
<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="defs900" />
<path
d="M 19.576035,3.3487939 C 18.237131,2.6038813 16.550693,3.0884898 15.809935,4.4246248 L 10.66893,13.676495 7.726664,10.734229 c -1.0813694,-1.0813704 -2.8342677,-1.0813704 -3.9156371,0 -1.0813693,1.081369 -1.0813693,2.834267 0,3.915637 l 5.538383,5.538383 c 0.523378,0.524762 1.2295221,0.812758 1.9578191,0.812758 l 0.383533,-0.02769 c 0.859834,-0.12046 1.614439,-0.636914 2.03674,-1.397057 L 20.650482,7.1148966 C 21.39401,5.777375 20.91217,4.0923218 19.576035,3.3487939 Z"
id="path894"
style="stroke-width:1.3846;fill:#99cc99;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -18,10 +18,10 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
[Map] [Map]
add = typ_plus.svg add = typ_plus.svg
backward = typ_chevron-left.svg backward = typ_chevron-left.svg
bookmark = typ_bookmark.svg
bullet-off = typ_media-record-outline.svg bullet-off = typ_media-record-outline.svg
bullet-on = typ_media-record.svg bullet-on = typ_media-record.svg
check = typ_tick.svg checked = mixed_input-checked.svg
clear = typ_backspace.svg
close = typ_times.svg close = typ_times.svg
cls_archive = typ_delete.svg cls_archive = typ_delete.svg
cls_character = typ_user.svg cls_character = typ_user.svg
@@ -35,32 +35,25 @@ cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg cls_trash = typ_trash.svg
cls_world = typ_location.svg cls_world = typ_location.svg
cross = typ_times.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 down = typ_chevron-down.svg
edit = typ_pencil.svg edit = typ_pencil.svg
forward = typ_chevron-right.svg forward = typ_chevron-right.svg
hash = typ_hash.svg
maximise = typ_arrow-maximise.svg maximise = typ_arrow-maximise.svg
menu = typ_th-menu.svg menu = typ_th-menu.svg
minimise = typ_arrow-minimise.svg minimise = typ_arrow-minimise.svg
noncheckable = mixed_input-none.svg
proj_chapter = mixed_document-chapter.svg proj_chapter = mixed_document-chapter.svg
proj_details = typ_th-list-grey.svg proj_details = typ_th-list-grey.svg
proj_document = typ_document-text.svg proj_document = typ_document-text.svg
proj_folder = typ_folder.svg proj_folder = typ_folder.svg
proj_note = mixed_document-note.svg proj_note = mixed_document-note.svg
proj_scene = mixed_document-scene.svg proj_scene = mixed_document-scene.svg
proj_section = mixed_document-section.svg
proj_stats = typ_chart-bar-grey.svg proj_stats = typ_chart-bar-grey.svg
proj_title = mixed_document-title.svg proj_title = mixed_document-title.svg
reference = typ_at.svg reference = typ_at.svg
refresh = typ_refresh.svg refresh = typ_refresh.svg
remove = typ_minus.svg remove = typ_minus.svg
save = typ_download.svg
search = typ_search.svg search = typ_search.svg
search_cancel = typ_cancel-grey.svg search_cancel = typ_cancel-grey.svg
search_case = nw_search-case.svg search_case = nw_search-case.svg
@@ -78,13 +71,16 @@ status_stats = typ_chart-bar-grey.svg
status_time = typ_stopwatch-grey.svg status_time = typ_stopwatch-grey.svg
sticky-off = typ_pin-outline.svg sticky-off = typ_pin-outline.svg
sticky-on = typ_pin.svg sticky-on = typ_pin.svg
unchecked = mixed_input-unchecked.svg
up = typ_chevron-up.svg up = typ_chevron-up.svg
view_build = typ_export.svg view_build = typ_export.svg
view_editor = mixed_edit.svg view_editor = mixed_edit.svg
view_novel = typ_book-grey.svg view_novel = typ_book-grey.svg
view_outline = typ_puzzle-outline.svg view_outline = typ_puzzle-outline.svg
deco_doc_h0 = nw_deco-h0.svg
deco_doc_h1 = nw_deco-h1.svg deco_doc_h1 = nw_deco-h1.svg
deco_doc_h2 = nw_deco-h2.svg deco_doc_h2 = nw_deco-h2.svg
deco_doc_h3 = nw_deco-h3.svg deco_doc_h3 = nw_deco-h3.svg
deco_doc_h4 = nw_deco-h4.svg deco_doc_h4 = nw_deco-h4.svg
deco_doc_more = nw_deco-noveltree-more.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

@@ -0,0 +1,39 @@
<?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"
width="24"
height="24"
version="1.2"
viewBox="0 0 24 24"
id="svg879">
<defs
id="defs883" />
<metadata
id="metadata873">
<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>
<path
d="m17.714 22h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
fill="#333"
stroke-width="1.4286"
id="path875" />
<path
d="m19.02 3.3503c-0.60933 0.010623-1.2003 0.31545-1.5586 0.86719l-5.0977 7.8477-2.3672-3.6445c-0.57329-0.88278-1.7442-1.1319-2.627-0.55859-0.88278 0.57329-1.1319 1.7461-0.55859 2.6289l3.9199 6.0391c0.38514 0.59307 1.0409 0.88965 1.6973 0.85352 0.03023-2.78e-4 0.05972-0.0041 0.08984-0.0059 0.0556-0.0057 0.11074-0.0088 0.16602-0.01953 0.52104-0.07621 1.0077-0.36147 1.3184-0.83984l6.6445-10.23c0.57329-0.88278 0.32419-2.0556-0.55859-2.6289-0.33104-0.21498-0.70276-0.31497-1.0684-0.30859z"
fill="#718c00"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.111"
id="path877" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,44 @@
<?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"
width="24"
height="24"
version="1.2"
viewBox="0 0 24 24"
id="svg973">
<defs
id="defs977" />
<metadata
id="metadata967">
<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>
<path
d="m2 17.714v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h11.429c2.3629 0 4.2857 1.9229 4.2857 4.2857v7.1429c0 0.79-0.64 1.4286-1.4286 1.4286-0.78857 0-1.4286-0.63857-1.4286-1.4286v-7.1429c0-0.78857-0.64143-1.4286-1.4286-1.4286h-11.429c-0.78714 0-1.4286 0.64-1.4286 1.4286v11.429c0 0.78857 0.64143 1.4286 1.4286 1.4286h4.2857c0.78857 0 1.4286 0.63857 1.4286 1.4286s-0.64 1.4286-1.4286 1.4286h-4.2857c-2.3629 0-4.2857-1.9229-4.2857-4.2857zm15.714 4.2857h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
fill="#333"
stroke-width="1.4286"
id="path969" />
<rect
x="7"
y="10.1"
width="10"
height="3.8"
rx="1.9"
ry="1.9"
fill="#f5871f"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.111"
id="rect971" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,39 @@
<?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"
width="24"
height="24"
version="1.2"
viewBox="0 0 24 24"
id="svg1616">
<defs
id="defs1620" />
<metadata
id="metadata1610">
<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>
<path
d="m17.714 22h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
fill="#333"
stroke-width="1.4286"
id="path1612" />
<path
d="m17.99 3.3379c-0.48501 0.025418-0.96034 0.23584-1.3125 0.62695l-4.6777 5.1953-1.332-1.4785c-0.70433-0.78223-1.9014-0.84495-2.6836-0.14062-0.78223 0.70433-0.84495 1.9014-0.14062 2.6836l1.5996 1.7754-1.5977 1.7754c-0.70433 0.78223-0.64161 1.9793 0.14062 2.6836 0.78223 0.70433 1.9773 0.64161 2.6816-0.14062l1.332-1.4785 1.332 1.4785c0.70433 0.78224 1.9014 0.84495 2.6836 0.14062 0.78223-0.70433 0.84495-1.9014 0.14062-2.6836l-1.5996-1.7754 4.9453-5.4922c0.70433-0.78223 0.64161-1.9793-0.14062-2.6836-0.39112-0.35216-0.88608-0.51175-1.3711-0.48633z"
fill="#c82829"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.111"
id="path1614" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg2161"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<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" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs2165" />
<path
d="M 9.7,18.2 16,12 9.7,5.8 C 9.5,5.6 9.2,5.5 9,5.5 8.8,5.5 8.5,5.6 8.3,5.8 8.1,6 8,6.2 8,6.5 v 11 c 0,0.3 0.1,0.5 0.3,0.7 0.2,0.2 0.5,0.3 0.7,0.3 0.2,0 0.5,-0.1 0.7,-0.3 z"
id="path3096"
style="fill:#848484;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 928 B

@@ -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:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
id="svg2608" version="1.2"
viewBox="0 0 24 24"
height="24"
width="24" width="24"
version="1.2"> height="24"
viewBox="0 0 24 24"
id="svg901">
<metadata <metadata
id="metadata2614"> id="metadata907">
<rdf:RDF> <rdf:RDF>
<cc:Work <cc:Work
rdf:about=""> rdf:about="">
@@ -23,9 +23,9 @@
</rdf:RDF> </rdf:RDF>
</metadata> </metadata>
<defs <defs
id="defs2612" /> id="defs905" />
<path <path
style="stroke-width:1.05081;fill:#4271ae;fill-opacity:1" 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" id="path899"
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" /> style="fill:#4271ae;fill-opacity:1;stroke-width:1.05081" />
</svg> </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

@@ -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="svg896">
<metadata
id="metadata902">
<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="defs900" />
<path
d="M 19.576035,3.3487939 C 18.237131,2.6038813 16.550693,3.0884898 15.809935,4.4246248 L 10.66893,13.676495 7.726664,10.734229 c -1.0813694,-1.0813704 -2.8342677,-1.0813704 -3.9156371,0 -1.0813693,1.081369 -1.0813693,2.834267 0,3.915637 l 5.538383,5.538383 c 0.523378,0.524762 1.2295221,0.812758 1.9578191,0.812758 l 0.383533,-0.02769 c 0.859834,-0.12046 1.614439,-0.636914 2.03674,-1.397057 L 20.650482,7.1148966 C 21.39401,5.777375 20.91217,4.0923218 19.576035,3.3487939 Z"
id="path894"
style="stroke-width:1.3846;fill:#718c00;fill-opacity:1" />
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

+4 -4
View File
@@ -2,11 +2,11 @@
<html> <html>
<body> <body>
<h2>Release Notes for 1.7 Beta 1</h2> <h2>Release Notes for 2.0 RC 2</h2>
<p><i>Released on 17 May 2022</i></p> <p><i>Released on 13 November 2022</i></p>
<p>This is a beta release of the next release version, and is intended for testing purposes. Please <p>This is a release candidate of the next release version, and is intended for testing purposes.
be careful when using this version on live writing projects, and make sure you take frequent Please be careful when using this version on live writing projects, and make sure you take frequent
backups.</p> backups.</p>
<p>Please check the changelog for an overview of changes. The full release notes will be added to <p>Please check the changelog for an overview of changes. The full release notes will be added to
the final release.</p> the final release.</p>
+3 -1
View File
@@ -1,2 +1,4 @@
[Main] [Main]
name = Default System Theme name = Default Theme
description = Qt standard colours
icontheme = typicons_light
+9 -7
View File
@@ -1,16 +1,18 @@
[Main] [Main]
name = Default Dark Theme name = Default Dark Theme
author = Veronica Berglyd Olsen description = The novelWriter standard dark theme
credit = Veronica Berglyd Olsen author = Veronica Berglyd Olsen
url = https://github.com/vkbo/novelWriter credit = Veronica Berglyd Olsen
license = CC BY-SA 4.0 url = https://github.com/vkbo/novelWriter
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/ license = CC BY-SA 4.0
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
icontheme = typicons_dark
[Palette] [Palette]
window = 54, 54, 54 window = 54, 54, 54
windowtext = 174, 174, 174 windowtext = 174, 174, 174
base = 62, 62, 62 base = 62, 62, 62
alternatebase = 67, 67, 67 alternatebase = 78, 78, 78
text = 174, 174, 174 text = 174, 174, 174
tooltipbase = 255, 255, 192 tooltipbase = 255, 255, 192
tooltiptext = 21, 21, 13 tooltiptext = 21, 21, 13
@@ -1,5 +0,0 @@
/**
* Default Theme: Dark
* This theme doesn't use any custom styles, so the file is only here as
* an example. There doesn't have to be a styles.qss file in the folder.
*/
@@ -5,12 +5,13 @@ credit = Ethan Schoonover
url = https://ethanschoonover.com/solarized/ url = https://ethanschoonover.com/solarized/
license = MIT license = MIT
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
icontheme = typicons_dark
[Palette] [Palette]
window = 0, 43, 54 window = 0, 43, 54
windowtext = 253, 246, 227 windowtext = 253, 246, 227
base = 7, 54, 66 base = 7, 54, 66
alternatebase = 67, 67, 67 alternatebase = 0, 43, 54
text = 253, 246, 227 text = 253, 246, 227
tooltipbase = 133, 153, 0 tooltipbase = 133, 153, 0
tooltiptext = 0, 43, 54 tooltiptext = 0, 43, 54
@@ -5,6 +5,7 @@ credit = Ethan Schoonover
url = https://ethanschoonover.com/solarized/ url = https://ethanschoonover.com/solarized/
license = MIT license = MIT
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
icontheme = typicons_light
[Palette] [Palette]
window = 238, 232, 213 window = 238, 232, 213
+147 -155
View File
@@ -23,11 +23,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import json import json
import uuid
import hashlib import hashlib
import logging import logging
from pathlib import Path
from datetime import datetime from datetime import datetime
from configparser import ConfigParser from configparser import ConfigParser
@@ -45,52 +46,55 @@ logger = logging.getLogger(__name__)
# Checker Functions # Checker Functions
# =============================================================================================== # # =============================================================================================== #
def checkString(value, default, allowNone=False): def checkStringNone(value, default):
"""Check if a variable is a string or a None. """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 return None
if isinstance(value, str): if isinstance(value, str):
return str(value) return str(value)
return default return default
def checkInt(value, default, allowNone=False): def checkString(value, default):
"""Check if a variable is an integer or a None. """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: try:
return int(value) return int(value)
except Exception: except Exception:
return default return default
def checkFloat(value, default, allowNone=False): def checkFloat(value, default):
"""Check if a variable is a float or a None. """Check if a variable is a float.
""" """
if allowNone and (value is None or value == "None"):
return None
try: try:
return float(value) return float(value)
except Exception: except Exception:
return default return default
def checkBool(value, default, allowNone=False): def checkBool(value, default):
"""Check if a variable is a boolean or a None. """Check if a variable is a boolean.
""" """
if allowNone and (value is None or value == "None"): if isinstance(value, bool):
return None return value
elif isinstance(value, str):
if isinstance(value, str): check = value.lower()
if value == "True": if check in ("true", "yes", "on"):
return True return True
elif value == "False": elif check in ("false", "no", "off"):
return False return False
else: else:
return default return default
elif isinstance(value, int): elif isinstance(value, int):
if value == 1: if value == 1:
return True return True
@@ -98,7 +102,6 @@ def checkBool(value, default, allowNone=False):
return False return False
else: else:
return default return default
return default return default
@@ -112,6 +115,26 @@ def checkHandle(value, default, allowNone=False):
return default return default
def checkUuid(value, default):
"""Try to process a value as an uuid, or return a default.
"""
try:
return str(uuid.UUID(value))
except Exception:
return default
def checkPath(value, default):
"""Check if a value is a valid path. Non-empty strings are accepted.
"""
if isinstance(value, Path):
return value
elif isinstance(value, str):
if value.strip():
return Path(value)
return default
# =============================================================================================== # # =============================================================================================== #
# Validator Functions # Validator Functions
# =============================================================================================== # # =============================================================================================== #
@@ -174,16 +197,6 @@ def hexToInt(value, default=0):
return default 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): def minmax(value, minVal, maxVal):
"""Make sure an integer is between min and max value (inclusive). """Make sure an integer is between min and max value (inclusive).
""" """
@@ -225,25 +238,25 @@ def formatInt(value):
return str(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 """Take a number (on the format returned by time.time()) and convert
it to a timestamp string. it to a timestamp string.
""" """
if fileSafe: if fileSafe:
return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_FSTAMP) return datetime.fromtimestamp(value).strftime(nwConst.FMT_FSTAMP)
else: 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 """Format a time in seconds in HH:MM:SS format or d-HH:MM:SS format
if a full day or longer. if a full day or longer.
""" """
if isinstance(tS, int): if isinstance(t, int):
if tS >= 86400: if t >= 86400:
return f"{tS//86400:d}-{tS%86400//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}" return f"{t//86400:d}-{t%86400//3600:02d}:{t%3600//60:02d}:{t%60:02d}"
else: 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" return "ERROR"
@@ -258,12 +271,18 @@ def simplified(string):
return " ".join(str(string).strip().split()) return " ".join(str(string).strip().split())
def yesNo(value):
"""Convert a boolean evaluated variable to a yes or no.
"""
return "yes" if value else "no"
def splitVersionNumber(value): def splitVersionNumber(value):
"""Split a version string on the form aa.bb.cc into major, minor """Split a version string on the form aa.bb.cc into major, minor
and patch, and computes an integer value aabbcc. and patch, and computes an integer value aabbcc.
""" """
if not isinstance(value, str): if not isinstance(value, str):
return [0, 0, 0, 0] return 0, 0, 0, 0
vMajor = 0 vMajor = 0
vMinor = 0 vMinor = 0
@@ -282,114 +301,114 @@ def splitVersionNumber(value):
vInt = vMajor*10000 + vMinor*100 + vPatch 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 """Transfers the case of the source word to the target word. This
will consider all upper or lower, and first char capitalisation. 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 return theResult
if len(theTarget) < 1 or len(theSource) < 1: if len(target) < 1 or len(source) < 1:
return theResult return theResult
if theSource.istitle(): if source.istitle():
theResult = theTarget.title() theResult = target.title()
if theSource.isupper(): if source.isupper():
theResult = theTarget.upper() theResult = target.upper()
elif theSource.islower(): elif source.islower():
theResult = theTarget.lower() theResult = target.lower()
return theResult return theResult
def fuzzyTime(secDiff): def fuzzyTime(seconds):
"""Converts a time difference in seconds into a fuzzy time string. """Converts a time difference in seconds into a fuzzy time string.
""" """
if secDiff < 0: if seconds < 0:
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "in the future" "Common", "in the future"
) )
elif secDiff < 30: elif seconds < 30:
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "just now" "Common", "just now"
) )
elif secDiff < 90: elif seconds < 90:
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "a minute ago" "Common", "a minute ago"
) )
elif secDiff < 3300: # 55 minutes elif seconds < 3300: # 55 minutes
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "{0} minutes ago" "Common", "{0} minutes ago"
).format(int(round(secDiff/60))) ).format(int(round(seconds/60)))
elif secDiff < 5400: # 90 minutes elif seconds < 5400: # 90 minutes
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "an hour ago" "Common", "an hour ago"
) )
elif secDiff < 84600: # 23.5 hours elif seconds < 84600: # 23.5 hours
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "{0} hours ago" "Common", "{0} hours ago"
).format(int(round(secDiff/3600))) ).format(int(round(seconds/3600)))
elif secDiff < 129600: # 1.5 days elif seconds < 129600: # 1.5 days
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "a day ago" "Common", "a day ago"
) )
elif secDiff < 561600: # 6.5 days elif seconds < 561600: # 6.5 days
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "{0} days ago" "Common", "{0} days ago"
).format(int(round(secDiff/86400))) ).format(int(round(seconds/86400)))
elif secDiff < 907200: # 10.5 days elif seconds < 907200: # 10.5 days
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "a week ago" "Common", "a week ago"
) )
elif secDiff < 2419200: # 28 days elif seconds < 2419200: # 28 days
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "{0} weeks ago" "Common", "{0} weeks ago"
).format(int(round(secDiff/604800))) ).format(int(round(seconds/604800)))
elif secDiff < 3888000: # 45 days elif seconds < 3888000: # 45 days
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "a month ago" "Common", "a month ago"
) )
elif secDiff < 29808000: # 345 days elif seconds < 29808000: # 345 days
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "{0} months ago" "Common", "{0} months ago"
).format(int(round(secDiff/2592000))) ).format(int(round(seconds/2592000)))
elif secDiff < 47336400: # 1.5 years elif seconds < 47336400: # 1.5 years
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "a year ago" "Common", "a year ago"
) )
else: else:
return QCoreApplication.translate( return QCoreApplication.translate(
"Common", "{0} years ago" "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. """Convert an integer to a Roman number.
""" """
if not isinstance(numVal, int): if not isinstance(value, int):
return "NAN" return "NAN"
if numVal < 1 or numVal > 4999: if value < 1 or value > 4999:
return "OOR" return "OOR"
theValues = [ lookup = [
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"), (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"), (50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
] ]
romNum = "" roman = ""
for theDiv, theSym in theValues: for divisor, symbol in lookup:
n = numVal//theDiv n = value//divisor
romNum += n*theSym roman += n*symbol
numVal -= n*theDiv value -= n*divisor
if numVal <= 0: if value <= 0:
break break
return romNum.lower() if toLower else romNum return roman.lower() if toLower else roman
# =============================================================================================== # # =============================================================================================== #
@@ -447,51 +466,47 @@ def jsonEncode(data, n=0, nmax=0):
# File and File System Functions # File and File System Functions
# =============================================================================================== # # =============================================================================================== #
def readTextFile(filePath): def readTextFile(path):
"""Read the content of a text file in a robust manner. """Read the content of a text file in a robust manner.
""" """
if not os.path.isfile(filePath): path = Path(path)
if not path.is_file():
return "" return ""
fileText = ""
try: try:
with open(filePath, mode="r", encoding="utf-8") as inFile: return path.read_text(encoding="utf-8")
fileText = inFile.read()
except Exception: except Exception:
logger.error("Could not read file: %s", filePath) logger.error("Could not read file: %s", path)
logException() logException()
return "" return ""
return fileText
def makeFileNameSafe(value): def makeFileNameSafe(value):
"""Returns a filename safe string of the value. """Returns a filename safe string of the value.
""" """
cleanName = "" clean = ""
for c in str(value).strip(): for c in str(value).strip():
if c.isalpha() or c.isdigit() or c == " ": if c.isalpha() or c.isdigit() or c == " ":
cleanName += c clean += c
return cleanName return clean
def sha256sum(filePath): def sha256sum(path):
"""Make a shasum of a file using a buffer. """Make a shasum of a file using a buffer.
Based on: https://stackoverflow.com/a/44873382/5825851 Based on: https://stackoverflow.com/a/44873382/5825851
""" """
hDigest = hashlib.sha256() digest = hashlib.sha256()
bData = bytearray(65536) bData = bytearray(65536)
mData = memoryview(bData) mData = memoryview(bData)
try: 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): for n in iter(lambda: inFile.readinto(mData), 0):
hDigest.update(mData[:n]) digest.update(mData[:n])
except Exception: except Exception:
logger.error("Could not create sha256sum of: %s", filePath) logger.error("Could not create sha256sum of: %s", path)
logException() logException()
return None return None
return hDigest.hexdigest() return digest.hexdigest()
# =============================================================================================== # # =============================================================================================== #
@@ -513,87 +528,64 @@ def getGuiItem(objName):
class NWConfigParser(ConfigParser): 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): def __init__(self):
super().__init__() super().__init__()
def rdStr(self, section, option, default): def rdStr(self, section, option, default):
"""Read string value. """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): def rdInt(self, section, option, default):
"""Read integer value. """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): def rdFlt(self, section, option, default):
"""Read float value. """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): def rdBool(self, section, option, default):
"""Read boolean value. """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 rdPath(self, section, option, default):
"""Read a path value.
"""
return checkPath(self.get(section, option, fallback=default), default)
def rdStrList(self, section, option, default): def rdStrList(self, section, option, default):
"""Read string list. """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): def rdIntList(self, section, option, default):
"""Read integer list. """Read integer list.
""" """
return self._parseLine(section, option, default, self.CNF_I_LST) result = default.copy() if isinstance(default, list) else []
##
# 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.
"""
if self.has_option(section, option): if self.has_option(section, option):
try: data = self.get(section, option, fallback="").split(",")
if type == self.CNF_STR: for i in range(min(len(data), len(result))):
return self.get(section, option) result[i] = checkInt(data[i].strip(), result[i])
elif type == self.CNF_INT: return result
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
# END Class NWConfigParser # END Class NWConfigParser
+454 -582
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -68,6 +68,7 @@ class nwHeaders:
class nwFiles: class nwFiles:
CONF_FILE = "novelwriter.conf"
PROJ_FILE = "nwProject.nwx" PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt" PROJ_DICT = "wordlist.txt"
PROJ_LOCK = "nwProject.lock" PROJ_LOCK = "nwProject.lock"
@@ -157,6 +158,7 @@ class nwLabels:
"doc_h1": QT_TRANSLATE_NOOP("Constant", "Novel Title Page"), "doc_h1": QT_TRANSLATE_NOOP("Constant", "Novel Title Page"),
"doc_h2": QT_TRANSLATE_NOOP("Constant", "Novel Chapter"), "doc_h2": QT_TRANSLATE_NOOP("Constant", "Novel Chapter"),
"doc_h3": QT_TRANSLATE_NOOP("Constant", "Novel Scene"), "doc_h3": QT_TRANSLATE_NOOP("Constant", "Novel Scene"),
"doc_h4": QT_TRANSLATE_NOOP("Constant", "Novel Section"),
"note": QT_TRANSLATE_NOOP("Constant", "Project Note"), "note": QT_TRANSLATE_NOOP("Constant", "Project Note"),
} }
KEY_NAME = { KEY_NAME = {
+4 -2
View File
@@ -19,7 +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/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from novelwriter.core.document import NWDoc from novelwriter.core.coretools import DocMerger, DocSplitter, ProjectBuilder
from novelwriter.core.index import countWords from novelwriter.core.index import countWords
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.core.spellcheck import NWSpellEnchant from novelwriter.core.spellcheck import NWSpellEnchant
@@ -28,8 +28,10 @@ from novelwriter.core.toodt import ToOdt
from novelwriter.core.tomd import ToMarkdown from novelwriter.core.tomd import ToMarkdown
__all__ = [ __all__ = [
"DocMerger",
"DocSplitter",
"ProjectBuilder",
"countWords", "countWords",
"NWDoc",
"NWProject", "NWProject",
"NWSpellEnchant", "NWSpellEnchant",
"ToHtml", "ToHtml",
+453
View File
@@ -0,0 +1,453 @@
"""
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 shutil
import logging
import novelwriter
from time import time
from functools import partial
from PyQt5.QtCore import QCoreApplication
from novelwriter.enum import nwAlert
from novelwriter.common import minmax, simplified
from novelwriter.constants import nwItemClass
from novelwriter.core.project import NWProject
logger = logging.getLogger(__name__)
class DocMerger:
"""Document tool for merging a set of documents into a single new
document. The parameters are defined by the user using the
GuiDocMerge dialog.
"""
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 = self.theProject.storage.getDocument(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 = self.theProject.storage.getDocument(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:
"""Document tool for splitting a document into a set of new
documents. The parameters are defined by the user using the
GuiDocSplit dialog.
"""
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 or self._srcItem 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
newItem = self.theProject.tree[dHandle]
newItem.setStatus(self._srcItem.itemStatus)
newItem.setImport(self._srcItem.itemImport)
outDoc = self.theProject.storage.getDocument(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
class ProjectBuilder:
"""A class to build a new project from a set of user-defined
parameter provided by the New Projecty Wizard.
"""
def __init__(self, mainGui):
self.mainGui = mainGui
self.mainConf = novelwriter.CONFIG
self.tr = partial(QCoreApplication.translate, "NWProject")
return
##
# Methods
##
def buildProject(self, data):
"""Build a project from a data dictionary of specifications
provided by the wizard.
"""
if not isinstance(data, dict):
logger.error("Invalid call to newProject function")
return False
popMinimal = data.get("popMinimal", True)
popCustom = data.get("popCustom", False)
popSample = data.get("popSample", False)
# Check if we're extracting the sample project. This is handled
# differently as it isn't actually a new project, so we forward
# this to another function and return here.
if popSample:
return self._extractSampleProject(data)
projPath = data.get("projPath", None)
if projPath is None:
logger.error("No project path set for the new project")
return False
project = NWProject(self.mainGui)
if not project.storage.openProjectInPlace(projPath, newProject=True):
return False
lblNewProject = self.tr("New Project")
lblNewChapter = self.tr("New Chapter")
lblNewScene = self.tr("New Scene")
lblTitlePage = self.tr("Title Page")
lblByAuthors = self.tr("By")
# Settings
projName = data.get("projName", lblNewProject)
projTitle = data.get("projTitle", lblNewProject)
projAuthors = data.get("projAuthors", "")
project.data.setUuid(None)
project.data.setName(projName)
project.data.setTitle(projTitle)
project.data.setAuthors(projAuthors)
project.setDefaultStatusImport()
project._projOpened = int(time())
# Add Root Folders
hNovelRoot = project.newRoot(nwItemClass.NOVEL)
hTitlePage = project.newFile(lblTitlePage, hNovelRoot)
novelTitle = project.data.title if project.data.title else project.data.name
titlePage = f"#! {novelTitle}\n\n"
if project.data.authors:
titlePage += f">> {lblByAuthors} {project.getFormattedAuthors()} <<\n\n"
aDoc = project.storage.getDocument(hTitlePage)
aDoc.writeDocument(titlePage)
if popMinimal:
# Creating a minimal project with a few root folders and a
# single chapter with a single scene.
hChapter = project.newFile(lblNewChapter, hNovelRoot)
aDoc = project.storage.getDocument(hChapter)
aDoc.writeDocument(f"## {lblNewChapter}\n\n")
hScene = project.newFile(lblNewScene, hChapter)
aDoc = project.storage.getDocument(hScene)
aDoc.writeDocument(f"### {lblNewScene}\n\n")
project.newRoot(nwItemClass.PLOT)
project.newRoot(nwItemClass.CHARACTER)
project.newRoot(nwItemClass.WORLD)
project.newRoot(nwItemClass.ARCHIVE)
project.saveProject()
project.closeProject()
elif popCustom:
# Create a project structure based on selected root folders
# and a number of chapters and scenes selected in the
# wizard's custom page.
# Create chapters and scenes
numChapters = data.get("numChapters", 0)
numScenes = data.get("numScenes", 0)
chSynop = self.tr("Summary of the chapter.")
scSynop = self.tr("Summary of the scene.")
# Create chapters
if numChapters > 0:
for ch in range(numChapters):
chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
cHandle = project.newFile(chTitle, hNovelRoot)
aDoc = project.storage.getDocument(cHandle)
aDoc.writeDocument(f"## {chTitle}\n\n% Synopsis: {chSynop}\n\n")
# Create chapter scenes
if numScenes > 0:
for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
sHandle = project.newFile(scTitle, cHandle)
aDoc = project.storage.getDocument(sHandle)
aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
# Create scenes (no chapters)
elif numScenes > 0:
for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
sHandle = project.newFile(scTitle, hNovelRoot)
aDoc = project.storage.getDocument(sHandle)
aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
# Create notes folders
noteTitles = {
nwItemClass.PLOT: self.tr("Main Plot"),
nwItemClass.CHARACTER: self.tr("Protagonist"),
nwItemClass.WORLD: self.tr("Main Location"),
}
addNotes = data.get("addNotes", False)
for newRoot in data.get("addRoots", []):
if newRoot in nwItemClass:
rHandle = project.newRoot(newRoot)
if addNotes:
aHandle = project.newFile(noteTitles[newRoot], rHandle)
ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
aDoc = project.storage.getDocument(aHandle)
aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n")
# Also add the archive and trash folders
project.newRoot(nwItemClass.ARCHIVE)
project.trashFolder()
project.saveProject()
project.closeProject()
return True
##
# Internal Functions
##
def _extractSampleProject(self, data):
"""Make a copy of the sample project by extracting the
sample.zip file to the new path.
"""
projPath = data.get("projPath", None)
if projPath is None:
logger.error("No project path set for the example project")
return False
pkgSample = self.mainConf.assetPath("sample.zip")
if pkgSample.is_file():
try:
shutil.unpack_archive(pkgSample, projPath)
except Exception as exc:
self.mainGui.makeAlert(self.tr(
"Failed to create a new example project."
), nwAlert.ERROR, exception=exc)
return False
else:
self.mainGui.makeAlert(self.tr(
"Failed to create a new example project. "
"Could not find the necessary files. "
"They seem to be missing from this installation."
), nwAlert.ERROR)
return False
return True
# END Class ProjectBuilder
+40 -26
View File
@@ -23,9 +23,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import logging import logging
from pathlib import Path
from novelwriter.enum import nwItemLayout, nwItemClass from novelwriter.enum import nwItemLayout, nwItemClass
from novelwriter.error import formatException from novelwriter.error import formatException
from novelwriter.common import isHandle, sha256sum from novelwriter.common import isHandle, sha256sum
@@ -33,7 +34,7 @@ from novelwriter.common import isHandle, sha256sum
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class NWDoc(): class NWDocument:
def __init__(self, theProject, theHandle): def __init__(self, theProject, theHandle):
@@ -57,7 +58,7 @@ class NWDoc():
return return
def __repr__(self): def __repr__(self):
return f"<NWDoc handle={self._docHandle}>" return f"<NWDocument handle={self._docHandle}>"
def __bool__(self): def __bool__(self):
return self._docHandle is not None and bool(self._theItem) return self._docHandle is not None and bool(self._theItem)
@@ -73,7 +74,7 @@ class NWDoc():
empty string. If something went wrong, return None. empty string. If something went wrong, return None.
""" """
self._docError = "" self._docError = ""
if self._docHandle is None: if not isinstance(self._docHandle, str):
logger.error("No document handle set") logger.error("No document handle set")
return None return None
@@ -81,17 +82,22 @@ class NWDoc():
logger.error("Unknown novelWriter document") logger.error("Unknown novelWriter document")
return None return None
contentPath = self.theProject.storage.contentPath
if not isinstance(contentPath, Path):
logger.error("No content path set")
return None
docFile = self._docHandle+".nwd" docFile = self._docHandle+".nwd"
logger.debug("Opening document: %s", docFile) logger.debug("Opening document: %s", docFile)
docPath = os.path.join(self.theProject.projContent, docFile) docPath = contentPath / docFile
self._fileLoc = docPath self._fileLoc = docPath
theText = "" theText = ""
self._docMeta = {} self._docMeta = {}
self._prevHash = None self._prevHash = None
if os.path.isfile(docPath): if docPath.exists():
self._prevHash = sha256sum(docPath) self._prevHash = sha256sum(docPath)
try: try:
with open(docPath, mode="r", encoding="utf-8") as inFile: with open(docPath, mode="r", encoding="utf-8") as inFile:
@@ -125,17 +131,20 @@ class NWDoc():
if not. if not.
""" """
self._docError = "" self._docError = ""
if self._docHandle is None: if not isinstance(self._docHandle, str):
logger.error("No document handle set") logger.error("No document handle set")
return False return False
self.theProject.ensureFolderStructure() contentPath = self.theProject.storage.contentPath
if not isinstance(contentPath, Path):
logger.error("No content path set")
return False
docFile = self._docHandle+".nwd" docFile = self._docHandle+".nwd"
logger.debug("Saving document: %s", docFile) logger.debug("Saving document: %s", docFile)
docPath = os.path.join(self.theProject.projContent, docFile) docPath = contentPath / docFile
docTemp = os.path.join(self.theProject.projContent, docFile+"~") docTemp = docPath.with_suffix(".tmp")
if self._prevHash is not None and not forceWrite: if self._prevHash is not None and not forceWrite:
self._currHash = sha256sum(docPath) self._currHash = sha256sum(docPath)
@@ -164,7 +173,7 @@ class NWDoc():
# If we're here, the file was successfully saved, so we can # If we're here, the file was successfully saved, so we can
# replace the temp file with the actual file # replace the temp file with the actual file
try: try:
os.replace(docTemp, docPath) docTemp.replace(docPath)
except OSError as exc: except OSError as exc:
self._docError = formatException(exc) self._docError = formatException(exc)
return False return False
@@ -179,23 +188,28 @@ class NWDoc():
from the project data folder. from the project data folder.
""" """
self._docError = "" self._docError = ""
if self._docHandle is None: if not isinstance(self._docHandle, str):
logger.error("No document handle set") logger.error("No document handle set")
return False return False
chkList = [ contentPath = self.theProject.storage.contentPath
os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd"), if not isinstance(contentPath, Path):
os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd~"), logger.error("No content path set")
] return False
for chkFile in chkList: docPath = contentPath / f"{self._docHandle}.nwd"
if os.path.isfile(chkFile): docTemp = docPath.with_suffix(".tmp")
try:
os.unlink(chkFile) try:
logger.debug("Deleted: %s", chkFile) # ToDo: When Python 3.7 is dropped, these can be changed to
except Exception as exc: # path.unlink(missing_ok=True)
self._docError = formatException(exc) if docPath.exists():
return False docPath.unlink()
if docTemp.exists():
docTemp.unlink()
except Exception as exc:
self._docError = formatException(exc)
return False
return True return True
@@ -206,7 +220,7 @@ class NWDoc():
def getFileLocation(self): def getFileLocation(self):
"""Return the file location of the current document. """Return the file location of the current document.
""" """
return self._fileLoc return str(self._fileLoc)
def getCurrentItem(self): def getCurrentItem(self):
"""Return a pointer to the currently open NWItem. """Return a pointer to the currently open NWItem.
@@ -263,4 +277,4 @@ class NWDoc():
return return
# END Class NWDoc # END Class NWDocument
+110 -100
View File
@@ -26,16 +26,15 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import json import json
import logging import logging
from time import time from time import time
from pathlib import Path
from novelwriter.enum import nwItemType, nwItemLayout from novelwriter.enum import nwItemType, nwItemLayout
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode, nwHeaders from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode, nwHeaders
from novelwriter.core.document import NWDoc
from novelwriter.common import ( from novelwriter.common import (
checkInt, isHandle, isItemClass, isTitleTag, jsonEncode checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
) )
@@ -59,23 +58,23 @@ class NWIndex:
The index data is cached in a JSON file between writing sessions. The index data is cached in a JSON file between writing sessions.
""" """
def __init__(self, theProject): def __init__(self, project):
self.theProject = theProject self._project = project
# Storage and State # Storage and State
self._tagsIndex = TagsIndex() self._tagsIndex = TagsIndex()
self._itemIndex = ItemIndex(theProject) self._itemIndex = ItemIndex(project)
self._indexBroken = False self._indexBroken = False
# TimeStamps # TimeStamps
self._indexChange = 0 self._indexChange = 0.0
self._rootChange = {} self._rootChange = {}
return return
def __repr__(self): def __repr__(self):
return f"<NWIndex project='{self.theProject.projName}'>" return f"<NWIndex project='{self._project.data.name}'>"
## ##
# Properties # Properties
@@ -94,10 +93,22 @@ class NWIndex:
""" """
self._tagsIndex.clear() self._tagsIndex.clear()
self._itemIndex.clear() self._itemIndex.clear()
self._indexChange = 0 self._indexChange = 0.0
self._rootChange = {} self._rootChange = {}
return return
def rebuildIndex(self):
"""Rebuild the entire index from scratch.
"""
self.clearIndex()
for nwItem in self._project.tree:
if nwItem is not None and nwItem.isFileType():
tHandle = nwItem.itemHandle
theDoc = self._project.storage.getDocument(tHandle)
self.scanText(tHandle, theDoc.readDocument() or "")
self._indexBroken = False
return
def deleteHandle(self, tHandle): def deleteHandle(self, tHandle):
"""Delete all entries of a given document handle. """Delete all entries of a given document handle.
""" """
@@ -114,11 +125,11 @@ class NWIndex:
moved from the archive or trash folders back into the active moved from the archive or trash folders back into the active
project. project.
""" """
if not self.theProject.tree.checkType(tHandle, nwItemType.FILE): if not self._project.tree.checkType(tHandle, nwItemType.FILE):
return False return False
logger.debug("Re-indexing item '%s'", tHandle) logger.debug("Re-indexing item '%s'", tHandle)
theDoc = NWDoc(self.theProject, tHandle) theDoc = self._project.storage.getDocument(tHandle)
self.scanText(tHandle, theDoc.readDocument() or "") self.scanText(tHandle, theDoc.readDocument() or "")
return True return True
@@ -126,13 +137,13 @@ class NWIndex:
def indexChangedSince(self, checkTime): def indexChangedSince(self, checkTime):
"""Check if the index has changed since a given time. """Check if the index has changed since a given time.
""" """
return self._indexChange > checkTime return self._indexChange > float(checkTime)
def rootChangedSince(self, rootHandle, checkTime): def rootChangedSince(self, rootHandle, checkTime):
"""Check if the index has changed since a given time for a """Check if the index has changed since a given time for a
given root item. given root item.
""" """
return self._rootChange.get(rootHandle, self._indexChange) > checkTime return self._rootChange.get(rootHandle, self._indexChange) > float(checkTime)
## ##
# Load and Save Index to/from File # Load and Save Index to/from File
@@ -141,12 +152,15 @@ class NWIndex:
def loadIndex(self): def loadIndex(self):
"""Load index from last session from the project meta folder. """Load index from last session from the project meta folder.
""" """
indexFile = self._project.storage.getMetaFile(nwFiles.INDEX_FILE)
if not isinstance(indexFile, Path):
return False
theData = {} theData = {}
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
tStart = time() tStart = time()
self._indexBroken = False self._indexBroken = False
if os.path.isfile(indexFile): if indexFile.exists():
logger.debug("Loading index file") logger.debug("Loading index file")
try: try:
with open(indexFile, mode="r", encoding="utf-8") as inFile: with open(indexFile, mode="r", encoding="utf-8") as inFile:
@@ -169,14 +183,14 @@ class NWIndex:
logger.debug("Checking index") logger.debug("Checking index")
# Check that all files are indexed # Check that all files are indexed
for fHandle in self.theProject.projFiles: for fHandle in self._project.projFiles:
if fHandle not in self._itemIndex: if fHandle not in self._itemIndex:
logger.warning("Item '%s' is not in the index", fHandle) logger.warning("Item '%s' is not in the index", fHandle)
self.reIndexHandle(fHandle) self.reIndexHandle(fHandle)
self._indexChange = round(time()) self._indexChange = time()
logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000) logger.debug("Index loaded in %.3f ms", (time() - tStart)*1000)
return True return True
@@ -184,8 +198,11 @@ class NWIndex:
"""Save the current index as a json file in the project meta """Save the current index as a json file in the project meta
data folder. data folder.
""" """
indexFile = self._project.storage.getMetaFile(nwFiles.INDEX_FILE)
if not isinstance(indexFile, Path):
return False
logger.debug("Saving index file") logger.debug("Saving index file")
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
tStart = time() tStart = time()
try: try:
@@ -202,7 +219,7 @@ class NWIndex:
logException() logException()
return False return False
logger.verbose("Index saved in %.3f ms", (time() - tStart)*1000) logger.debug("Index saved in %.3f ms", (time() - tStart)*1000)
return True return True
@@ -217,11 +234,11 @@ class NWIndex:
files before we save them, in which case we already have the files before we save them, in which case we already have the
text. text.
""" """
theItem = self.theProject.tree[tHandle] theItem = self._project.tree[tHandle]
if theItem is None: if theItem is None:
logger.info("Not indexing unknown item '%s'", tHandle) logger.info("Not indexing unknown item '%s'", tHandle)
return False return False
if theItem.itemType != nwItemType.FILE: if not theItem.isFileType():
logger.info("Not indexing non-file item '%s'", tHandle) logger.info("Not indexing non-file item '%s'", tHandle)
return False return False
@@ -243,20 +260,43 @@ class NWIndex:
if theItem.itemParent is None: if theItem.itemParent is None:
logger.info("Not indexing orphaned item '%s'", tHandle) logger.info("Not indexing orphaned item '%s'", tHandle)
return False return False
if theItem.isInactive():
logger.debug("Not indexing inactive item '%s'", tHandle)
return False
logger.debug("Indexing item with handle '%s'", tHandle) 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 = 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 nTitle = 0
findHeader = True
theLines = theText.splitlines() theLines = theText.splitlines()
for nLine, aLine in enumerate(theLines, start=1): for nLine, aLine in enumerate(theLines, start=1):
if len(aLine.strip()) == 0: if len(aLine.strip()) == 0:
continue continue
if aLine.startswith("#"): if aLine.startswith("#"):
if findHeader:
hDepth, _ = self._splitHeading(aLine)
if hDepth != "H0":
theItem.setMainHeading(hDepth)
findHeader = False
isTitle = self._indexTitle(tHandle, aLine, nLine) isTitle = self._indexTitle(tHandle, aLine, nLine)
if isTitle and nLine > 0: if isTitle and nLine > 0:
if nTitle > 0: if nTitle > 0:
@@ -289,48 +329,49 @@ class NWIndex:
# Prune no longer used tags # Prune no longer used tags
for tTag, isActive in itemTags.items(): for tTag, isActive in itemTags.items():
if not isActive: if not isActive:
logger.verbose("Deleting removed tag '%s'", tTag) logger.debug("Deleting removed tag '%s'", tTag)
del self._tagsIndex[tTag] del self._tagsIndex[tTag]
# Update timestamps for index changes return
nowTime = round(time())
self._indexChange = nowTime
self._rootChange[theItem.itemRoot] = nowTime
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
## def _splitHeading(self, aLine):
# Internal Indexer Helpers """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): def _indexTitle(self, tHandle, aLine, nTitle):
"""Save information about the title and its location in the """Save information about the title and its location in the
file to the index. file to the index.
""" """
if aLine.startswith("# "): hDepth, hText = self._splitHeading(aLine)
hDepth = "H1" if hDepth == "H0":
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:
return False return False
sTitle = f"T{nTitle:06d}" sTitle = f"T{nTitle:06d}"
self._itemIndex.addItemHeading(tHandle, sTitle, hDepth, hText) self._itemIndex.addItemHeading(tHandle, sTitle, hDepth, hText)
return True return True
def _indexWordCounts(self, tHandle, theText, nTitle): def _indexWordCounts(self, tHandle, theText, nTitle):
@@ -493,18 +534,15 @@ class NWIndex:
for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle) for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle)
] ]
def getHandleHeaderLevel(self, tHandle): def getTableOfContents(self, rootHandle, maxDepth, skipExcl=True):
"""Get the header level of the first header of a handle.
"""
return self._itemIndex.mainItemHeader(tHandle)
def getTableOfContents(self, maxDepth, skipExcl=True):
"""Generate a table of contents up to a maximum depth. """Generate a table of contents up to a maximum depth.
""" """
tOrder = [] tOrder = []
tData = {} tData = {}
pKey = None 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}" tKey = f"{tHandle}:{sTitle}"
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0) iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
if iLevel > maxDepth: if iLevel > maxDepth:
@@ -647,23 +685,17 @@ class TagsIndex:
def tagHandle(self, tagKey): def tagHandle(self, tagKey):
"""Get the handle of a given tag. """Get the handle of a given tag.
""" """
if tagKey in self._tags: return self._tags.get(tagKey, {}).get("handle", None)
return self._tags.get(tagKey).get("handle")
return None
def tagHeading(self, tagKey): def tagHeading(self, tagKey):
"""Get the heading of a given tag. """Get the heading of a given tag.
""" """
if tagKey in self._tags: return self._tags.get(tagKey, {}).get("heading", nwHeaders.TT_NONE)
return self._tags.get(tagKey).get("heading")
return nwHeaders.TT_NONE
def tagClass(self, tagKey): def tagClass(self, tagKey):
"""Get the class of a given tag. """Get the class of a given tag.
""" """
if tagKey in self._tags: return self._tags.get(tagKey, {}).get("class", None)
return self._tags.get(tagKey).get("class")
return None
## ##
# Pack/Unpack # Pack/Unpack
@@ -717,8 +749,8 @@ class ItemIndex:
IndexHeading object for each header of the text. IndexHeading object for each header of the text.
""" """
def __init__(self, theProject): def __init__(self, project):
self.theProject = theProject self._project = project
self._items = {} self._items = {}
return return
@@ -755,13 +787,6 @@ class ItemIndex:
self._items[tHandle] = IndexItem(tHandle, tItem) self._items[tHandle] = IndexItem(tHandle, tItem)
return 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): def allItemTags(self, tHandle):
"""Get all tags set for headings of an item. """Get all tags set for headings of an item.
""" """
@@ -789,12 +814,12 @@ class ItemIndex:
"""Iterate over all items and headers in the novel structure for """Iterate over all items and headers in the novel structure for
a given root handle, or for all if root handle is None. a given root handle, or for all if root handle is None.
""" """
for tItem in self.theProject.tree: for tItem in self._project.tree:
if tItem is None: if tItem is None:
continue continue
if tItem.itemLayout == nwItemLayout.NOTE: if tItem.isNoteLayout():
continue continue
if skipExcl and not tItem.isExported: if skipExcl and not tItem.isActive:
continue continue
tHandle = tItem.itemHandle tHandle = tItem.itemHandle
@@ -807,8 +832,6 @@ class ItemIndex:
elif tItem.itemRoot == rootHandle: elif tItem.itemRoot == rootHandle:
for sTitle in self._items[tHandle].headings(): for sTitle in self._items[tHandle].headings():
yield tHandle, sTitle, self._items[tHandle][sTitle] yield tHandle, sTitle, self._items[tHandle][sTitle]
else:
continue
return return
@@ -821,7 +844,6 @@ class ItemIndex:
""" """
if tHandle in self._items: if tHandle in self._items:
tItem = self._items[tHandle] tItem = self._items[tHandle]
tItem.updateLevel(hDepth)
tItem.addHeading(IndexHeading(sTitle, hDepth, hText)) tItem.addHeading(IndexHeading(sTitle, hDepth, hText))
return return
@@ -875,7 +897,7 @@ class ItemIndex:
if not isHandle(tHandle): if not isHandle(tHandle):
raise ValueError("itemIndex keys must be handles") raise ValueError("itemIndex keys must be handles")
nwItem = self.theProject.tree[tHandle] nwItem = self._project.tree[tHandle]
if nwItem is not None: if nwItem is not None:
tItem = IndexItem(tHandle, nwItem) tItem = IndexItem(tHandle, nwItem)
tItem.unpackData(tData) tItem.unpackData(tData)
@@ -897,7 +919,6 @@ class IndexItem:
def __init__(self, tHandle, tItem): def __init__(self, tHandle, tItem):
self._handle = tHandle self._handle = tHandle
self._item = tItem self._item = tItem
self._level = "H0"
self._headings = {} self._headings = {}
self._index = 0 self._index = 0
@@ -917,21 +938,10 @@ class IndexItem:
def item(self): def item(self):
return self._item return self._item
@property
def level(self):
return self._level
## ##
# Setters # 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): def addHeading(self, tHeading):
"""Add a heading to the item. Also remove the placeholder entry """Add a heading to the item. Also remove the placeholder entry
if it exists. if it exists.
@@ -1011,7 +1021,7 @@ class IndexItem:
if hRefs: if hRefs:
refs[sTitle] = hRefs refs[sTitle] = hRefs
data = {"level": self._level} data = {}
data["headings"] = heads data["headings"] = heads
if refs: if refs:
data["references"] = refs data["references"] = refs
@@ -1021,7 +1031,6 @@ class IndexItem:
def unpackData(self, data): def unpackData(self, data):
"""Unpack an item entry from the data. """Unpack an item entry from the data.
""" """
self._level = data.get("level", "H0")
references = data.get("references", {}) references = data.get("references", {})
for sTitle, hData in data.get("headings", {}).items(): for sTitle, hData in data.get("headings", {}).items():
if not isTitleTag(sTitle): if not isTitleTag(sTitle):
@@ -1030,6 +1039,7 @@ class IndexItem:
tHeading.unpackData(hData) tHeading.unpackData(hData)
tHeading.unpackReferences(references.get(sTitle, {})) tHeading.unpackReferences(references.get(sTitle, {}))
self.addHeading(tHeading) self.addHeading(tHeading)
return return
# END Class IndexItem # END Class IndexItem
+161 -151
View File
@@ -25,23 +25,27 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging import logging
from lxml import etree
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.common import ( from novelwriter.common import (
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified, yesNo
) )
from novelwriter.constants import nwLabels, trConst from novelwriter.constants import nwHeaders, nwLabels, trConst
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class NWItem(): class NWItem:
def __init__(self, theProject): __slots__ = (
"_project", "_name", "_handle", "_parent", "_root", "_order",
"_type", "_class", "_layout", "_status", "_import", "_active",
"_expanded", "_heading", "_charCount", "_wordCount",
"_paraCount", "_cursorPos", "_initCount",
)
self.theProject = theProject def __init__(self, project):
self._project = project
self._name = "" self._name = ""
self._handle = None self._handle = None
self._parent = None self._parent = None
@@ -52,15 +56,16 @@ class NWItem():
self._layout = nwItemLayout.NO_LAYOUT self._layout = nwItemLayout.NO_LAYOUT
self._status = None self._status = None
self._import = None self._import = None
self._active = True
self._expanded = False self._expanded = False
self._exported = True
# Document Meta Data # Document Meta Data
self._charCount = 0 # Current character count self._heading = "H0" # The main heading
self._wordCount = 0 # Current word count self._charCount = 0 # Current character count
self._paraCount = 0 # Current paragraph count self._wordCount = 0 # Current word count
self._cursorPos = 0 # Last cursor position self._paraCount = 0 # Current paragraph count
self._initCount = 0 # Initial word count self._cursorPos = 0 # Last cursor position
self._initCount = 0 # Initial word count
return return
@@ -114,13 +119,17 @@ class NWItem():
def itemImport(self): def itemImport(self):
return self._import return self._import
@property
def isActive(self):
return self._active
@property @property
def isExpanded(self): def isExpanded(self):
return self._expanded return self._expanded
@property @property
def isExported(self): def mainHeading(self):
return self._exported return self._heading
@property @property
def charCount(self): def charCount(self):
@@ -143,101 +152,75 @@ class NWItem():
return self._cursorPos return self._cursorPos
## ##
# XML Pack/Unpack # Pack/Unpack Data
## ##
def packXML(self, xParent): def pack(self):
"""Pack all the data in the class instance into an XML object. """Pack all the data in the class instance into a dictionary.
""" """
itemAttrib = {} item = {}
itemAttrib["handle"] = str(self._handle) meta = {}
itemAttrib["parent"] = str(self._parent) name = {}
itemAttrib["root"] = str(self._root)
itemAttrib["order"] = str(self._order) item["handle"] = str(self._handle)
itemAttrib["type"] = str(self._type.name) item["parent"] = str(self._parent)
itemAttrib["class"] = str(self._class.name) item["root"] = str(self._root)
item["order"] = str(self._order)
item["type"] = str(self._type.name)
item["class"] = str(self._class.name)
meta["expanded"] = yesNo(self._expanded)
name["status"] = str(self._status)
name["import"] = str(self._import)
if self._type == nwItemType.FILE: if self._type == nwItemType.FILE:
itemAttrib["layout"] = str(self._layout.name) item["layout"] = str(self._layout.name)
meta["heading"] = str(self._heading)
meta["charCount"] = str(self._charCount)
meta["wordCount"] = str(self._wordCount)
meta["paraCount"] = str(self._paraCount)
meta["cursorPos"] = str(self._cursorPos)
name["active"] = yesNo(self._active)
metaAttrib = {} data = {
metaAttrib["expanded"] = str(self._expanded) "name": str(self._name),
if self._type == nwItemType.FILE: "itemAttr": item,
metaAttrib["charCount"] = str(self._charCount) "metaAttr": meta,
metaAttrib["wordCount"] = str(self._wordCount) "nameAttr": name,
metaAttrib["paraCount"] = str(self._paraCount) }
metaAttrib["cursorPos"] = str(self._cursorPos)
nameAttrib = {} return data
nameAttrib["status"] = str(self._status)
nameAttrib["import"] = str(self._import)
if self._type == nwItemType.FILE:
nameAttrib["exported"] = str(self._exported)
xPack = etree.SubElement(xParent, "item", attrib=itemAttrib) def unpack(self, data):
self._subPack(xPack, "meta", attrib=metaAttrib) """Set the values from a data dictionary.
self._subPack(xPack, "name", text=str(self._name), attrib=nameAttrib)
return
def unpackXML(self, xItem):
"""Set the values from an XML entry of type 'item'.
""" """
if xItem.tag != "item": item = data.get("itemAttr", {})
logger.error("XML entry is not an NWItem") meta = data.get("metaAttr", {})
return False name = data.get("nameAttr", {})
if "handle" in xItem.attrib: if "handle" in item:
self.setHandle(xItem.attrib["handle"]) self.setHandle(item["handle"])
else: else:
logger.error("XML item entry does not have a handle") logger.error("Item does not have a handle")
return False return False
self.setParent(xItem.attrib.get("parent", None)) self.setName(data.get("name", ""))
self.setRoot(xItem.attrib.get("root", None)) self.setParent(item.get("parent", None))
self.setOrder(xItem.attrib.get("order", 0)) self.setRoot(item.get("root", None))
self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE)) self.setOrder(item.get("order", 0))
self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS)) self.setType(item.get("type", nwItemType.NO_TYPE))
self.setLayout(xItem.attrib.get("layout", nwItemLayout.NO_LAYOUT)) self.setClass(item.get("class", nwItemClass.NO_CLASS))
self.setExpanded(meta.get("expanded", False))
self.setStatus(name.get("status", None))
self.setImport(name.get("import", None))
for xValue in xItem: if self._type == nwItemType.FILE:
if xValue.tag == "meta": self.setLayout(item.get("layout", nwItemLayout.NO_LAYOUT))
self.setExpanded(xValue.attrib.get("expanded", False)) self.setMainHeading(meta.get("heading", "H0"))
self.setCharCount(xValue.attrib.get("charCount", 0)) self.setCharCount(meta.get("charCount", 0))
self.setWordCount(xValue.attrib.get("wordCount", 0)) self.setWordCount(meta.get("wordCount", 0))
self.setParaCount(xValue.attrib.get("paraCount", 0)) self.setParaCount(meta.get("paraCount", 0))
self.setCursorPos(xValue.attrib.get("cursorPos", 0)) self.setCursorPos(meta.get("cursorPos", 0))
elif xValue.tag == "name": self.setActive(name.get("active", True))
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))
# Legacy Format (1.3 and earlier)
elif xValue.tag == "status":
self.setImportStatus(xValue.text)
elif xValue.tag == "type":
self.setType(xValue.text)
elif xValue.tag == "class":
self.setClass(xValue.text)
elif xValue.tag == "layout":
self.setLayout(xValue.text)
elif xValue.tag == "expanded":
self.setExpanded(xValue.text)
elif xValue.tag == "exported":
self.setExported(xValue.text)
elif xValue.tag == "charCount":
self.setCharCount(xValue.text)
elif xValue.tag == "wordCount":
self.setWordCount(xValue.text)
elif xValue.tag == "paraCount":
self.setParaCount(xValue.text)
elif xValue.tag == "cursorPos":
self.setCursorPos(xValue.text)
else:
# Sliently skip as we may otherwise cause orphaned
# items if an otherwise valid file is opened by a
# version of novelWriter that doesn't know the tag
logger.error("Unknown tag '%s'", xValue.tag)
# Make some checks to ensure consistency # Make some checks to ensure consistency
if self._type == nwItemType.ROOT: if self._type == nwItemType.ROOT:
@@ -245,31 +228,22 @@ class NWItem():
self._parent = None # Root items cannot have a parent self._parent = None # Root items cannot have a parent
if self._type != nwItemType.FILE: if self._type != nwItemType.FILE:
self._charCount = 0 # Only set for files # Reset values that should only be set for files
self._wordCount = 0 # Only set for files self._layout = nwItemLayout.NO_LAYOUT
self._paraCount = 0 # Only set for files self._heading = "H0"
self._cursorPos = 0 # Only set for files self._active = False
self._charCount = 0
self._wordCount = 0
self._paraCount = 0
self._cursorPos = 0
return True return True
@staticmethod
def _subPack(xParent, name, attrib=None, text=None, none=True):
"""Pack the values into an XML element.
"""
if not none and (text is None or text == "None"):
return None
xAttr = {} if attrib is None else attrib
xSub = etree.SubElement(xParent, name, attrib=xAttr)
if text is not None:
xSub.text = text
return
## ##
# Lookup Methods # Lookup Methods
## ##
def describeMe(self, hLevel=None): def describeMe(self):
"""Return a string description of the item. """Return a string description of the item.
""" """
descKey = "none" descKey = "none"
@@ -279,12 +253,14 @@ class NWItem():
descKey = "folder" descKey = "folder"
elif self._type == nwItemType.FILE: elif self._type == nwItemType.FILE:
if self._layout == nwItemLayout.DOCUMENT: if self._layout == nwItemLayout.DOCUMENT:
if hLevel == "H1": if self._heading == "H1":
descKey = "doc_h1" descKey = "doc_h1"
elif hLevel == "H2": elif self._heading == "H2":
descKey = "doc_h2" descKey = "doc_h2"
elif hLevel == "H3": elif self._heading == "H3":
descKey = "doc_h3" descKey = "doc_h3"
elif self._heading == "H4":
descKey = "doc_h4"
else: else:
descKey = "document" descKey = "document"
elif self._layout == nwItemLayout.NOTE: elif self._layout == nwItemLayout.NOTE:
@@ -292,6 +268,22 @@ class NWItem():
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, "")) return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
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._project.data.itemStatus.name(self._status)
stIcon = self._project.data.itemStatus.icon(self._status) if incIcon else None
else:
stName = self._project.data.itemImport.name(self._import)
stIcon = self._project.data.itemImport.icon(self._import) if incIcon else None
return stName, stIcon
##
# Checker Methods
##
def isNovelLike(self): def isNovelLike(self):
"""Returns true if the item is of a novel-like class. """Returns true if the item is of a novel-like class.
""" """
@@ -307,17 +299,20 @@ class NWItem():
""" """
return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH) return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH)
def getImportStatus(self): def isRootType(self):
"""Return the relevant importance or status label and icon for return self._type == nwItemType.ROOT
the current item based on its class.
""" def isFolderType(self):
if self.isNovelLike(): return self._type == nwItemType.FOLDER
stName = self.theProject.statusItems.name(self._status)
stIcon = self.theProject.statusItems.icon(self._status) def isFileType(self):
else: return self._type == nwItemType.FILE
stName = self.theProject.importItems.name(self._import)
stIcon = self.theProject.importItems.icon(self._import) def isNoteLayout(self):
return stName, stIcon return self._layout == nwItemLayout.NOTE
def isDocumentLayout(self):
return self._layout == nwItemLayout.DOCUMENT
## ##
# Special Setters # Special Setters
@@ -419,8 +414,6 @@ class NWItem():
self._type = value self._type = value
elif isItemType(value): elif isItemType(value):
self._type = nwItemType[value] self._type = nwItemType[value]
elif value == "TRASH":
self._type = nwItemType.ROOT
else: else:
logger.error("Unrecognised item type '%s'", value) logger.error("Unrecognised item type '%s'", value)
self._type = nwItemType.NO_TYPE self._type = nwItemType.NO_TYPE
@@ -447,8 +440,6 @@ class NWItem():
self._layout = value self._layout = value
elif isItemLayout(value): elif isItemLayout(value):
self._layout = nwItemLayout[value] self._layout = nwItemLayout[value]
elif value in ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"):
self._layout = nwItemLayout.DOCUMENT
else: else:
logger.error("Unrecognised item layout '%s'", value) logger.error("Unrecognised item layout '%s'", value)
self._layout = nwItemLayout.NO_LAYOUT self._layout = nwItemLayout.NO_LAYOUT
@@ -458,60 +449,79 @@ class NWItem():
"""Set the item status by looking it up in the valid status """Set the item status by looking it up in the valid status
items of the current project. items of the current project.
""" """
self._status = self.theProject.statusItems.check(value) self._status = self._project.data.itemStatus.check(value)
return return
def setImport(self, value): def setImport(self, value):
"""Set the item importance by looking it up in the valid import """Set the item importance by looking it up in the valid import
items of the current project. items of the current project.
""" """
self._import = self.theProject.importItems.check(value) self._import = self._project.data.itemImport.check(value)
return
def setActive(self, state):
"""Set the active flag.
"""
if isinstance(state, bool):
self._active = state
else:
self._active = False
return return
def setExpanded(self, state): def setExpanded(self, state):
"""Set the expanded status of an item in the project tree. """Set the expanded status of an item in the project tree.
""" """
if isinstance(state, str): if isinstance(state, bool):
self._expanded = (state == str(True)) self._expanded = state
else: else:
self._expanded = (state is True) self._expanded = False
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 return
## ##
# Set Document Meta Data # 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): def setCharCount(self, count):
"""Set the character count, and ensure that it is an integer. """Set the character count, and ensure that it is an integer.
""" """
self._charCount = max(0, checkInt(count, 0)) if isinstance(count, int):
self._charCount = max(0, count)
else:
self._charCount = 0
return return
def setWordCount(self, count): def setWordCount(self, count):
"""Set the word count, and ensure that it is an integer. """Set the word count, and ensure that it is an integer.
""" """
self._wordCount = max(0, checkInt(count, 0)) if isinstance(count, int):
self._wordCount = max(0, count)
else:
self._wordCount = 0
return return
def setParaCount(self, count): def setParaCount(self, count):
"""Set the paragraph count, and ensure that it is an integer. """Set the paragraph count, and ensure that it is an integer.
""" """
self._paraCount = max(0, checkInt(count, 0)) if isinstance(count, int):
self._paraCount = max(0, count)
else:
self._paraCount = 0
return return
def setCursorPos(self, position): def setCursorPos(self, position):
"""Set the cursor position, and ensure that it is an integer. """Set the cursor position, and ensure that it is an integer.
""" """
self._cursorPos = max(0, checkInt(position, 0)) if isinstance(position, int):
self._cursorPos = max(0, position)
else:
self._cursorPos = 0
return return
def saveInitialCount(self): def saveInitialCount(self):
+13 -16
View File
@@ -24,11 +24,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import json import json
import logging import logging
from enum import Enum from enum import Enum
from pathlib import Path
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import checkBool, checkFloat, checkInt, checkString from novelwriter.common import checkBool, checkFloat, checkInt, checkString
@@ -40,30 +40,30 @@ VALID_MAP = {
"GuiWritingStats": { "GuiWritingStats": {
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2", "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
"widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes", "widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes",
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax" "hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax",
}, },
"GuiDocSplit": {"spLevel"}, "GuiDocSplit": {"spLevel", "intoFolder", "docHierarchy"},
"GuiBuildNovel": { "GuiBuildNovel": {
"winWidth", "winHeight", "boxWidth", "docWidth", "hideScene", "winWidth", "winHeight", "boxWidth", "docWidth", "hideScene",
"hideSection", "addNovel", "addNotes", "ignoreFlag", "justifyText", "hideSection", "addNovel", "addNotes", "ignoreFlag", "justifyText",
"excludeBody", "textFont", "textSize", "lineHeight", "noStyling", "excludeBody", "textFont", "textSize", "lineHeight", "noStyling",
"incSynopsis", "incComments", "incKeywords", "incBodyText", "incSynopsis", "incComments", "incKeywords", "incBodyText",
"replaceTabs", "replaceUCode" "replaceTabs", "replaceUCode", "rootFilter",
}, },
"GuiOutline": {"headerOrder", "columnWidth", "columnHidden"}, "GuiOutline": {"headerOrder", "columnWidth", "columnHidden"},
"GuiProjectSettings": { "GuiProjectSettings": {
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW" "winWidth", "winHeight", "replaceColW", "statusColW", "importColW",
}, },
"GuiProjectDetails": { "GuiProjectDetails": {
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2", "winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble" "widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
}, },
"GuiWordList": {"winWidth", "winHeight"}, "GuiWordList": {"winWidth", "winHeight"},
"GuiNovelView": {"lastCol"}, "GuiNovelView": {"lastCol"},
} }
class OptionState(): class OptionState:
def __init__(self, theProject): def __init__(self, theProject):
self.theProject = theProject self.theProject = theProject
@@ -77,13 +77,12 @@ class OptionState():
def loadSettings(self): def loadSettings(self):
"""Load the options dictionary from the project settings file. """Load the options dictionary from the project settings file.
""" """
if self.theProject.projMeta is None: stateFile = self.theProject.storage.getMetaFile(nwFiles.OPTS_FILE)
if not isinstance(stateFile, Path):
return False return False
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
theState = {} theState = {}
if stateFile.exists():
if os.path.isfile(stateFile):
logger.debug("Loading GUI options file") logger.debug("Loading GUI options file")
try: try:
with open(stateFile, mode="r", encoding="utf-8") as inFile: with open(stateFile, mode="r", encoding="utf-8") as inFile:
@@ -106,12 +105,11 @@ class OptionState():
def saveSettings(self): def saveSettings(self):
"""Save the options dictionary to the project settings file. """Save the options dictionary to the project settings file.
""" """
if self.theProject.projMeta is None: stateFile = self.theProject.storage.getMetaFile(nwFiles.OPTS_FILE)
if not isinstance(stateFile, Path):
return False return False
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
logger.debug("Saving GUI options file") logger.debug("Saving GUI options file")
try: try:
with open(stateFile, mode="w+", encoding="utf-8") as outFile: with open(stateFile, mode="w+", encoding="utf-8") as outFile:
json.dump(self._theState, outFile, indent=2) json.dump(self._theState, outFile, indent=2)
@@ -188,8 +186,7 @@ class OptionState():
the default value. the default value.
""" """
if group in self._theState: 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 return default
def getEnum(self, group, name, lookup, default): def getEnum(self, group, name, lookup, default):
+288 -1075
View File
File diff suppressed because it is too large Load Diff
+354
View File
@@ -0,0 +1,354 @@
"""
novelWriter Project Data Class
================================
Data class for novelWriter projects
File History:
Created: 2022-10-30 [2.0rc1]
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/>.
"""
from __future__ import annotations
import uuid
import logging
from novelwriter.common import (
checkBool, checkInt, checkStringNone, checkUuid, isHandle, simplified
)
from novelwriter.core.status import NWStatus
logger = logging.getLogger(__name__)
class NWProjectData:
def __init__(self, theProject):
self.theProject = theProject
# Project Meta
self._uuid = ""
self._name = ""
self._title = ""
self._authors = []
self._saveCount = 0
self._autoCount = 0
self._editTime = 0
# Project Settings
self._doBackup = True
self._language = None
self._spellCheck = False
self._spellLang = None
# Project Dictionaries
self._initCounts = [0, 0]
self._currCounts = [0, 0]
self._lastHandle: dict[str, str | None] = {
"editor": None,
"viewer": None,
"novelTree": None,
"outline": None,
}
self._autoReplace: dict[str, str] = {}
self._titleFormat: dict[str, str] = {
"title": "%title%",
"chapter": "%title%",
"unnumbered": "%title%",
"scene": "* * *",
"section": "",
}
self._status = NWStatus(NWStatus.STATUS)
self._import = NWStatus(NWStatus.IMPORT)
return
##
# Properties
##
@property
def uuid(self):
return self._uuid
@property
def name(self):
return self._name
@property
def title(self):
return self._title
@property
def authors(self):
return self._authors
@property
def saveCount(self):
return self._saveCount
@property
def autoCount(self):
return self._autoCount
@property
def editTime(self):
return self._editTime
@property
def doBackup(self):
return self._doBackup
@property
def language(self):
return self._language
@property
def spellCheck(self):
return self._spellCheck
@property
def spellLang(self):
return self._spellLang
@property
def initCounts(self):
return tuple(self._initCounts)
@property
def currCounts(self):
return tuple(self._currCounts)
@property
def lastHandle(self):
return self._lastHandle
@property
def autoReplace(self):
return self._autoReplace
@property
def titleFormat(self):
return self._titleFormat
@property
def itemStatus(self):
return self._status
@property
def itemImport(self):
return self._import
##
# Methods
##
def addAuthor(self, value):
"""Add an author to the authors list.
"""
self._authors.append(simplified(str(value)))
self.theProject.setProjectChanged(True)
return
def incSaveCount(self):
"""Increment the save count by one.
"""
self._saveCount += 1
self.theProject.setProjectChanged(True)
return
def incAutoCount(self):
"""Increment the auto save count by one.
"""
self._autoCount += 1
self.theProject.setProjectChanged(True)
return
##
# Getters
##
def getLastHandle(self, component):
"""Retrieve the last used handle for a given component.
"""
return self._lastHandle.get(component, None)
def getTitleFormat(self, kind):
"""Retrieve the title format string for a given kind of header.
"""
return self._titleFormat.get(kind, "%title%")
##
# Setters
##
def setUuid(self, value):
"""Set the project id.
"""
value = checkUuid(value, "")
if not value:
self._uuid = str(uuid.uuid4())
elif value != self._uuid:
self._uuid = value
self.theProject.setProjectChanged(True)
return
def setName(self, value):
"""Set a new project name.
"""
if value != self._name:
self._name = simplified(str(value))
self.theProject.setProjectChanged(True)
return
def setTitle(self, value):
"""Set a new novel title.
"""
if value != self._title:
self._title = simplified(str(value))
self.theProject.setProjectChanged(True)
return
def setAuthors(self, value):
"""Set the list of authors from either a string with one author
per line, or a list of authors.
"""
self._authors = []
self.theProject.setProjectChanged(True)
if isinstance(value, str):
for author in value.splitlines():
author = simplified(author)
if author:
self._authors.append(author)
self.theProject.setProjectChanged(True)
elif isinstance(value, list):
self._authors = value
return
def setSaveCount(self, value):
"""Set the save count from last session.
"""
self._saveCount = checkInt(value, 0)
self.theProject.setProjectChanged(True)
return
def setAutoCount(self, value):
"""Set the auto save count from last session.
"""
self._autoCount = checkInt(value, 0)
self.theProject.setProjectChanged(True)
return
def setEditTime(self, value):
"""Set tyje edit time from last session.
"""
self._editTime = checkInt(value, 0)
self.theProject.setProjectChanged(True)
return
def setDoBackup(self, value):
"""Set the do write backup flag.
"""
if value != self._doBackup:
self._doBackup = checkBool(value, False)
self.theProject.setProjectChanged(True)
return
def setLanguage(self, value):
"""Set the project language.
"""
if value != self._language:
self._language = checkStringNone(value, None)
self.theProject.setProjectChanged(True)
return
def setSpellCheck(self, value):
"""Set the spell check flag.
"""
if value != self._spellCheck:
self._spellCheck = checkBool(value, False)
self.theProject.setProjectChanged(True)
return
def setSpellLang(self, value):
"""Set the spell check language.
"""
if value != self._spellLang:
self._spellLang = checkStringNone(value, None)
self.theProject.setProjectChanged(True)
return
def setLastHandle(self, value, component=None):
"""Set a last used handle into the handle registry. If component
is None, the value is assumed to be the whole dictionary of
values.
"""
if isinstance(component, str):
self._lastHandle[component] = checkStringNone(value, None)
self.theProject.setProjectChanged(True)
elif isinstance(value, dict):
for key, entry in value.items():
if key in self._lastHandle:
self._lastHandle[key] = str(entry) if isHandle(entry) else None
self.theProject.setProjectChanged(True)
return
def setInitCounts(self, novel=None, notes=None):
"""Set the worc count totals for novel and note files.
"""
if novel is not None:
self._initCounts[0] = checkInt(novel, 0)
self._currCounts[0] = checkInt(novel, 0)
if notes is not None:
self._initCounts[1] = checkInt(notes, 0)
self._currCounts[1] = checkInt(notes, 0)
return
def setCurrCounts(self, novel=None, notes=None):
"""Set the worc count totals for novel and note files.
"""
if novel is not None:
self._currCounts[0] = checkInt(novel, 0)
if notes is not None:
self._currCounts[1] = checkInt(notes, 0)
return
def setAutoReplace(self, value):
"""Set the auto-replace dictionary.
"""
if isinstance(value, dict):
self._autoReplace = {}
for key, entry in value.items():
if isinstance(entry, str):
self._autoReplace[key] = simplified(entry)
self.theProject.setProjectChanged(True)
return
def setTitleFormat(self, value):
"""Set the title formats.
"""
if isinstance(value, dict):
for key, entry in value.items():
if key in self._titleFormat and isinstance(entry, str):
self._titleFormat[key] = simplified(entry)
self.theProject.setProjectChanged(True)
return
# END Class NWProjectData
+611
View File
@@ -0,0 +1,611 @@
"""
novelWriter Project XML Read/Write
====================================
Classes for reading and writing the project XML file
File History:
Created: 2022-09-28 [2.0rc1] ProjectXMLReader
Created: 2022-09-28 [2.0rc1] XMLReadState
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
import novelwriter
from enum import Enum
from lxml import etree
from time import time
from pathlib import Path
from novelwriter.common import (
checkBool, checkInt, checkString, checkStringNone, formatTimeStamp,
hexToInt, simplified, yesNo
)
from novelwriter.constants import nwFiles
logger = logging.getLogger(__name__)
FILE_VERSION = "1.5" # The current project file format version
HEX_VERSION = 0x0105
NUM_VERSION = {
"1.0": 0x0100, # Up to 0.7
"1.1": 0x0101, # Up to 0.10
"1.2": 0x0102, # Up to 1.5
"1.3": 0x0103, # Up to 2.0 Beta 1
"1.4": 0x0104, # Up to 2.0 RC 2
"1.5": 0x0105, # Current
}
class XMLReadState(Enum):
NO_ACTION = 0
NO_ERROR = 1
PARSED_BACKUP = 2
CANNOT_PARSE = 3
NOT_NWX_FILE = 4
UNKNOWN_VERSION = 5
PARSED_OK = 6
WAS_LEGACY = 7
# END Class XMLReadState
class ProjectXMLReader:
"""The main project XML file reader class. All data is read into a
NWProjectData instance, which must be provided.
File Format Version Change History
==================================
1.0 Original file format.
1.1 Changes the way documents are structured in the project folder
from data_X, where X is the first hex value of the handle, to a
single content folder. Introduced in version 0.7.
1.2 Changes the way autoReplace entries are stored. Introduced in
version 0.10.
1.3 Reduces the number of layouts to only two. One for novel
documents and one for project notes. Introduced in version 1.5.
1.4 Introduces a more compact format for storing items. All settings
aside from name are now attributes. This format also changes the
way satus and importance labels are stored. This format was only
a part of version 2.0 RC 1
1.5 The actual format released for 2.0. It moves last used handles
and title formats into a key/value format similar to auto-
replace, status and imporetance. It adds the heading value to
the content item meta entry. It also moves meta data related to
the project or the content into their respective section nodes
as attributes. The id attribute was also added to the project.
"""
def __init__(self, path):
self._path = Path(path)
self._state = XMLReadState.NO_ACTION
self._root = ""
self._version = 0x0
self._appVersion = ""
self._hexVersion = 0x0
self._timeStamp = ""
return
##
# Properties
##
@property
def state(self):
"""The state of the parsing as an XMLReadState enum value.
"""
return self._state
@property
def xmlRoot(self):
"""The root tag name of the XNL file,
"""
return self._root
@property
def xmlVersion(self):
"""The project XML version number.
"""
return self._version
@property
def appVersion(self):
"""The novelWriter version number who wrote the file.
"""
return self._appVersion
@property
def hexVersion(self):
"""The novelWriter version number who wrote the file as hex.
"""
return self._hexVersion
@property
def timeStamp(self):
"""The date and time when the file was written.
"""
return self._timeStamp
##
# Methods
##
def read(self, projData, projContent):
"""Read and parse the project XML file.
"""
tStart = time()
logger.debug("Reading project XML")
try:
xml = etree.parse(str(self._path))
self._state = XMLReadState.NO_ERROR
except Exception as exc:
# Trying to open backup file instead
logger.error("Failed to parse project XML", exc_info=exc)
self._state = XMLReadState.CANNOT_PARSE
backFile = self._path.with_suffix(".bak")
if backFile.is_file():
try:
xml = etree.parse(str(backFile))
self._state = XMLReadState.PARSED_BACKUP
logger.info("Backup project file parsed")
except Exception as exc:
logger.error("Failed to parse backup project XML", exc_info=exc)
self._state = XMLReadState.CANNOT_PARSE
return False
else:
self._state = XMLReadState.CANNOT_PARSE
return False
xRoot = xml.getroot()
self._root = str(xRoot.tag)
if self._root != "novelWriterXML":
self._state = XMLReadState.NOT_NWX_FILE
return False
fileVersion = str(xRoot.attrib.get("fileVersion", ""))
if fileVersion in NUM_VERSION:
self._version = NUM_VERSION[fileVersion]
else:
self._state = XMLReadState.UNKNOWN_VERSION
return False
logger.debug("XML is '%s' version '%s'", self._root, fileVersion)
self._appVersion = str(xRoot.attrib.get("appVersion", ""))
self._hexVersion = hexToInt(xRoot.attrib.get("hexVersion", ""))
self._timeStamp = str(xRoot.attrib.get("timeStamp", ""))
for xSection in xRoot:
if xSection.tag == "project":
self._parseProjectMeta(xSection, projData)
elif xSection.tag == "settings":
self._parseProjectSettings(xSection, projData)
elif xSection.tag == "content":
if self._version >= 0x0104:
self._parseProjectContent(xSection, projData, projContent)
else:
self._parseProjectContentLegacy(xSection, projData, projContent)
else:
logger.warning("Ignored <root/%s> in XML", xSection.tag)
if self._version == HEX_VERSION:
self._state = XMLReadState.PARSED_OK
else:
self._state = XMLReadState.WAS_LEGACY
logger.debug("Project XML loaded in %.3f ms", (time() - tStart)*1000)
return True
##
# Internal Functions
##
def _parseProjectMeta(self, xSection, projData):
"""Parse the project section of the XML file.
"""
logger.debug("Parsing <project> section")
projData.setUuid(xSection.attrib.get("id", None)) # Added in 1.5
projData.setSaveCount(xSection.attrib.get("saveCount", 0)) # Moved in 1.5
projData.setAutoCount(xSection.attrib.get("autoCount", 0)) # Moved in 1.5
projData.setEditTime(xSection.attrib.get("editTime", 0)) # Moved in 1.5
for xItem in xSection:
if xItem.tag == "name":
projData.setName(xItem.text)
elif xItem.tag == "title":
projData.setTitle(xItem.text)
elif xItem.tag == "author":
projData.addAuthor(xItem.text)
else:
logger.warning("Ignored <root/project/%s> in XML", xItem.tag)
# Deprecated Nodes
if self._version < HEX_VERSION:
for xItem in xSection:
if xItem.tag == "saveCount": # Moved to attribute in 1.5
projData.setSaveCount(xItem.text)
elif xItem.tag == "autoCount": # Moved to attribute in 1.5
projData.setAutoCount(xItem.text)
elif xItem.tag == "editTime": # Moved to attribute in 1.5
projData.setEditTime(xItem.text)
return
def _parseProjectSettings(self, xSection, projData):
"""Parse the settings section of the XML file.
"""
logger.debug("Parsing <settings> section")
for xItem in xSection:
if xItem.tag == "doBackup":
projData.setDoBackup(xItem.text)
elif xItem.tag == "language":
projData.setLanguage(xItem.text)
elif xItem.tag == "spellChecking":
projData.setSpellLang(xItem.text)
projData.setSpellCheck(xItem.attrib.get("auto", False))
elif xItem.tag == "status":
self._parseStatusImport(xItem, projData.itemStatus)
elif xItem.tag == "importance":
self._parseStatusImport(xItem, projData.itemImport)
elif xItem.tag == "lastHandle":
projData.setLastHandle(self._parseDictKeyText(xItem))
elif xItem.tag == "autoReplace":
if self._version >= 0x0102:
projData.setAutoReplace(self._parseDictKeyText(xItem))
else: # Pre 1.2 format
projData.setAutoReplace(self._parseDictTagText(xItem))
elif xItem.tag == "titleFormat":
if self._version >= 0x0105:
projData.setTitleFormat(self._parseDictKeyText(xItem))
else: # Pre 1.4 format
projData.setTitleFormat(self._parseDictTagText(xItem))
else:
logger.warning("Ignored <root/settings/%s> in XML", xItem.tag)
# Deprecated Nodes
if self._version < HEX_VERSION:
for xItem in xSection:
if xItem.tag == "spellCheck": # Changed to spellChecking in 1.5
projData.setSpellCheck(xItem.text)
elif xItem.tag == "spellLang": # Changed to spellChecking in 1.5
projData.setSpellLang(xItem.text)
elif xItem.tag == "novelWordCount": # Moved to content attribute in 1.5
projData.setInitCounts(novel=xItem.text)
elif xItem.tag == "notesWordCount": # Moved to content attribute in 1.5
projData.setInitCounts(notes=xItem.text)
return
def _parseProjectContent(self, xSection, projData, projContent):
"""Parse the content section of the XML file.
"""
logger.debug("Parsing <content> section")
projData.setInitCounts(novel=xSection.attrib.get("novelWords", None)) # Moved in 1.5
projData.setInitCounts(notes=xSection.attrib.get("notesWords", None)) # Moved in 1.5
for xItem in xSection:
if xItem.tag != "item":
logger.warning("Ignored item <root/content/%s> in XML", xItem.tag)
continue
item = {}
meta = {}
name = {}
itemName = ""
item["handle"] = checkStringNone(xItem.attrib.get("handle"), None)
item["parent"] = checkStringNone(xItem.attrib.get("parent"), None)
item["root"] = checkStringNone(xItem.attrib.get("root"), None)
item["order"] = checkInt(xItem.attrib.get("order"), 0)
item["type"] = checkString(xItem.attrib.get("type"), "NO_TYPE")
item["class"] = checkString(xItem.attrib.get("class"), "NO_CLASS")
item["layout"] = checkString(xItem.attrib.get("layout"), "NO_LAYOUT")
for xVal in xItem:
if xVal.tag == "meta":
meta["expanded"] = checkBool(xVal.attrib.get("expanded"), False)
meta["heading"] = checkString(xVal.attrib.get("heading"), "H0")
meta["charCount"] = checkInt(xVal.attrib.get("charCount"), 0)
meta["wordCount"] = checkInt(xVal.attrib.get("wordCount"), 0)
meta["paraCount"] = checkInt(xVal.attrib.get("paraCount"), 0)
meta["cursorPos"] = checkInt(xVal.attrib.get("cursorPos"), 0)
elif xVal.tag == "name":
itemName = simplified(checkString(xVal.text, ""))
name["status"] = checkStringNone(xVal.attrib.get("status"), None)
name["import"] = checkStringNone(xVal.attrib.get("import"), None)
name["active"] = checkBool(xVal.attrib.get("active"), False)
else:
logger.warning("Ignored <root/content/item/%s> in XML", xVal.tag)
# Deprecated Nodes
if self._version < HEX_VERSION:
for xVal in xItem:
if xVal.tag == "name" and "exported" in xVal.attrib:
name["active"] = checkBool(xVal.attrib.get("exported"), False)
projContent.append({
"name": itemName,
"itemAttr": item,
"metaAttr": meta,
"nameAttr": name,
})
return
def _parseProjectContentLegacy(self, xSection, projData, projContent):
"""Parse the content section of the XML file for older versions.
"""
logger.debug("Parsing <content> section (legacy format)")
# Create maps to look up name -> key for status and importance
statusMap = {entry.get("name"): key for key, entry in projData.itemStatus.items()}
importMap = {entry.get("name"): key for key, entry in projData.itemImport.items()}
for xItem in xSection:
if xItem.tag != "item":
logger.warning("Ignored item <root/content/%s> in XML", xItem.tag)
continue
item = {}
meta = {}
name = {}
itemName = ""
item["handle"] = checkStringNone(xItem.attrib.get("handle", None), None)
item["parent"] = checkStringNone(xItem.attrib.get("parent", None), None)
item["root"] = None # Value was added in 1.4
item["order"] = checkInt(xItem.attrib.get("order", 0), 0)
meta["heading"] = "H0" # Value was added in 1.4
tmpStatus = ""
for xVal in xItem:
if xVal.tag == "name":
itemName = simplified(checkString(xVal.text, ""))
elif xVal.tag == "status":
tmpStatus = checkStringNone(xVal.text, None)
elif xVal.tag == "type":
item["type"] = checkString(xVal.text, "")
elif xVal.tag == "class":
item["class"] = checkString(xVal.text, "")
elif xVal.tag == "layout":
item["layout"] = checkString(xVal.text, "")
elif xVal.tag == "expanded":
meta["expanded"] = checkBool(xVal.text, False)
elif xVal.tag == "exported": # Renamed to active in 1.5
name["active"] = checkBool(xVal.text, False)
elif xVal.tag == "charCount":
meta["charCount"] = checkInt(xVal.text, 0)
elif xVal.tag == "wordCount":
meta["wordCount"] = checkInt(xVal.text, 0)
elif xVal.tag == "paraCount":
meta["paraCount"] = checkInt(xVal.text, 0)
elif xVal.tag == "cursorPos":
meta["cursorPos"] = checkInt(xVal.text, 0)
else:
logger.warning("Ignored <root/content/item/%s> in XML", xVal.tag)
# Status was split into separate status/import with a key in 1.4
if item.get("class", "") in ("NOVEL", "ARCHIVE"):
name["status"] = statusMap.get(tmpStatus, None)
else:
name["import"] = importMap.get(tmpStatus, None)
# A number of layouts were removed in 1.3
if item.get("layout", "") in (
"TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"
):
item["layout"] = "DOCUMENT"
# The trash type was removed in 1.4
if item.get("type", "") == "TRASH":
item["type"] = "ROOT"
projContent.append({
"name": itemName,
"itemAttr": item,
"metaAttr": meta,
"nameAttr": name,
})
return
def _parseStatusImport(self, xItem, sObject):
"""Parse a status or importance entry.
"""
for xEntry in xItem:
if xEntry.tag == "entry":
key = xEntry.attrib.get("key", None)
red = checkInt(xEntry.attrib.get("red", 0), 0)
green = checkInt(xEntry.attrib.get("green", 0), 0)
blue = checkInt(xEntry.attrib.get("blue", 0), 0)
count = checkInt(xEntry.attrib.get("count", 0), 0)
sObject.write(key, xEntry.text, (red, green, blue), count)
return
def _parseDictKeyText(self, xItem):
"""Parse a dictionary stored with key as an attribute and the
value as the text porperty.
"""
result = {}
for xEntry in xItem:
if xEntry.tag == "entry" and "key" in xEntry.attrib:
result[xEntry.attrib["key"]] = checkString(xEntry.text, "")
return result
def _parseDictTagText(self, xItem):
"""Parse a dictionary stored with key as the tag and the value
as the text porperty.
"""
return {xNode.tag: checkString(xNode.text, "") for xNode in xItem}
# END Class ProjectXMLReader
class ProjectXMLWriter:
def __init__(self, path):
self._path = Path(path)
self._error = None
return
##
# Properties
##
@property
def error(self):
return self._error
##
# Methods
##
def write(self, projData, projContent, saveTime, editTime):
"""Write the project data and content to the XML files.
"""
tStart = time()
logger.debug("Writing project XML")
xRoot = etree.Element("novelWriterXML", attrib={
"appVersion": str(novelwriter.__version__),
"hexVersion": str(novelwriter.__hexversion__),
"fileVersion": FILE_VERSION,
"timeStamp": formatTimeStamp(saveTime),
})
# Save Project Meta
projAttr = {
"id": projData.uuid,
"saveCount": str(projData.saveCount),
"autoCount": str(projData.autoCount),
"editTime": str(editTime),
}
xProject = etree.SubElement(xRoot, "project", attrib=projAttr)
self._packSingleValue(xProject, "name", projData.name)
self._packSingleValue(xProject, "title", projData.title)
self._packListValue(xProject, "author", projData.authors)
# Save Project Settings
xSettings = etree.SubElement(xRoot, "settings")
self._packSingleValue(xSettings, "doBackup", yesNo(projData.doBackup))
self._packSingleValue(xSettings, "language", projData.language)
self._packSingleValue(xSettings, "spellChecking", projData.spellLang, attrib={
"auto": yesNo(projData.spellCheck)
})
self._packDictKeyValue(xSettings, "lastHandle", projData.lastHandle)
self._packDictKeyValue(xSettings, "autoReplace", projData.autoReplace)
self._packDictKeyValue(xSettings, "titleFormat", projData.titleFormat)
# Save Status/Importance
xStatus = etree.SubElement(xSettings, "status")
for label, attrib in projData.itemStatus.pack():
self._packSingleValue(xStatus, "entry", label, attrib=attrib)
xImport = etree.SubElement(xSettings, "importance")
for label, attrib in projData.itemImport.pack():
self._packSingleValue(xImport, "entry", label, attrib=attrib)
# Save Tree Content
contAttr = {
"items": str(len(projContent)),
"novelWords": str(projData.currCounts[0]),
"notesWords": str(projData.currCounts[1]),
}
xContent = etree.SubElement(xRoot, "content", attrib=contAttr)
for item in projContent:
xItem = etree.SubElement(xContent, "item", attrib=item.get("itemAttr", {}))
etree.SubElement(xItem, "meta", attrib=item.get("metaAttr", {}))
xName = etree.SubElement(xItem, "name", attrib=item.get("nameAttr", {}))
xName.text = item["name"]
# Write the XML tree to file
saveFile = self._path / nwFiles.PROJ_FILE
tempFile = saveFile.with_suffix(".tmp")
backFile = saveFile.with_suffix(".bak")
try:
tempFile.write_bytes(etree.tostring(
xRoot, pretty_print=True, encoding="utf-8", xml_declaration=True
))
except Exception as exc:
self._error = exc
return False
# If we're here, the file was successfully saved,
# so let's sort out the temps and backups
try:
if saveFile.exists():
saveFile.replace(backFile)
tempFile.replace(saveFile)
except Exception as exc:
self._error = exc
return False
logger.debug("Project XML saved in %.3f ms", (time() - tStart)*1000)
return True
##
# Internal Functions
##
def _packSingleValue(self, xParent, name, value, attrib=None):
"""Pack a single value into an XML element.
"""
xItem = etree.SubElement(xParent, name, attrib=attrib)
xItem.text = str(value) or ""
return
def _packListValue(self, xParent, name, data):
"""Pack a list of values into an XML element.
"""
for value in data:
xItem = etree.SubElement(xParent, name)
xItem.text = str(value) or ""
return
def _packDictKeyValue(self, xParent, name, data):
"""Pack the entries of a dictionary into an XML element.
"""
xItem = etree.SubElement(xParent, name)
for key, value in data.items():
if len(key) > 0:
xEntry = etree.SubElement(xItem, "entry", attrib={"key": key})
xEntry.text = str(value) or ""
return
# END Class ProjectXMLWriter
+34 -18
View File
@@ -23,15 +23,17 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import logging import logging
from collections import namedtuple
from pathlib import Path
from novelwriter.error import logException from novelwriter.error import logException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class NWSpellEnchant(): class NWSpellEnchant:
def __init__(self): def __init__(self):
@@ -46,36 +48,47 @@ class NWSpellEnchant():
return return
## ##
# Getters and Setters # Properties
## ##
@property
def spellLanguage(self): def spellLanguage(self):
return self._spellLanguage return self._spellLanguage
##
# Setters
##
def setLanguage(self, theLang, projectDict=None): def setLanguage(self, theLang, projectDict=None):
"""Load a dictionary for the language specified in the config. """Load a dictionary for the language specified in the config.
If that fails, we load a mock dictionary so that lookups don't If that fails, we load a mock dictionary so that lookups don't
crash. crash. Note that enchant will allow loading an empty string as
a tag, but this will fail later on. See issue #1096.
""" """
self._theBroker = None
self._theDict = None
self._spellLanguage = None
try: try:
import enchant import enchant
if self._theBroker is not None:
logger.debug("Deleting old pyenchant broker")
del self._theBroker
self._theBroker = enchant.Broker() if theLang and enchant.dict_exists(theLang):
self._theDict = self._theBroker.request_dict(theLang) self._theBroker = enchant.Broker()
self._spellLanguage = theLang self._theDict = self._theBroker.request_dict(theLang)
logger.debug("Enchant spell checking for language '%s' loaded", theLang) self._spellLanguage = theLang
logger.debug("Enchant spell checking for language '%s' loaded", theLang)
else:
logger.warning("Enchant found no dictionary for language '%s'", theLang)
except Exception: except Exception:
logger.error("Failed to load enchant spell checking for language '%s'", theLang) logger.error("Failed to load enchant spell checking for language '%s'", theLang)
self._theDict = FakeEnchant()
self._spellLanguage = None
self._readProjectDictionary(projectDict) if self._theDict is None:
for pWord in self._projDict: self._theDict = FakeEnchant()
self._theDict.add_to_session(pWord) else:
self._readProjectDictionary(projectDict)
for pWord in self._projDict:
self._theDict.add_to_session(pWord)
return return
@@ -160,10 +173,10 @@ class NWSpellEnchant():
self._projDict = set() self._projDict = set()
self._projectDict = projectDict self._projectDict = projectDict
if projectDict is None: if not isinstance(projectDict, Path):
return False return False
if not os.path.isfile(projectDict): if not projectDict.exists():
return False return False
try: try:
@@ -189,6 +202,9 @@ class FakeEnchant:
"""Fallback for when Enchant is selected, but not installed. """Fallback for when Enchant is selected, but not installed.
""" """
def __init__(self): def __init__(self):
self.tag = ""
self.provider = namedtuple("provider", "name")
self.provider.name = ""
return return
def check(self, theWord): def check(self, theWord):
+48 -43
View File
@@ -28,16 +28,15 @@ import random
import logging import logging
import novelwriter import novelwriter
from lxml import etree from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor
from PyQt5.QtCore import QRectF, Qt
from PyQt5.QtGui import QIcon, QPixmap, QColor from novelwriter.common import minmax, simplified
from novelwriter.common import checkInt, minmax, simplified
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class NWStatus(): class NWStatus:
STATUS = 1 STATUS = 1
IMPORT = 2 IMPORT = 2
@@ -46,13 +45,17 @@ class NWStatus():
self._type = type self._type = type
self._store = {} self._store = {}
self._reverse = {}
self._default = None self._default = None
self._iconSize = novelwriter.CONFIG.pxInt(32) self._iPX = novelwriter.CONFIG.pxInt(24)
pixmap = QPixmap(self._iconSize, self._iconSize)
pixmap.fill(QColor(100, 100, 100)) pA = novelwriter.CONFIG.pxInt(2)
self._defaultIcon = QIcon(pixmap) pB = novelwriter.CONFIG.pxInt(20)
pR = float(novelwriter.CONFIG.pxInt(4))
self._iconPath = QPainterPath()
self._iconPath.addRoundedRect(QRectF(pA, pA, pB, pB), pR, pR)
self._defaultIcon = self._createIcon(100, 100, 100)
if self._type == self.STATUS: if self._type == self.STATUS:
self._prefix = "s" self._prefix = "s"
@@ -63,31 +66,30 @@ class NWStatus():
return return
def write(self, key, name, cols, count=None): def write(self, key, name, col, count=None):
"""Add or update a status entry. If the key is invalid, a new """Add or update a status entry. If the key is invalid, a new
key is generated. key is generated.
""" """
if not self._isKey(key): if not self._isKey(key):
key = self._newKey() key = self._newKey()
if not isinstance(cols, tuple): if not isinstance(col, tuple):
cols = (100, 100, 100) col = (100, 100, 100)
if len(cols) != 3: if len(col) != 3:
cols = (100, 100, 100) col = (100, 100, 100)
pixmap = QPixmap(self._iconSize, self._iconSize)
pixmap.fill(QColor(*cols))
cR = minmax(col[0], 0, 255)
cG = minmax(col[1], 0, 255)
cB = minmax(col[2], 0, 255)
name = simplified(name) name = simplified(name)
if count is None: if count is None:
count = self._store[key]["count"] if key in self._store else 0 count = self._store.get(key, {}).get("count", 0)
self._store[key] = { self._store[key] = {
"name": name, "name": name,
"icon": QIcon(pixmap), "icon": self._createIcon(cR, cG, cB),
"cols": cols, "cols": (cR, cG, cB),
"count": count, "count": count,
} }
self._reverse[name] = key
if self._default is None: if self._default is None:
self._default = key self._default = key
@@ -103,7 +105,6 @@ class NWStatus():
if self._store[key]["count"] > 0: if self._store[key]["count"] > 0:
return False return False
del self._reverse[self._store[key]["name"]]
del self._store[key] del self._store[key]
keys = list(self._store.keys()) keys = list(self._store.keys())
@@ -120,8 +121,6 @@ class NWStatus():
""" """
if self._isKey(value) and value in self._store: if self._isKey(value) and value in self._store:
return value return value
elif value in self._reverse:
return self._reverse[value]
elif self._default is not None: elif self._default is not None:
return self._default return self._default
else: else:
@@ -203,37 +202,30 @@ class NWStatus():
self._store[key]["count"] += 1 self._store[key]["count"] += 1
return return
def packXML(self, xParent): def pack(self):
"""Pack the status entries into an XML object for saving to the """Pack the status entries into a dictionary.
main project file.
""" """
for key, data in self._store.items(): for key, data in self._store.items():
xSub = etree.SubElement(xParent, "entry", attrib={ yield (data["name"], {
"key": key, "key": key,
"count": str(data["count"]), "count": str(data["count"]),
"red": str(data["cols"][0]), "red": str(data["cols"][0]),
"green": str(data["cols"][1]), "green": str(data["cols"][1]),
"blue": str(data["cols"][2]), "blue": str(data["cols"][2]),
}) })
xSub.text = data["name"] return
return True def unpack(self, data):
"""Unpack a data dictionary and set the class values.
def unpackXML(self, xParent):
"""Unpack an XML tree and set the class values.
""" """
self._store = {} self._store = {}
self._reverse = {}
self._default = None self._default = None
for xChild in xParent: for key, entry in data.items():
key = xChild.attrib.get("key", None) label = entry.get("label", "")
name = xChild.text.strip() colour = entry.get("colour", (100, 100, 100))
count = max(checkInt(xChild.attrib.get("count", 0), 0), 0) count = entry.get("count", 0)
red = minmax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255) self.write(key, label, colour, count)
green = minmax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255)
blue = minmax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255)
self.write(key, name, (red, green, blue), count)
return True return True
@@ -267,6 +259,19 @@ class NWStatus():
return False return False
return True return True
def _createIcon(self, red, green, blue):
"""Generate an icon for a status label.
"""
pixmap = QPixmap(self._iPX, self._iPX)
pixmap.fill(Qt.transparent)
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.Antialiasing)
painter.fillPath(self._iconPath, QColor(red, green, blue))
painter.end()
return QIcon(pixmap)
## ##
# Iterator Bits # Iterator Bits
## ##
+396
View File
@@ -0,0 +1,396 @@
"""
novelWriter Project Storage Class
===================================
The main class handling the project storage
File History:
Created: 2022-11-01 [2.0rc1] NWStorage
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
import novelwriter
from time import time
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
from novelwriter.common import minmax
from novelwriter.constants import nwFiles
from novelwriter.core.document import NWDocument
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter
from novelwriter.error import logException
logger = logging.getLogger(__name__)
class NWStorage:
MODE_INACTIVE = 0
MODE_INPLACE = 1
MODE_ARCHIVE = 2
def __init__(self, theProject):
self.mainConf = novelwriter.CONFIG
self.theProject = theProject
self._storagePath = None
self._runtimePath = None
self._lockFilePath = None
self._openMode = self.MODE_INACTIVE
return
def clear(self):
"""Reset internal variables.
"""
self._storagePath = None
self._runtimePath = None
self._openMode = self.MODE_INACTIVE
return
##
# Properties
##
@property
def storagePath(self):
return self._storagePath
@property
def runtimePath(self):
return self._runtimePath
@property
def contentPath(self):
if self._runtimePath is not None:
return self._runtimePath / "content"
return None
##
# Core Methods
##
def isOpen(self):
"""Check if the storage location is open.
"""
return self._runtimePath is not None
def openProjectInPlace(self, path, newProject=False):
"""Open a novelWriter project in-place. That is, it is opened
directly from a project folder.
"""
inPath = Path(path).resolve()
if inPath.is_file():
# The path should not point to an exisitng file,
# but it can point to a folder containing files
inPath = inPath.parent
self._storagePath = inPath
self._runtimePath = inPath
self._lockFilePath = inPath / nwFiles.PROJ_LOCK
self._openMode = self.MODE_INPLACE
if not self._prepareStorage(checkLegacy=True, newProject=newProject):
self.clear()
return False
return True
def openProjectArchive(self, path): # pragma: no cover
pass
def runPostSaveTasks(self, autoSave=False): # pragma: no cover
"""Run tasks after the project has been saved.
"""
if self._openMode == self.MODE_INPLACE:
# Nothing to do, so we just return
return True
return True
def closeSession(self):
"""Run tasks related to closing the session.
"""
# Clear lockfile
self.clear()
return
##
# Content Access Methods
##
def getXmlReader(self):
"""Return a properly configured ProjectXMLReader instance.
"""
if self._runtimePath is None:
return None
projFile = self._runtimePath / nwFiles.PROJ_FILE
xmlReader = ProjectXMLReader(projFile)
return xmlReader
def getXmlWriter(self):
"""Return a properly configured ProjectXMLWriter instance.
"""
if self._runtimePath is None:
return None
xmlWriter = ProjectXMLWriter(self._runtimePath)
return xmlWriter
def getDocument(self, tHandle):
"""Return a document wrapper object.
"""
if self._runtimePath is not None:
return NWDocument(self.theProject, tHandle)
return NWDocument(self.theProject, None)
def getMetaFile(self, fileName):
"""Return the path to a file in the project meta folder.
"""
if self._runtimePath is not None:
return self._runtimePath / "meta" / fileName
return None
def getCacheFile(self, fileName):
"""Return the path to a file in the project cache folder.
"""
if self._runtimePath is not None:
return self._runtimePath / "cache" / fileName
return None
def readLockFile(self):
"""Read the project lock file.
"""
if self._lockFilePath is None:
return ["ERROR"]
if not self._lockFilePath.exists():
return []
try:
lines = self._lockFilePath.read_text(encoding="utf-8").split(";")
except Exception:
logger.error("Failed to read project lockfile")
logException()
return ["ERROR"]
if len(lines) != 4:
return ["ERROR"]
return lines
def writeLockFile(self):
"""Write the project lock file.
"""
if self._lockFilePath is None:
return False
data = [
self.mainConf.hostName, self.mainConf.osType,
self.mainConf.kernelVer, str(int(time()))
]
try:
self._lockFilePath.write_text(";".join(data), encoding="utf-8")
except Exception:
logger.error("Failed to write project lockfile")
logException()
return False
return True
def clearLockFile(self):
"""Remove the lock file, if it exists.
"""
if self._lockFilePath is None:
return False
if self._lockFilePath.exists():
try:
self._lockFilePath.unlink()
except Exception:
logger.error("Failed to remove project lockfile")
logException()
return False
return True
def zipIt(self, target, compression=None):
"""Zip the content of the project at its runtime location into a
zip file. This process will only grab files that are supposed to
be in the project. All non-project files will be left out.
"""
basePath = self._runtimePath
if not isinstance(basePath, Path):
logger.error("No path set")
return False
baseMeta = basePath / "meta"
baseCont = basePath / "content"
files = [
(basePath / nwFiles.PROJ_FILE, nwFiles.PROJ_FILE),
(baseMeta / nwFiles.OPTS_FILE, f"meta/{nwFiles.OPTS_FILE}"),
(baseMeta / nwFiles.SESS_STATS, f"meta/{nwFiles.SESS_STATS}"),
(baseMeta / nwFiles.INDEX_FILE, f"meta/{nwFiles.INDEX_FILE}"),
(baseMeta / nwFiles.PROJ_DICT, f"meta/{nwFiles.PROJ_DICT}"),
]
for contItem in baseCont.iterdir():
name = contItem.name
if contItem.is_file() and len(name) == 17 and name.endswith(".nwd"):
files.append((contItem, f"content/{name}"))
comp = ZIP_STORED if compression is None else ZIP_DEFLATED
level = minmax(compression, 0, 9) if isinstance(compression, int) else None
try:
with ZipFile(target, mode="w", compression=comp, compresslevel=level) as zipObj:
logger.info("Creating archive: %s", target)
for srcPath, zipPath in files:
if srcPath.is_file():
zipObj.write(srcPath, zipPath)
logger.debug("Added: %s", zipPath)
except Exception:
logger.error("Failed to create acrhive")
logException()
return False
return True
##
# Internal Functions
##
def _prepareStorage(self, checkLegacy=True, newProject=False):
"""Prepare the storage area for the project.
"""
path = self._runtimePath
if not isinstance(path, Path):
logger.error("No path set")
self.clear()
return False
if path == Path.home().absolute():
logger.error("Cannot use the user's home path as the root of a project")
self.clear()
return False
if newProject:
# If it's a new project, we check that there is no existing
# project in the selected path.
if path.exists() and len(list(path.iterdir())) > 0:
logger.error("The new project folder is not empty")
self.clear()
return False
# The folder is not required to exist, as it could be a new
# project, so we make sure it does. Then we add subfolders.
try:
path.mkdir(exist_ok=True)
(path / "content").mkdir(exist_ok=True)
(path / "cache").mkdir(exist_ok=True)
(path / "meta").mkdir(exist_ok=True)
except Exception as exc:
logger.error("Failed to create required project folders", exc_info=exc)
self.clear()
return False
if not checkLegacy:
# The legacy content check is only needed for project folder
# storage, so if it is not expected to be that, there's no
# need for the remaning checks.
return True
# Check for legacy data folders
for child in path.iterdir():
if child.is_dir() and child.name.startswith("data_"):
self._legacyDataFolder(path, child)
# Check for no longer used files, and delete them
self._deleteDeprecatedFiles(path)
return True
##
# Legacy Project Data Handlers
##
def _legacyDataFolder(self, path: Path, child: Path):
"""Handle the content of a legacy data folder from a version 1.0
project.
"""
logger.info("Processing legacy data folder: %s", path)
# Move Documents to Content
first = child.name[-1]
if first not in "0123456789abcdef":
return
for item in child.iterdir():
if not item.is_file():
continue
name = item.name
if len(name) == 21 and name.endswith("_main.nwd"):
newPath = path / "content" / f"{first}{name[:12]}.nwd"
try:
item.rename(newPath)
logger.info("Moved file: %s", newPath)
except Exception as exc:
logger.warning("Failed to move: %s", item, exc_info=exc)
elif len(name) == 21 and name.endswith("_main.bak"):
try:
item.unlink()
logger.info("Deleted file: %s", item)
except Exception as exc:
logger.warning("Failed to delete: %s", item, exc_info=exc)
# Remove Data Folder
try:
child.rmdir()
logger.info("Deleted folder: %s", child)
except Exception as exc:
logger.warning("Failed to delete: %s", child, exc_info=exc)
return
def _deleteDeprecatedFiles(self, path: Path):
"""Delete files that are no longer used by novelWriter.
"""
remove = [
path / "meta" / "mainOptions.json", # Replaced in 0.5
path / "meta" / "exportOptions.json", # Replaced in 0.5
path / "meta" / "outlineOptions.json", # Replaced in 0.5
path / "meta" / "timelineOptions.json", # Replaced in 0.5
path / "meta" / "docMergeOptions.json", # Replaced in 0.5
path / "meta" / "sessionLogOptions.json", # Replaced in 0.5
path / "ToC.json", # Dropped in 1.0 RC 1
]
for item in remove:
if item.is_file():
try:
item.unlink()
logger.info("Deleted: %s", item)
except Exception as exc:
logger.warning("Failed to delete: %s", item, exc_info=exc)
return
# END Class NWStorage
+3 -3
View File
@@ -38,7 +38,7 @@ class ToHtml(Tokenizer):
M_EBOOK = 2 # Tweak output for converting to epub M_EBOOK = 2 # Tweak output for converting to epub
def __init__(self, theProject): def __init__(self, theProject):
Tokenizer.__init__(self, theProject) super().__init__(theProject)
self._genMode = self.M_EXPORT self._genMode = self.M_EXPORT
self._cssStyles = True self._cssStyles = True
@@ -107,7 +107,7 @@ class ToHtml(Tokenizer):
"""Extend the auto-replace to also properly encode some unicode """Extend the auto-replace to also properly encode some unicode
characters into their respective HTML entities. characters into their respective HTML entities.
""" """
Tokenizer.doPreProcessing(self) super().doPreProcessing()
self._theText = self._theText.translate(self._trMap) self._theText = self._theText.translate(self._trMap)
return return
@@ -315,7 +315,7 @@ class ToHtml(Tokenizer):
"</body>\n" "</body>\n"
"</html>\n" "</html>\n"
).format( ).format(
projTitle=self.theProject.projName, projTitle=self.theProject.data.name,
htmlStyle="\n".join(theStyle), htmlStyle="\n".join(theStyle),
bodyText=bodyText, bodyText=bodyText,
) )
+7 -13
View File
@@ -36,7 +36,6 @@ from PyQt5.QtCore import QCoreApplication, QRegularExpression
from novelwriter.enum import nwItemLayout, nwItemType from novelwriter.enum import nwItemLayout, nwItemType
from novelwriter.common import numberToRoman, checkInt from novelwriter.common import numberToRoman, checkInt
from novelwriter.constants import nwConst, nwRegEx, nwUnicode from novelwriter.constants import nwConst, nwRegEx, nwUnicode
from novelwriter.core.document import NWDoc
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -304,16 +303,10 @@ class Tokenizer(ABC):
if self._theItem is None: if self._theItem is None:
return False return False
self._theText = "" if theText is None:
if theText is not None: theText = self.theProject.storage.getDocument(theHandle).readDocument() or ""
# If the text is set, just use that
self._theText = theText self._theText = theText
else:
# Otherwise, load it from file
theDoc = NWDoc(self.theProject, theHandle)
theText = theDoc.readDocument()
if theText:
self._theText = theText
docSize = len(self._theText) docSize = len(self._theText)
if docSize > nwConst.MAX_DOCSIZE: if docSize > nwConst.MAX_DOCSIZE:
@@ -333,9 +326,10 @@ class Tokenizer(ABC):
"""Run trough the various replace doctionaries. """Run trough the various replace doctionaries.
""" """
# Process the user's auto-replace dictionary # Process the user's auto-replace dictionary
if len(self.theProject.autoReplace) > 0: autoReplace = self.theProject.data.autoReplace
if len(autoReplace) > 0:
repDict = {} repDict = {}
for aKey, aVal in self.theProject.autoReplace.items(): for aKey, aVal in autoReplace.items():
repDict[f"<{aKey}>"] = aVal repDict[f"<{aKey}>"] = aVal
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL) xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
self._theText = xRep.sub(lambda x: repDict[x.group(0)], self._theText) self._theText = xRep.sub(lambda x: repDict[x.group(0)], self._theText)
+1 -1
View File
@@ -37,7 +37,7 @@ class ToMarkdown(Tokenizer):
M_GH = 1 # GitHub Markdown M_GH = 1 # GitHub Markdown
def __init__(self, theProject): def __init__(self, theProject):
Tokenizer.__init__(self, theProject) super().__init__(theProject)
self._genMode = self.M_STD self._genMode = self.M_STD
self._fullMD = [] self._fullMD = []
+6 -6
View File
@@ -89,7 +89,7 @@ M_DEL = ~X_DEL
class ToOdt(Tokenizer): class ToOdt(Tokenizer):
def __init__(self, theProject, isFlat): def __init__(self, theProject, isFlat):
Tokenizer.__init__(self, theProject) super().__init__(theProject)
self._isFlat = isFlat # Flat: .fodt, otherwise .odt self._isFlat = isFlat # Flat: .fodt, otherwise .odt
@@ -261,8 +261,8 @@ class ToOdt(Tokenizer):
# =============== # ===============
if self._headerText == "": if self._headerText == "":
theTitle = self.theProject.bookTitle theTitle = self.theProject.data.title
theAuth = self.theProject.getAuthors() theAuth = self.theProject.getFormattedAuthors()
self._headerText = f"{theTitle} / {theAuth} /" self._headerText = f"{theTitle} / {theAuth} /"
# Create Roots # Create Roots
@@ -994,7 +994,7 @@ class ToOdt(Tokenizer):
# Auto-Style Classes # Auto-Style Classes
# =============================================================================================== # # =============================================================================================== #
class ODTParagraphStyle(): class ODTParagraphStyle:
"""Wrapper class for the paragraph style setting used by the """Wrapper class for the paragraph style setting used by the
exporter. Only the used settings are exposed here to keep the class exporter. Only the used settings are exposed here to keep the class
minimal and fast. minimal and fast.
@@ -1208,7 +1208,7 @@ class ODTParagraphStyle():
# END Class ODTParagraphStyle # END Class ODTParagraphStyle
class ODTTextStyle(): class ODTTextStyle:
"""Wrapper class for the text style setting used by the exporter. """Wrapper class for the text style setting used by the exporter.
Only the used settings are exposed here to keep the class minimal Only the used settings are exposed here to keep the class minimal
and fast. and fast.
@@ -1297,7 +1297,7 @@ X_SPAN_TEXT = 2
X_SPAN_SING = 3 X_SPAN_SING = 3
class XMLParagraph(): class XMLParagraph:
"""This is a helper class to manage the text content of a single """This is a helper class to manage the text content of a single
XML element using mixed content tags. XML element using mixed content tags.
+45 -48
View File
@@ -23,13 +23,12 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import random import random
import logging import logging
from lxml import etree from pathlib import Path
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout from novelwriter.enum import nwItemClass, nwItemLayout
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import checkHandle from novelwriter.common import checkHandle
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
@@ -38,7 +37,7 @@ from novelwriter.core.item import NWItem
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class NWTree(): class NWTree:
MAX_DEPTH = 1000 # Cap of tree traversing for loops MAX_DEPTH = 1000 # Cap of tree traversing for loops
@@ -89,20 +88,20 @@ class NWTree():
logger.warning("Duplicate handle '%s' detected, skipping", tHandle) logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
return False 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.setHandle(tHandle)
nwItem.setParent(pHandle) nwItem.setParent(pHandle)
if nwItem.itemType == nwItemType.ROOT: 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 self._treeRoots[tHandle] = nwItem
if nwItem.itemClass == nwItemClass.ARCHIVE: 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 self._archRoot = tHandle
elif nwItem.itemClass == nwItemClass.TRASH: elif nwItem.itemClass == nwItemClass.TRASH:
if self._trashRoot is None: 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 self._trashRoot = tHandle
else: else:
logger.error("Only one trash folder allowed") logger.error("Only one trash folder allowed")
@@ -114,30 +113,25 @@ class NWTree():
return True return True
def packXML(self, xParent): def pack(self):
"""Pack the content of the tree into the provided XML object. In """Pack the content of the tree into the provided XML object. In
the order defined by the _treeOrder list. the order defined by the _treeOrder list.
""" """
xContent = etree.SubElement(xParent, "content", attrib={ tree = []
"count": str(len(self._treeOrder))}
)
for tHandle in self._treeOrder: for tHandle in self._treeOrder:
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
tItem.packXML(xContent) if tItem:
return tree.append(tItem.pack())
return tree
def unpackXML(self, xContent): def unpack(self, data):
"""Iterate through all items of a content XML object and add """Iterate through all items of a list and add them to the
them to the project tree. project tree.
""" """
if xContent.tag != "content":
logger.error("XML entry is not a NWTree")
return False
self.clear() self.clear()
for xItem in xContent: for item in data:
nwItem = NWItem(self.theProject) nwItem = NWItem(self.theProject)
if nwItem.unpackXML(xItem): if nwItem.unpack(item):
self.append(nwItem.itemHandle, nwItem.itemParent, nwItem) self.append(nwItem.itemHandle, nwItem.itemParent, nwItem)
nwItem.saveInitialCount() nwItem.saveInitialCount()
@@ -147,16 +141,22 @@ class NWTree():
"""Write the convenience table of contents file in the root of """Write the convenience table of contents file in the root of
the project directory. the project directory.
""" """
runtimePath = self.theProject.storage.runtimePath
contentPath = self.theProject.storage.contentPath
if not (isinstance(contentPath, Path) and isinstance(runtimePath, Path)):
return False
tocList = [] tocList = []
tocLen = 0 tocLen = 0
for tHandle in self._treeOrder: for tHandle in self._treeOrder:
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem is None: if tItem is None:
continue continue
tFile = tHandle+".nwd" tFile = tHandle+".nwd"
if os.path.isfile(os.path.join(self.theProject.projContent, tFile)): if (contentPath / tFile).is_file():
tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format( tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format(
os.path.join("content", tFile), str(Path("content") / tFile),
tItem.itemClass.name, tItem.itemClass.name,
tItem.itemLayout.name, tItem.itemLayout.name,
tItem.itemName, tItem.itemName,
@@ -166,7 +166,7 @@ class NWTree():
try: try:
# Dump the text # Dump the text
tocText = os.path.join(self.theProject.projPath, nwFiles.TOC_TXT) tocText = runtimePath / nwFiles.TOC_TXT
with open(tocText, mode="w", encoding="utf-8") as outFile: with open(tocText, mode="w", encoding="utf-8") as outFile:
outFile.write("\n") outFile.write("\n")
outFile.write("Table of Contents\n") outFile.write("Table of Contents\n")
@@ -274,11 +274,13 @@ class NWTree():
return rootClasses return rootClasses
def iterRoots(self, itemClass): 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(): for tHandle in self._treeOrder:
if nwItem.itemClass == itemClass: nwItem = self.__getitem__(tHandle)
yield tHandle, nwItem if nwItem is not None and nwItem.isRootType():
if itemClass is None or nwItem.itemClass == itemClass:
yield tHandle, nwItem
return return
def isRoot(self, tHandle): def isRoot(self, tHandle):
@@ -329,25 +331,20 @@ class NWTree():
def setOrder(self, newOrder): def setOrder(self, newOrder):
"""Reorders the tree based on a list of items. """Reorders the tree based on a list of items.
""" """
tmpOrder = [] tmpOrder = [tHandle for tHandle in newOrder if tHandle in self._projTree]
if not (len(tmpOrder) == len(newOrder) == len(self._treeOrder)):
# Add all known elements to a new temp list # Something is wrong, so let's debug it
for tHandle in newOrder: for tHandle in newOrder:
if tHandle in self._projTree: if tHandle not in self._projTree:
tmpOrder.append(tHandle) logger.error("Handle '%s' in new tree order is not in old order", tHandle)
else: for tHandle in self._treeOrder:
logger.error("Handle '%s' in new tree order is not in project tree", tHandle) if tHandle not in tmpOrder:
logger.warning("Handle '%s' in old tree order is not in new order", tHandle)
# Do a reverse lookup to check for items that will be lost
# This is mainly for debugging purposes
for tHandle in self._treeOrder:
if tHandle not in tmpOrder:
logger.warning("Handle '%s' in old tree order is not in new tree order", tHandle)
# Save the temp list # Save the temp list
self._treeOrder = tmpOrder self._treeOrder = tmpOrder
self._setTreeChanged(True) self._setTreeChanged(True)
logger.verbose("Project tree order updated") logger.debug("Project tree order updated")
return return
@@ -357,7 +354,7 @@ class NWTree():
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem is None: if tItem is None:
return False return False
if tItem.itemType != nwItemType.FILE: if not tItem.isFileType():
logger.error("Item '%s' is not a file", tHandle) logger.error("Item '%s' is not a file", tHandle)
return False return False
if not isinstance(itemLayout, nwItemLayout): if not isinstance(itemLayout, nwItemLayout):
@@ -457,7 +454,7 @@ class NWTree():
"""Generate a unique item handle. In the event that the key """Generate a unique item handle. In the event that the key
already exists, generate a new one. already exists, generate a new one.
""" """
logger.verbose("Generating new handle") logger.debug("Generating new handle")
handle = f"{random.getrandbits(52):013x}" handle = f"{random.getrandbits(52):013x}"
if handle in self._projTree: if handle in self._projTree:
logger.warning("Duplicate handle encountered! Retrying ...") logger.warning("Duplicate handle encountered! Retrying ...")
@@ -202,7 +202,7 @@ class QConfigLayout(QGridLayout):
class QHelpLabel(QLabel): class QHelpLabel(QLabel):
def __init__(self, theText, textCol, fontSize=0.9): def __init__(self, theText, textCol, fontSize=0.9):
QLabel.__init__(self, theText) super().__init__(theText)
if isinstance(textCol, QColor): if isinstance(textCol, QColor):
qCol = textCol qCol = textCol
@@ -267,8 +267,8 @@ class QSwitch(QAbstractButton):
return self._offset return self._offset
@offset.setter @offset.setter
def offset(self, theOffset): def offset(self, offset):
self._offset = theOffset self._offset = offset
self.update() self.update()
return return
@@ -276,11 +276,11 @@ class QSwitch(QAbstractButton):
# Getters and Setters # Getters and Setters
## ##
def setChecked(self, isChecked): def setChecked(self, checked):
"""Overload setChecked to also alter the offset. """Overload setChecked to also alter the offset.
""" """
super().setChecked(isChecked) super().setChecked(checked)
if isChecked: if checked:
self.offset = self._xW - self._xR self.offset = self._xW - self._xR
else: else:
self.offset = self._xR self.offset = self._xR
@@ -290,10 +290,10 @@ class QSwitch(QAbstractButton):
# Events # Events
## ##
def resizeEvent(self, theEvent): def resizeEvent(self, event):
"""Overload resize to ensure correct offset. """Overload resize to ensure correct offset.
""" """
super().resizeEvent(theEvent) super().resizeEvent(event)
if self.isChecked(): if self.isChecked():
self.offset = self._xW - self._xR self.offset = self._xW - self._xR
else: else:
@@ -377,7 +377,7 @@ class QSwitch(QAbstractButton):
class PagedDialog(QDialog): class PagedDialog(QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
QDialog.__init__(self, parent=parent) super().__init__(parent=parent)
self._tabBar = VerticalTabBar(self) self._tabBar = VerticalTabBar(self)
self._tabBar.setExpanding(False) self._tabBar.setExpanding(False)
@@ -410,31 +410,37 @@ class PagedDialog(QDialog):
return return
def addTab(self, widget, label): 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) self._tabBox.addTab(widget, label)
return return
def addControls(self, buttonBar): def addControls(self, buttonBar):
"""Adds a button bar to the dialog. """Add a button bar to the dialog.
""" """
self._buttonBox.addWidget(buttonBar) self._buttonBox.addWidget(buttonBar)
return return
def setCurrentWidget(self, widget):
"""Forward the changing of tab to the QTabWidget.
"""
self._tabBox.setCurrentWidget(widget)
return
# END Class PagedDialog # END Class PagedDialog
class VerticalTabBar(QTabBar): class VerticalTabBar(QTabBar):
def __init__(self, parent=None): def __init__(self, parent=None):
QTabBar.__init__(self, parent=parent) super().__init__(parent=parent)
self._mW = novelwriter.CONFIG.pxInt(150) self._mW = novelwriter.CONFIG.pxInt(150)
return return
def tabSizeHint(self, index): 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.transpose()
tSize.setWidth(min(tSize.width(), self._mW)) tSize.setWidth(min(tSize.width(), self._mW))
return tSize return tSize
+3 -4
View File
@@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import logging import logging
import novelwriter import novelwriter
@@ -44,7 +43,7 @@ logger = logging.getLogger(__name__)
class GuiAbout(QDialog): class GuiAbout(QDialog):
def __init__(self, mainGui): def __init__(self, mainGui):
QDialog.__init__(self, mainGui) super().__init__(parent=mainGui)
logger.debug("Initialising GuiAbout ...") logger.debug("Initialising GuiAbout ...")
self.setObjectName("GuiAbout") self.setObjectName("GuiAbout")
@@ -234,7 +233,7 @@ class GuiAbout(QDialog):
def _fillNotesPage(self): def _fillNotesPage(self):
"""Load the content for the Release Notes page. """Load the content for the Release Notes page.
""" """
docPath = os.path.join(self.mainConf.assetPath, "text", "release_notes.htm") docPath = self.mainConf.assetPath("text") / "release_notes.htm"
docText = readTextFile(docPath) docText = readTextFile(docPath)
if docText: if docText:
self.pageNotes.setHtml(docText) self.pageNotes.setHtml(docText)
@@ -245,7 +244,7 @@ class GuiAbout(QDialog):
def _fillLicensePage(self): def _fillLicensePage(self):
"""Load the content for the Licence page. """Load the content for the Licence page.
""" """
docPath = os.path.join(self.mainConf.assetPath, "text", "gplv3_en.htm") docPath = self.mainConf.assetPath("text") / "gplv3_en.htm"
docText = readTextFile(docPath) docText = readTextFile(docPath)
if docText: if docText:
self.pageLicense.setHtml(docText) self.pageLicense.setHtml(docText)
+88 -110
View File
@@ -1,10 +1,11 @@
""" """
novelWriter GUI Doc Merge Tool novelWriter GUI Doc Merge Dialog
================================ ==================================
GUI class for merging multiple documents to one document Custom dialog class for merging documents.
File History: 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 This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen Copyright 20182022, Veronica Berglyd Olsen
@@ -26,169 +27,146 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging import logging
import novelwriter import novelwriter
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QLabel, QListWidget, QAbstractItemView, QAbstractItemView, QDialog, QDialogButtonBox, QGridLayout, QLabel,
QListWidgetItem, QDialogButtonBox QListWidget, QListWidgetItem, QVBoxLayout,
) )
from novelwriter.core import NWDoc from novelwriter.custom import QHelpLabel, QSwitch
from novelwriter.enum import nwAlert, nwItemType
from novelwriter.gui.custom import QHelpLabel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GuiDocMerge(QDialog): class GuiDocMerge(QDialog):
def __init__(self, mainGui): def __init__(self, mainGui, sHandle, itemList):
QDialog.__init__(self, mainGui) super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocMerge ...") logger.debug("Initialising GuiDocMerge ...")
self.setObjectName("GuiDocMerge") self.setObjectName("GuiDocMerge")
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject self.theProject = mainGui.theProject
self.sourceItem = None
self.outerBox = QVBoxLayout() self._data = {}
self.setWindowTitle(self.tr("Merge Documents")) self.setWindowTitle(self.tr("Merge Documents"))
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge"))) self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge")))
self.helpLabel = QHelpLabel( self.helpLabel = QHelpLabel(self.tr(
self.tr("Drag and drop items to change the order."), self.mainGui.mainTheme.helpText "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 = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.InternalMove) self.listBox.setIconSize(QSize(iPx, iPx))
self.listBox.setMinimumWidth(self.mainConf.pxInt(400)) self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
self.listBox.setMinimumHeight(self.mainConf.pxInt(180)) 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 = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doMerge) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self._doClose) 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.setSpacing(0)
self.outerBox.addWidget(self.headLabel) self.outerBox.addWidget(self.headLabel)
self.outerBox.addWidget(self.helpLabel) 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.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.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
self.rejected.connect(self._doClose) # Load Content
self._loadContent(sHandle, itemList)
self._populateList()
logger.debug("GuiDocMerge initialisation complete") logger.debug("GuiDocMerge initialisation complete")
return return
## def getData(self):
# Buttons """Return the user's choices.
##
def _doMerge(self):
"""Perform the merge of the files in the selected folder, and
create a new file in the same parent folder. The old files are
not removed in the merge process, and must be deleted manually.
""" """
logger.verbose("GuiDocMerge merge button clicked") finalItems = []
finalOrder = []
for i in range(self.listBox.count()): 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._data["moveToTrash"] = self.trashSwitch.isChecked()
self.mainGui.makeAlert(self.tr( self._data["finalItems"] = finalItems
"No source documents found. Nothing to do."
), nwAlert.ERROR)
return False
theText = "" return self._data
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"
if self.sourceItem is None: ##
self.mainGui.makeAlert(self.tr( # Slots
"No source folder selected. Nothing to do." ##
), nwAlert.ERROR)
return False
srcItem = self.theProject.tree[self.sourceItem] def _resetList(self):
if srcItem is None: """Reset the content of the list box to its original state.
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.
""" """
self.close() logger.debug("Resetting list box content")
sHandle = self._data.get("sHandle", None)
itemList = self._data.get("origItems", [])
self._loadContent(sHandle, itemList)
return return
## ##
# Internal Functions # Internal Functions
## ##
def _populateList(self): def _loadContent(self, sHandle, itemList):
"""Get the item selected in the tree, check that it is a folder, """Load content from a given list of items.
and try to find all files associated with it. The valid files
are then added to the list view in order. The list itself can be
reordered by the user.
""" """
tHandle = self.mainGui.projView.getSelectedHandle() self._data = {}
self.sourceItem = tHandle self._data["sHandle"] = sHandle
if tHandle is None: self._data["origItems"] = itemList
return False
nwItem = self.theProject.tree[tHandle] self.listBox.clear()
if nwItem is None: for tHandle in itemList:
return False nwItem = self.theProject.tree[tHandle]
if nwItem is None or not nwItem.isFileType():
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 nwItem.itemType is not nwItemType.FILE:
continue continue
itemIcon = self.mainTheme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading
)
newItem = QListWidgetItem()
newItem.setIcon(itemIcon)
newItem.setText(nwItem.itemName) newItem.setText(nwItem.itemName)
newItem.setData(Qt.UserRole, sHandle) newItem.setData(Qt.UserRole, tHandle)
newItem.setCheckState(Qt.Checked)
self.listBox.addItem(newItem) self.listBox.addItem(newItem)
return True return
# END Class GuiDocMerge # END Class GuiDocMerge
+131 -160
View File
@@ -1,10 +1,11 @@
""" """
novelWriter GUI Doc Split Tool novelWriter GUI Doc Split Dialog
================================ ==================================
GUI class for splitting a single document into multiple documents Custom dialog class for splitting documents.
File History: 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 This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen Copyright 20182022, Veronica Berglyd Olsen
@@ -29,32 +30,34 @@ import novelwriter
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QComboBox, QListWidget, QAbstractItemView, QDialog, QVBoxLayout, QComboBox, QListWidget, QAbstractItemView,
QListWidgetItem, QDialogButtonBox, QLabel QListWidgetItem, QDialogButtonBox, QLabel, QGridLayout
) )
from novelwriter.core import NWDoc from novelwriter.custom import QHelpLabel, QSwitch
from novelwriter.enum import nwAlert, nwItemType
from novelwriter.gui.custom import QHelpLabel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GuiDocSplit(QDialog): class GuiDocSplit(QDialog):
def __init__(self, mainGui): LINE_ROLE = Qt.UserRole
QDialog.__init__(self, mainGui) LEVEL_ROLE = Qt.UserRole + 1
LABEL_ROLE = Qt.UserRole + 2
def __init__(self, mainGui, sHandle):
super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocSplit ...") logger.debug("Initialising GuiDocSplit ...")
self.setObjectName("GuiDocSplit") self.setObjectName("GuiDocSplit")
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject self.theProject = mainGui.theProject
self.sourceItem = None self._data = {}
self.sourceText = [] self._text = []
self.outerBox = QVBoxLayout()
self.setWindowTitle(self.tr("Split Document")) self.setWindowTitle(self.tr("Split Document"))
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Document Headers"))) self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Document Headers")))
@@ -63,6 +66,18 @@ class GuiDocSplit(QDialog):
self.mainGui.mainTheme.helpText 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 = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop) self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
self.listBox.setMinimumWidth(self.mainConf.pxInt(400)) self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
@@ -73,206 +88,162 @@ 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 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 3 (Scene)"), 3)
self.splitLevel.addItem(self.tr("Split up to Header Level 4 (Section)"), 4) self.splitLevel.addItem(self.tr("Split up to Header Level 4 (Section)"), 4)
spIndex = self.splitLevel.findData( spIndex = self.splitLevel.findData(spLevel)
self.theProject.options.getInt("GuiDocSplit", "spLevel", 3)
)
if spIndex != -1: if spIndex != -1:
self.splitLevel.setCurrentIndex(spIndex) 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.trashLabel = QLabel(self.tr("Move split document to Trash"))
self.trashSwitch = QSwitch(width=2*iPx, height=iPx)
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.addWidget(self.trashLabel, 2, 0)
self.optBox.addWidget(self.trashSwitch, 2, 1)
self.optBox.setVerticalSpacing(vSp)
self.optBox.setHorizontalSpacing(hSp)
self.optBox.setColumnStretch(3, 1)
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doSplit) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self._doClose) self.buttonBox.rejected.connect(self.reject)
# Assemble
self.outerBox = QVBoxLayout()
self.outerBox.setSpacing(0) self.outerBox.setSpacing(0)
self.outerBox.addWidget(self.headLabel) self.outerBox.addWidget(self.headLabel)
self.outerBox.addWidget(self.helpLabel) 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.listBox)
self.outerBox.addWidget(self.splitLevel) 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.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
self.rejected.connect(self._doClose) # Load Content
self._loadContent(sHandle)
self._populateList()
logger.debug("GuiDocSplit initialisation complete") logger.debug("GuiDocSplit initialisation complete")
return return
## def getData(self):
# Buttons """Return the user's choices. Also save the users options for
## the next time the dialog is used.
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.
""" """
logger.verbose("GuiDocSplit split button clicked") headerList = []
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 = []
for i in range(self.listBox.count()): for i in range(self.listBox.count()):
listItem = self.listBox.item(i) item = self.listBox.item(i)
wTitle = listItem.text() if item is not None:
lineNo = listItem.data(Qt.UserRole) headerList.append((
finalOrder.append([wTitle, lineNo, nLines]) item.data(self.LINE_ROLE),
if i > 0: item.data(self.LEVEL_ROLE),
finalOrder[i-1][2] = lineNo item.data(self.LABEL_ROLE),
))
nFiles = len(finalOrder) spLevel = self.splitLevel.currentData()
if nFiles == 0: intoFolder = self.folderSwitch.isChecked()
self.mainGui.makeAlert(self.tr( docHierarchy = self.hierarchySwitch.isChecked()
"No headers found. Nothing to do." moveToTrash = self.trashSwitch.isChecked()
), nwAlert.ERROR)
return False
msgYes = self.mainGui.askQuestion( self._data["spLevel"] = spLevel
self.tr("Split Document"), self._data["headerList"] = headerList
"{0}<br><br>{1}".format( self._data["intoFolder"] = intoFolder
self.tr( self._data["docHierarchy"] = docHierarchy
"The document will be split into {0} file(s) in a new folder. " self._data["moveToTrash"] = moveToTrash
"The original document will remain intact."
).format(nFiles),
self.tr(
"Continue with the splitting process?"
)
)
)
if not msgYes:
return False
# Create the folder pOptions = self.theProject.options
fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent) pOptions.setValue("GuiDocSplit", "spLevel", spLevel)
self.mainGui.projView.revealNewTreeItem(fHandle) pOptions.setValue("GuiDocSplit", "intoFolder", intoFolder)
logger.verbose("Creating folder '%s'", fHandle) pOptions.setValue("GuiDocSplit", "docHierarchy", docHierarchy)
# Loop through, and create the files return self._data, self._text
for wTitle, iStart, iEnd in finalOrder:
wTitle = wTitle.lstrip("#").strip() ##
nHandle = self.theProject.newFile(wTitle, fHandle) # Slots
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
)
theText = "\n".join(self.sourceText[iStart:iEnd]) def _reloadList(self):
theText = theText.rstrip("\n") + "\n\n" """Reload the content of the list box.
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.
""" """
self.theProject.options.saveSettings() sHandle = self._data.get("sHandle", None)
self.close() self._loadContent(sHandle)
return return
## ##
# Internal Functions # Internal Functions
## ##
def _populateList(self): def _loadContent(self, sHandle):
"""Get the item selected in the tree, check that it is a folder, """Load content from a given source item.
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.
""" """
self._data = {}
self._data["sHandle"] = sHandle
self.listBox.clear() self.listBox.clear()
if self.sourceItem is None:
self.sourceItem = self.mainGui.projView.getSelectedHandle()
if self.sourceItem is None: nwItem = self.theProject.tree[sHandle]
return False if nwItem is None or not nwItem.isFileType():
return
nwItem = self.theProject.tree[self.sourceItem]
if nwItem is None:
return False
if nwItem.itemType is not nwItemType.FILE:
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
spLevel = self.splitLevel.currentData() spLevel = self.splitLevel.currentData()
self.theProject.options.setValue("GuiDocSplit", "spLevel", spLevel) if not self._text:
logger.debug( inDoc = self.theProject.storage.getDocument(sHandle)
"Scanning document '%s' for headings level <= %d", self._text = (inDoc.readDocument() or "").splitlines()
self.sourceItem, spLevel
)
self.sourceText = theText.splitlines() for lineNo, aLine in enumerate(self._text):
for lineNo, aLine in enumerate(self.sourceText):
onLine = -1 onLine = -1
hLevel = 0
hLabel = aLine.strip()
if aLine.startswith("# ") and spLevel >= 1: if aLine.startswith("# ") and spLevel >= 1:
onLine = lineNo onLine = lineNo
hLevel = 1
hLabel = aLine[2:].strip()
elif aLine.startswith("## ") and spLevel >= 2: elif aLine.startswith("## ") and spLevel >= 2:
onLine = lineNo onLine = lineNo
hLevel = 2
hLabel = aLine[3:].strip()
elif aLine.startswith("### ") and spLevel >= 3: elif aLine.startswith("### ") and spLevel >= 3:
onLine = lineNo onLine = lineNo
hLevel = 3
hLabel = aLine[4:].strip()
elif aLine.startswith("#### ") and spLevel >= 4: elif aLine.startswith("#### ") and spLevel >= 4:
onLine = lineNo 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 = QListWidgetItem()
newItem.setText(aLine.strip()) 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) self.listBox.addItem(newItem)
return True return
# END Class GuiDocSplit # END Class GuiDocSplit
+1 -1
View File
@@ -36,7 +36,7 @@ logger = logging.getLogger(__name__)
class GuiEditLabel(QDialog): class GuiEditLabel(QDialog):
def __init__(self, parent, text=""): def __init__(self, parent, text=""):
QDialog.__init__(self, parent=parent) super().__init__(parent=parent)
self.setObjectName("GuiEditLabel") self.setObjectName("GuiEditLabel")
self.setWindowTitle(self.tr("Item Label")) self.setWindowTitle(self.tr("Item Label"))
+100 -125
View File
@@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import logging import logging
import novelwriter import novelwriter
@@ -34,8 +33,7 @@ from PyQt5.QtWidgets import (
QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox
) )
from novelwriter.enum import nwAlert from novelwriter.custom import QSwitch, QConfigLayout, PagedDialog
from novelwriter.gui.custom import QSwitch, QConfigLayout, PagedDialog
from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.dialogs.quotes import GuiQuoteSelect
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -44,7 +42,7 @@ logger = logging.getLogger(__name__)
class GuiPreferences(PagedDialog): class GuiPreferences(PagedDialog):
def __init__(self, mainGui): def __init__(self, mainGui):
PagedDialog.__init__(self, mainGui) super().__init__(parent=mainGui)
logger.debug("Initialising GuiPreferences ...") logger.debug("Initialising GuiPreferences ...")
self.setObjectName("GuiPreferences") self.setObjectName("GuiPreferences")
@@ -55,13 +53,13 @@ class GuiPreferences(PagedDialog):
self.setWindowTitle(self.tr("Preferences")) self.setWindowTitle(self.tr("Preferences"))
self.tabGeneral = GuiPreferencesGeneral(self.mainGui) self.tabGeneral = GuiPreferencesGeneral(self)
self.tabProjects = GuiPreferencesProjects(self.mainGui) self.tabProjects = GuiPreferencesProjects(self)
self.tabDocs = GuiPreferencesDocuments(self.mainGui) self.tabDocs = GuiPreferencesDocuments(self)
self.tabEditor = GuiPreferencesEditor(self.mainGui) self.tabEditor = GuiPreferencesEditor(self)
self.tabSyntax = GuiPreferencesSyntax(self.mainGui) self.tabSyntax = GuiPreferencesSyntax(self)
self.tabAuto = GuiPreferencesAutomation(self.mainGui) self.tabAuto = GuiPreferencesAutomation(self)
self.tabQuote = GuiPreferencesQuotes(self.mainGui) self.tabQuote = GuiPreferencesQuotes(self)
self.addTab(self.tabGeneral, self.tr("General")) self.addTab(self.tabGeneral, self.tr("General"))
self.addTab(self.tabProjects, self.tr("Projects")) self.addTab(self.tabProjects, self.tr("Projects"))
@@ -76,12 +74,38 @@ class GuiPreferences(PagedDialog):
self.buttonBox.rejected.connect(self._doClose) self.buttonBox.rejected.connect(self._doClose)
self.addControls(self.buttonBox) self.addControls(self.buttonBox)
self.resize(*self.mainConf.getPreferencesSize()) self.resize(*self.mainConf.preferencesWinSize)
# Settings
self._updateTheme = False
self._updateSyntax = False
self._needsRestart = False
self._refreshTree = False
logger.debug("GuiPreferences initialisation complete") logger.debug("GuiPreferences initialisation complete")
return return
##
# Properties
##
@property
def updateTheme(self):
return self._updateTheme
@property
def updateSyntax(self):
return self._updateSyntax
@property
def needsRestart(self):
return self._needsRestart
@property
def refreshTree(self):
return self._refreshTree
## ##
# Slots # Slots
## ##
@@ -92,8 +116,7 @@ class GuiPreferences(PagedDialog):
""" """
logger.debug("Saving new preferences") logger.debug("Saving new preferences")
needsRestart, refreshTree = self.tabGeneral.saveValues() self.tabGeneral.saveValues()
self.tabProjects.saveValues() self.tabProjects.saveValues()
self.tabDocs.saveValues() self.tabDocs.saveValues()
self.tabEditor.saveValues() self.tabEditor.saveValues()
@@ -101,15 +124,8 @@ class GuiPreferences(PagedDialog):
self.tabAuto.saveValues() self.tabAuto.saveValues()
self.tabQuote.saveValues() self.tabQuote.saveValues()
if needsRestart:
self.mainGui.makeAlert(self.tr(
"Some changes will not be applied until novelWriter has been restarted."
), nwAlert.INFO)
if refreshTree:
self.mainGui.projView.populateTree()
self._saveWindowSize() self._saveWindowSize()
self.mainConf.saveConfig()
self.accept() self.accept()
return return
@@ -128,9 +144,7 @@ class GuiPreferences(PagedDialog):
def _saveWindowSize(self): def _saveWindowSize(self):
"""Save the dialog window size. """Save the dialog window size.
""" """
winWidth = self.mainConf.rpxInt(self.width()) self.mainConf.setPreferencesWinSize(self.width(), self.height())
winHeight = self.mainConf.rpxInt(self.height())
self.mainConf.setPreferencesSize(winWidth, winHeight)
return return
# END Class GuiPreferences # END Class GuiPreferences
@@ -138,12 +152,13 @@ class GuiPreferences(PagedDialog):
class GuiPreferencesGeneral(QWidget): class GuiPreferencesGeneral(QWidget):
def __init__(self, mainGui): def __init__(self, prefsGui):
QWidget.__init__(self, mainGui) super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui self.prefsGui = prefsGui
self.mainTheme = mainGui.mainTheme self.mainGui = prefsGui.mainGui
self.mainTheme = prefsGui.mainGui.mainTheme
# The Form # The Form
self.mainForm = QConfigLayout() self.mainForm = QConfigLayout()
@@ -156,19 +171,19 @@ class GuiPreferencesGeneral(QWidget):
minWidth = self.mainConf.pxInt(200) minWidth = self.mainConf.pxInt(200)
# Select Locale # Select Locale
self.guiLang = QComboBox() self.guiLocale = QComboBox()
self.guiLang.setMinimumWidth(minWidth) self.guiLocale.setMinimumWidth(minWidth)
theLangs = self.mainConf.listLanguages(self.mainConf.LANG_NW) theLangs = self.mainConf.listLanguages(self.mainConf.LANG_NW)
for lang, langName in theLangs: for lang, langName in theLangs:
self.guiLang.addItem(langName, lang) self.guiLocale.addItem(langName, lang)
langIdx = self.guiLang.findData(self.mainConf.guiLang) langIdx = self.guiLocale.findData(self.mainConf.guiLocale)
if langIdx != -1: if langIdx != -1:
self.guiLang.setCurrentIndex(langIdx) self.guiLocale.setCurrentIndex(langIdx)
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Main GUI language"), self.tr("Main GUI language"),
self.guiLang, self.guiLocale,
self.tr("Requires restart.") self.tr("Requires restart to take effect.")
) )
# Select Theme # Select Theme
@@ -184,23 +199,7 @@ class GuiPreferencesGeneral(QWidget):
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Main GUI theme"), self.tr("Main GUI theme"),
self.guiTheme, self.guiTheme,
self.tr("Requires restart.") self.tr("General colour theme and icons.")
)
# Select Icon Theme
self.guiIcons = QComboBox()
self.guiIcons.setMinimumWidth(minWidth)
self.iconCache = self.mainTheme.iconCache.listThemes()
for iconDir, iconName in self.iconCache:
self.guiIcons.addItem(iconName, iconDir)
iconIdx = self.guiIcons.findData(self.mainConf.guiIcons)
if iconIdx != -1:
self.guiIcons.setCurrentIndex(iconIdx)
self.mainForm.addRow(
self.tr("Main icon theme"),
self.guiIcons,
self.tr("Requires restart.")
) )
# Editor Theme # Editor Theme
@@ -230,7 +229,7 @@ class GuiPreferencesGeneral(QWidget):
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Font family"), self.tr("Font family"),
self.guiFont, self.guiFont,
self.tr("Requires restart."), self.tr("Requires restart to take effect."),
theButton=self.fontButton theButton=self.fontButton
) )
@@ -243,7 +242,7 @@ class GuiPreferencesGeneral(QWidget):
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Font size"), self.tr("Font size"),
self.guiFontSize, self.guiFontSize,
self.tr("Requires restart."), self.tr("Requires restart to take effect."),
theUnit=self.tr("pt") theUnit=self.tr("pt")
) )
@@ -288,29 +287,23 @@ class GuiPreferencesGeneral(QWidget):
def saveValues(self): def saveValues(self):
"""Save the values set for this tab. """Save the values set for this tab.
""" """
guiLang = self.guiLang.currentData() guiLocale = self.guiLocale.currentData()
guiTheme = self.guiTheme.currentData() guiTheme = self.guiTheme.currentData()
guiIcons = self.guiIcons.currentData()
guiSyntax = self.guiSyntax.currentData() guiSyntax = self.guiSyntax.currentData()
guiFont = self.guiFont.text() guiFont = self.guiFont.text()
guiFontSize = self.guiFontSize.value() guiFontSize = self.guiFontSize.value()
emphLabels = self.emphLabels.isChecked() emphLabels = self.emphLabels.isChecked()
# Check if restart is needed # Update Flags
needsRestart = False self.prefsGui._updateTheme |= self.mainConf.guiTheme != guiTheme
needsRestart |= self.mainConf.guiLang != guiLang self.prefsGui._updateSyntax |= self.mainConf.guiSyntax != guiSyntax
needsRestart |= self.mainConf.guiTheme != guiTheme self.prefsGui._needsRestart |= self.mainConf.guiLocale != guiLocale
needsRestart |= self.mainConf.guiIcons != guiIcons self.prefsGui._needsRestart |= self.mainConf.guiFont != guiFont
needsRestart |= self.mainConf.guiFont != guiFont self.prefsGui._needsRestart |= self.mainConf.guiFontSize != guiFontSize
needsRestart |= self.mainConf.guiFontSize != guiFontSize self.prefsGui._refreshTree |= self.mainConf.emphLabels != emphLabels
# Check if refreshing project tree is needed self.mainConf.guiLocale = guiLocale
refreshTree = False
refreshTree |= self.mainConf.emphLabels != emphLabels
self.mainConf.guiLang = guiLang
self.mainConf.guiTheme = guiTheme self.mainConf.guiTheme = guiTheme
self.mainConf.guiIcons = guiIcons
self.mainConf.guiSyntax = guiSyntax self.mainConf.guiSyntax = guiSyntax
self.mainConf.guiFont = guiFont self.mainConf.guiFont = guiFont
self.mainConf.guiFontSize = guiFontSize self.mainConf.guiFontSize = guiFontSize
@@ -319,9 +312,7 @@ class GuiPreferencesGeneral(QWidget):
self.mainConf.hideVScroll = self.hideVScroll.isChecked() self.mainConf.hideVScroll = self.hideVScroll.isChecked()
self.mainConf.hideHScroll = self.hideHScroll.isChecked() self.mainConf.hideHScroll = self.hideHScroll.isChecked()
self.mainConf.confChanged = True return
return needsRestart, refreshTree
## ##
# Slots # Slots
@@ -344,12 +335,12 @@ class GuiPreferencesGeneral(QWidget):
class GuiPreferencesProjects(QWidget): class GuiPreferencesProjects(QWidget):
def __init__(self, mainGui): def __init__(self, prefsGui):
QWidget.__init__(self, mainGui) super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui self.mainGui = prefsGui.mainGui
self.mainTheme = mainGui.mainTheme self.mainTheme = prefsGui.mainGui.mainTheme
# The Form # The Form
self.mainForm = QConfigLayout() self.mainForm = QConfigLayout()
@@ -391,7 +382,7 @@ class GuiPreferencesProjects(QWidget):
self.mainForm.addGroupLabel(self.tr("Project Backup")) self.mainForm.addGroupLabel(self.tr("Project Backup"))
# Backup Path # Backup Path
self.backupPath = self.mainConf.backupPath self.backupPath = self.mainConf.backupPath()
self.backupGetPath = QPushButton(self.tr("Browse")) self.backupGetPath = QPushButton(self.tr("Browse"))
self.backupGetPath.clicked.connect(self._backupFolder) self.backupGetPath.clicked.connect(self._backupFolder)
self.backupPathRow = self.mainForm.addRow( self.backupPathRow = self.mainForm.addRow(
@@ -458,7 +449,7 @@ class GuiPreferencesProjects(QWidget):
self.mainConf.autoSaveProj = self.autoSaveProj.value() self.mainConf.autoSaveProj = self.autoSaveProj.value()
# Project Backup # Project Backup
self.mainConf.backupPath = self.backupPath self.mainConf.setBackupPath(self.backupPath)
self.mainConf.backupOnClose = self.backupOnClose.isChecked() self.mainConf.backupOnClose = self.backupOnClose.isChecked()
self.mainConf.askBeforeBackup = self.askBeforeBackup.isChecked() self.mainConf.askBeforeBackup = self.askBeforeBackup.isChecked()
@@ -466,8 +457,6 @@ class GuiPreferencesProjects(QWidget):
self.mainConf.stopWhenIdle = self.stopWhenIdle.isChecked() self.mainConf.stopWhenIdle = self.stopWhenIdle.isChecked()
self.mainConf.userIdleTime = round(self.userIdleTime.value() * 60) self.mainConf.userIdleTime = round(self.userIdleTime.value() * 60)
self.mainConf.confChanged = True
return return
## ##
@@ -477,12 +466,9 @@ class GuiPreferencesProjects(QWidget):
def _backupFolder(self): def _backupFolder(self):
"""Open a dialog to select the backup folder. """Open a dialog to select the backup folder.
""" """
currDir = self.backupPath currDir = self.backupPath or ""
if not os.path.isdir(currDir):
currDir = ""
newDir = QFileDialog.getExistingDirectory( newDir = QFileDialog.getExistingDirectory(
self, self.tr("Backup Directory"), currDir, options=QFileDialog.ShowDirsOnly self, self.tr("Backup Directory"), str(currDir), options=QFileDialog.ShowDirsOnly
) )
if newDir: if newDir:
self.backupPath = newDir self.backupPath = newDir
@@ -505,12 +491,12 @@ class GuiPreferencesProjects(QWidget):
class GuiPreferencesDocuments(QWidget): class GuiPreferencesDocuments(QWidget):
def __init__(self, mainGui): def __init__(self, prefsGui):
QWidget.__init__(self, mainGui) super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui self.mainGui = prefsGui.mainGui
self.mainTheme = mainGui.mainTheme self.mainTheme = prefsGui.mainGui.mainTheme
# The Form # The Form
self.mainForm = QConfigLayout() self.mainForm = QConfigLayout()
@@ -640,8 +626,6 @@ class GuiPreferencesDocuments(QWidget):
self.mainConf.textMargin = self.textMargin.value() self.mainConf.textMargin = self.textMargin.value()
self.mainConf.tabWidth = self.tabWidth.value() self.mainConf.tabWidth = self.tabWidth.value()
self.mainConf.confChanged = True
return return
## ##
@@ -666,12 +650,12 @@ class GuiPreferencesDocuments(QWidget):
class GuiPreferencesEditor(QWidget): class GuiPreferencesEditor(QWidget):
def __init__(self, mainGui): def __init__(self, prefsGui):
QWidget.__init__(self, mainGui) super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui self.mainGui = prefsGui.mainGui
self.mainTheme = mainGui.mainTheme self.mainTheme = prefsGui.mainGui.mainTheme
# The Form # The Form
self.mainForm = QConfigLayout() self.mainForm = QConfigLayout()
@@ -831,8 +815,6 @@ class GuiPreferencesEditor(QWidget):
self.mainConf.autoScroll = self.autoScroll.isChecked() self.mainConf.autoScroll = self.autoScroll.isChecked()
self.mainConf.autoScrollPos = self.autoScrollPos.value() self.mainConf.autoScrollPos = self.autoScrollPos.value()
self.mainConf.confChanged = True
return return
# END Class GuiPreferencesEditor # END Class GuiPreferencesEditor
@@ -840,12 +822,12 @@ class GuiPreferencesEditor(QWidget):
class GuiPreferencesSyntax(QWidget): class GuiPreferencesSyntax(QWidget):
def __init__(self, mainGui): def __init__(self, prefsGui):
QWidget.__init__(self, mainGui) super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui self.mainGui = prefsGui.mainGui
self.mainTheme = mainGui.mainTheme self.mainTheme = prefsGui.mainGui.mainTheme
# The Form # The Form
self.mainForm = QConfigLayout() self.mainForm = QConfigLayout()
@@ -922,8 +904,6 @@ class GuiPreferencesSyntax(QWidget):
# Text Errors # Text Errors
self.mainConf.showMultiSpaces = self.showMultiSpaces.isChecked() self.mainConf.showMultiSpaces = self.showMultiSpaces.isChecked()
self.mainConf.confChanged = True
return return
## ##
@@ -943,12 +923,12 @@ class GuiPreferencesSyntax(QWidget):
class GuiPreferencesAutomation(QWidget): class GuiPreferencesAutomation(QWidget):
def __init__(self, mainGui): def __init__(self, prefsGui):
QWidget.__init__(self, mainGui) super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui self.mainGui = prefsGui.mainGui
self.mainTheme = mainGui.mainTheme self.mainTheme = prefsGui.mainGui.mainTheme
# The Form # The Form
self.mainForm = QConfigLayout() self.mainForm = QConfigLayout()
@@ -1076,8 +1056,6 @@ class GuiPreferencesAutomation(QWidget):
self.mainConf.fmtPadAfter = self.fmtPadAfter.text().strip() self.mainConf.fmtPadAfter = self.fmtPadAfter.text().strip()
self.mainConf.fmtPadThin = self.fmtPadThin.isChecked() self.mainConf.fmtPadThin = self.fmtPadThin.isChecked()
self.mainConf.confChanged = True
return return
## ##
@@ -1100,12 +1078,12 @@ class GuiPreferencesAutomation(QWidget):
class GuiPreferencesQuotes(QWidget): class GuiPreferencesQuotes(QWidget):
def __init__(self, mainGui): def __init__(self, prefsGui):
QWidget.__init__(self, mainGui) super().__init__(parent=prefsGui)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui self.mainGui = prefsGui.mainGui
self.mainTheme = mainGui.mainTheme self.mainTheme = prefsGui.mainGui.mainTheme
# The Form # The Form
self.mainForm = QConfigLayout() self.mainForm = QConfigLayout()
@@ -1126,7 +1104,7 @@ class GuiPreferencesQuotes(QWidget):
self.quoteSym["SO"].setReadOnly(True) self.quoteSym["SO"].setReadOnly(True)
self.quoteSym["SO"].setFixedWidth(qWidth) self.quoteSym["SO"].setFixedWidth(qWidth)
self.quoteSym["SO"].setAlignment(Qt.AlignCenter) self.quoteSym["SO"].setAlignment(Qt.AlignCenter)
self.quoteSym["SO"].setText(self.mainConf.fmtSingleQuotes[0]) self.quoteSym["SO"].setText(self.mainConf.fmtSQuoteOpen)
self.btnSingleStyleO = QPushButton("...") self.btnSingleStyleO = QPushButton("...")
self.btnSingleStyleO.setMaximumWidth(bWidth) self.btnSingleStyleO.setMaximumWidth(bWidth)
self.btnSingleStyleO.clicked.connect(lambda: self._getQuote("SO")) self.btnSingleStyleO.clicked.connect(lambda: self._getQuote("SO"))
@@ -1142,7 +1120,7 @@ class GuiPreferencesQuotes(QWidget):
self.quoteSym["SC"].setReadOnly(True) self.quoteSym["SC"].setReadOnly(True)
self.quoteSym["SC"].setFixedWidth(qWidth) self.quoteSym["SC"].setFixedWidth(qWidth)
self.quoteSym["SC"].setAlignment(Qt.AlignCenter) self.quoteSym["SC"].setAlignment(Qt.AlignCenter)
self.quoteSym["SC"].setText(self.mainConf.fmtSingleQuotes[1]) self.quoteSym["SC"].setText(self.mainConf.fmtSQuoteClose)
self.btnSingleStyleC = QPushButton("...") self.btnSingleStyleC = QPushButton("...")
self.btnSingleStyleC.setMaximumWidth(bWidth) self.btnSingleStyleC.setMaximumWidth(bWidth)
self.btnSingleStyleC.clicked.connect(lambda: self._getQuote("SC")) self.btnSingleStyleC.clicked.connect(lambda: self._getQuote("SC"))
@@ -1159,7 +1137,7 @@ class GuiPreferencesQuotes(QWidget):
self.quoteSym["DO"].setReadOnly(True) self.quoteSym["DO"].setReadOnly(True)
self.quoteSym["DO"].setFixedWidth(qWidth) self.quoteSym["DO"].setFixedWidth(qWidth)
self.quoteSym["DO"].setAlignment(Qt.AlignCenter) self.quoteSym["DO"].setAlignment(Qt.AlignCenter)
self.quoteSym["DO"].setText(self.mainConf.fmtDoubleQuotes[0]) self.quoteSym["DO"].setText(self.mainConf.fmtDQuoteOpen)
self.btnDoubleStyleO = QPushButton("...") self.btnDoubleStyleO = QPushButton("...")
self.btnDoubleStyleO.setMaximumWidth(bWidth) self.btnDoubleStyleO.setMaximumWidth(bWidth)
self.btnDoubleStyleO.clicked.connect(lambda: self._getQuote("DO")) self.btnDoubleStyleO.clicked.connect(lambda: self._getQuote("DO"))
@@ -1175,7 +1153,7 @@ class GuiPreferencesQuotes(QWidget):
self.quoteSym["DC"].setReadOnly(True) self.quoteSym["DC"].setReadOnly(True)
self.quoteSym["DC"].setFixedWidth(qWidth) self.quoteSym["DC"].setFixedWidth(qWidth)
self.quoteSym["DC"].setAlignment(Qt.AlignCenter) self.quoteSym["DC"].setAlignment(Qt.AlignCenter)
self.quoteSym["DC"].setText(self.mainConf.fmtDoubleQuotes[1]) self.quoteSym["DC"].setText(self.mainConf.fmtDQuoteClose)
self.btnDoubleStyleC = QPushButton("...") self.btnDoubleStyleC = QPushButton("...")
self.btnDoubleStyleC.setMaximumWidth(bWidth) self.btnDoubleStyleC.setMaximumWidth(bWidth)
self.btnDoubleStyleC.clicked.connect(lambda: self._getQuote("DC")) self.btnDoubleStyleC.clicked.connect(lambda: self._getQuote("DC"))
@@ -1192,13 +1170,10 @@ class GuiPreferencesQuotes(QWidget):
"""Save the values set for this tab. """Save the values set for this tab.
""" """
# Quotation Style # Quotation Style
self.mainConf.fmtSingleQuotes[0] = self.quoteSym["SO"].text() self.mainConf.fmtSQuoteOpen = self.quoteSym["SO"].text()
self.mainConf.fmtSingleQuotes[1] = self.quoteSym["SC"].text() self.mainConf.fmtSQuoteClose = self.quoteSym["SC"].text()
self.mainConf.fmtDoubleQuotes[0] = self.quoteSym["DO"].text() self.mainConf.fmtDQuoteOpen = self.quoteSym["DO"].text()
self.mainConf.fmtDoubleQuotes[1] = self.quoteSym["DC"].text() self.mainConf.fmtDQuoteClose = self.quoteSym["DC"].text()
self.mainConf.confChanged = True
return return
## ##
+67 -20
View File
@@ -27,16 +27,18 @@ import math
import logging import logging
import novelwriter import novelwriter
from PyQt5.QtCore import Qt, QSize from PyQt5.QtCore import Qt, QSize, pyqtSlot
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QWidget, QDialogButtonBox, QVBoxLayout, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QComboBox, QDialogButtonBox, QGridLayout, QHBoxLayout,
QLabel, QSpinBox, QGridLayout, QHBoxLayout, QLineEdit, QAbstractItemView QLabel, QLineEdit, QSpinBox, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
QWidget
) )
from novelwriter.enum import nwItemClass
from novelwriter.common import numberToRoman from novelwriter.common import numberToRoman
from novelwriter.constants import nwUnicode from novelwriter.custom import PagedDialog, QSwitch
from novelwriter.gui.custom import PagedDialog, QSwitch from novelwriter.constants import nwLabels, nwUnicode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -44,7 +46,7 @@ logger = logging.getLogger(__name__)
class GuiProjectDetails(PagedDialog): class GuiProjectDetails(PagedDialog):
def __init__(self, mainGui): def __init__(self, mainGui):
PagedDialog.__init__(self, mainGui) super().__init__(parent=mainGui)
logger.debug("Initialising GuiProjectDetails ...") logger.debug("Initialising GuiProjectDetails ...")
self.setObjectName("GuiProjectDetails") self.setObjectName("GuiProjectDetails")
@@ -140,7 +142,7 @@ class GuiProjectDetails(PagedDialog):
class GuiProjectDetailsMain(QWidget): class GuiProjectDetailsMain(QWidget):
def __init__(self, mainGui, theProject): def __init__(self, mainGui, theProject):
QWidget.__init__(self, mainGui) super().__init__(parent=mainGui)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.theProject = theProject self.theProject = theProject
@@ -155,7 +157,7 @@ class GuiProjectDetailsMain(QWidget):
# Header # Header
# ====== # ======
self.bookTitle = QLabel(self.theProject.bookTitle) self.bookTitle = QLabel(self.theProject.data.title)
bookFont = self.bookTitle.font() bookFont = self.bookTitle.font()
bookFont.setPointSizeF(2.2*fPt) bookFont.setPointSizeF(2.2*fPt)
bookFont.setWeight(QFont.Bold) bookFont.setWeight(QFont.Bold)
@@ -164,7 +166,7 @@ class GuiProjectDetailsMain(QWidget):
self.bookTitle.setWordWrap(True) self.bookTitle.setWordWrap(True)
self.projName = QLabel( self.projName = QLabel(
self.tr("Working Title: {0}").format(self.theProject.projName) self.tr("Working Title: {0}").format(self.theProject.data.name)
) )
workFont = self.projName.font() workFont = self.projName.font()
workFont.setPointSizeF(0.8*fPt) workFont.setPointSizeF(0.8*fPt)
@@ -173,7 +175,9 @@ class GuiProjectDetailsMain(QWidget):
self.projName.setAlignment(Qt.AlignHCenter) self.projName.setAlignment(Qt.AlignHCenter)
self.projName.setWordWrap(True) self.projName.setWordWrap(True)
self.bookAuthors = QLabel(self.tr("By {0}").format(self.theProject.getAuthors())) self.bookAuthors = QLabel(self.tr("By {0}").format(
self.theProject.getFormattedAuthors()
))
authFont = self.bookAuthors.font() authFont = self.bookAuthors.font()
authFont.setPointSizeF(1.2*fPt) authFont.setPointSizeF(1.2*fPt)
self.bookAuthors.setFont(authFont) self.bookAuthors.setFont(authFont)
@@ -253,10 +257,10 @@ class GuiProjectDetailsMain(QWidget):
self.wordCountVal.setText(f"{nwCount:n}") self.wordCountVal.setText(f"{nwCount:n}")
self.chapCountVal.setText(f"{hCounts[2]:n}") self.chapCountVal.setText(f"{hCounts[2]:n}")
self.sceneCountVal.setText(f"{hCounts[3]:n}") self.sceneCountVal.setText(f"{hCounts[3]:n}")
self.revCountVal.setText(f"{self.theProject.saveCount:n}") self.revCountVal.setText(f"{self.theProject.data.saveCount:n}")
self.editTimeVal.setText(f"{edTime//3600:02d}:{edTime%3600//60:02d}") self.editTimeVal.setText(f"{edTime//3600:02d}:{edTime%3600//60:02d}")
self.projPathVal.setText(self.theProject.projPath) self.projPathVal.setText(str(self.theProject.storage.storagePath))
return return
@@ -272,7 +276,7 @@ class GuiProjectDetailsContents(QWidget):
C_PROG = 4 C_PROG = 4
def __init__(self, mainGui, theProject): def __init__(self, mainGui, theProject):
QWidget.__init__(self, mainGui) super().__init__(parent=mainGui)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.theProject = theProject self.theProject = theProject
@@ -281,12 +285,26 @@ class GuiProjectDetailsContents(QWidget):
# Internal # Internal
self._theToC = [] self._theToC = []
self._currentRoot = None
iPx = self.mainTheme.baseIconSize iPx = self.mainTheme.baseIconSize
hPx = self.mainConf.pxInt(12) hPx = self.mainConf.pxInt(12)
vPx = self.mainConf.pxInt(4) vPx = self.mainConf.pxInt(4)
pOptions = self.theProject.options 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 # Contents Tree
# ============= # =============
@@ -389,7 +407,7 @@ class GuiProjectDetailsContents(QWidget):
# ======== # ========
self.outerBox = QVBoxLayout() 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.addWidget(self.tocTree)
self.outerBox.addLayout(self.optionsBox) self.outerBox.addLayout(self.optionsBox)
@@ -412,19 +430,35 @@ class GuiProjectDetailsContents(QWidget):
def updateValues(self): def updateValues(self):
"""Populate the tree. """Populate the tree.
""" """
self._prepareData() self._currentRoot = None
self._populateNovelList()
rootHandle = self.novelValue.currentData()
self._prepareData(rootHandle)
self._populateTree() self._populateTree()
return return
## ##
# Internal Functions # Internal Functions
## ##
def _prepareData(self): def _populateNovelList(self):
"""Extract the data for the tree. """Fill the novel combo box with a list of all novel folders.
""" """
self._theToC = [] self.novelValue.clear()
self._theToC = self.theProject.index.getTableOfContents(2)
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)) self._theToC.append(("", 0, self.tr("END"), 0))
return return
@@ -432,6 +466,18 @@ class GuiProjectDetailsContents(QWidget):
# Slots # 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): def _populateTree(self):
"""Set the content of the chapter/page tree. """Set the content of the chapter/page tree.
""" """
@@ -466,10 +512,11 @@ class GuiProjectDetailsContents(QWidget):
progPage = f"{cPage:n}" progPage = f"{cPage:n}"
progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%" progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%"
hDec = self.mainTheme.getHeaderDecoration(tLevel)
if tTitle.strip() == "": if tTitle.strip() == "":
tTitle = self.tr("Untitled") 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_TITLE, tTitle)
newItem.setText(self.C_WORDS, f"{wCount:n}") newItem.setText(self.C_WORDS, f"{wCount:n}")
newItem.setText(self.C_PAGES, f"{pCount:n}") newItem.setText(self.C_PAGES, f"{pCount:n}")
+16 -27
View File
@@ -23,10 +23,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import logging import logging
import novelwriter import novelwriter
from pathlib import Path
from datetime import datetime from datetime import datetime
from PyQt5.QtGui import QKeySequence from PyQt5.QtGui import QKeySequence
@@ -54,7 +54,7 @@ class GuiProjectLoad(QDialog):
C_TIME = 2 C_TIME = 2
def __init__(self, mainGui): def __init__(self, mainGui):
QDialog.__init__(self, mainGui) super().__init__(parent=mainGui)
logger.debug("Initialising GuiProjectLoad ...") logger.debug("Initialising GuiProjectLoad ...")
self.setObjectName("GuiProjectLoad") self.setObjectName("GuiProjectLoad")
@@ -77,7 +77,6 @@ class GuiProjectLoad(QDialog):
self.setWindowTitle(self.tr("Open Project")) self.setWindowTitle(self.tr("Open Project"))
self.setMinimumWidth(self.mainConf.pxInt(650)) self.setMinimumWidth(self.mainConf.pxInt(650))
self.setMinimumHeight(self.mainConf.pxInt(400)) self.setMinimumHeight(self.mainConf.pxInt(400))
self.setModal(True)
self.nwIcon = QLabel() self.nwIcon = QLabel()
self.nwIcon.setPixmap(self.mainGui.mainTheme.getPixmap("novelwriter", (nPx, nPx))) self.nwIcon.setPixmap(self.mainGui.mainTheme.getPixmap("novelwriter", (nPx, nPx)))
@@ -158,7 +157,6 @@ class GuiProjectLoad(QDialog):
def _doOpenRecent(self): def _doOpenRecent(self):
"""Close the dialog window with a recent project selected. """Close the dialog window with a recent project selected.
""" """
logger.verbose("GuiProjectLoad open button clicked")
self._saveSettings() self._saveSettings()
self.openPath = None self.openPath = None
@@ -183,7 +181,6 @@ class GuiProjectLoad(QDialog):
def _doBrowse(self): def _doBrowse(self):
"""Browse for a folder path. """Browse for a folder path.
""" """
logger.verbose("GuiProjectLoad browse button clicked")
extFilter = [ extFilter = [
self.tr("novelWriter Project File ({0})").format(nwFiles.PROJ_FILE), self.tr("novelWriter Project File ({0})").format(nwFiles.PROJ_FILE),
self.tr("All files ({0})").format("*"), self.tr("All files ({0})").format("*"),
@@ -192,8 +189,8 @@ class GuiProjectLoad(QDialog):
self, self.tr("Open Project"), "", filter=";;".join(extFilter) self, self.tr("Open Project"), "", filter=";;".join(extFilter)
) )
if projFile: if projFile:
thePath = os.path.abspath(os.path.dirname(projFile)) thePath = Path(projFile).absolute()
self.selPath.setText(thePath) self.selPath.setText(str(thePath))
self.openPath = thePath self.openPath = thePath
self.openState = self.OPEN_STATE self.openState = self.OPEN_STATE
self.accept() self.accept()
@@ -203,7 +200,6 @@ class GuiProjectLoad(QDialog):
def _doCancel(self): def _doCancel(self):
"""Close the dialog window without doing anything. """Close the dialog window without doing anything.
""" """
logger.verbose("GuiProjectLoad close button clicked")
self.openPath = None self.openPath = None
self.openState = self.NONE_STATE self.openState = self.NONE_STATE
self.close() self.close()
@@ -212,7 +208,6 @@ class GuiProjectLoad(QDialog):
def _doNewProject(self): def _doNewProject(self):
"""Create a new project. """Create a new project.
""" """
logger.verbose("GuiProjectLoad new project button clicked")
self._saveSettings() self._saveSettings()
self.openPath = None self.openPath = None
self.openState = self.NEW_STATE self.openState = self.NEW_STATE
@@ -233,7 +228,7 @@ class GuiProjectLoad(QDialog):
).format(projName) ).format(projName)
) )
if msgYes: if msgYes:
self.mainConf.removeFromRecentCache( self.mainConf.recentProjects.remove(
selList[0].data(self.C_NAME, Qt.UserRole) selList[0].data(self.C_NAME, Qt.UserRole)
) )
self._populateList() self._populateList()
@@ -262,29 +257,23 @@ class GuiProjectLoad(QDialog):
colWidths[self.C_NAME] = self.listBox.columnWidth(self.C_NAME) colWidths[self.C_NAME] = self.listBox.columnWidth(self.C_NAME)
colWidths[self.C_COUNT] = self.listBox.columnWidth(self.C_COUNT) colWidths[self.C_COUNT] = self.listBox.columnWidth(self.C_COUNT)
colWidths[self.C_TIME] = self.listBox.columnWidth(self.C_TIME) colWidths[self.C_TIME] = self.listBox.columnWidth(self.C_TIME)
self.mainConf.setProjColWidths(colWidths) self.mainConf.setProjLoadColWidths(colWidths)
return return
def _populateList(self): def _populateList(self):
"""Populate the list box with recent project data. """Populate the list box with recent project data.
""" """
dataList = []
for projPath in self.mainConf.recentProj:
theEntry = self.mainConf.recentProj[projPath]
theTitle = theEntry.get("title", "")
theTime = theEntry.get("time", 0)
theWords = theEntry.get("words", 0)
dataList.append([theTitle, theTime, theWords, projPath])
self.listBox.clear() self.listBox.clear()
sortList = sorted(dataList, key=lambda x: x[1], reverse=True) dataList = self.mainConf.recentProjects.listEntries()
for theTitle, theTime, theWords, projPath in sortList: sortList = sorted(dataList, key=lambda x: x[3], reverse=True)
nwxIcon = self.mainGui.mainTheme.getIcon("proj_nwx")
for path, title, words, time in sortList:
newItem = QTreeWidgetItem([""]*4) newItem = QTreeWidgetItem([""]*4)
newItem.setIcon(self.C_NAME, self.mainGui.mainTheme.getIcon("proj_nwx")) newItem.setIcon(self.C_NAME, nwxIcon)
newItem.setText(self.C_NAME, theTitle) newItem.setText(self.C_NAME, title)
newItem.setData(self.C_NAME, Qt.UserRole, projPath) newItem.setData(self.C_NAME, Qt.UserRole, path)
newItem.setText(self.C_COUNT, formatInt(theWords)) newItem.setText(self.C_COUNT, formatInt(words))
newItem.setText(self.C_TIME, datetime.fromtimestamp(theTime).strftime("%x %X")) newItem.setText(self.C_TIME, datetime.fromtimestamp(time).strftime("%x %X"))
newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter) newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter)
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter) newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter)
newItem.setTextAlignment(self.C_TIME, Qt.AlignRight | Qt.AlignVCenter) newItem.setTextAlignment(self.C_TIME, Qt.AlignRight | Qt.AlignVCenter)
@@ -294,7 +283,7 @@ class GuiProjectLoad(QDialog):
if self.listBox.topLevelItemCount() > 0: if self.listBox.topLevelItemCount() > 0:
self.listBox.topLevelItem(0).setSelected(True) self.listBox.topLevelItem(0).setSelected(True)
projColWidth = self.mainConf.getProjColWidths() projColWidth = self.mainConf.projLoadColWidths
if len(projColWidth) == 3: if len(projColWidth) == 3:
self.listBox.setColumnWidth(self.C_NAME, projColWidth[self.C_NAME]) self.listBox.setColumnWidth(self.C_NAME, projColWidth[self.C_NAME])
self.listBox.setColumnWidth(self.C_COUNT, projColWidth[self.C_COUNT]) self.listBox.setColumnWidth(self.C_COUNT, projColWidth[self.C_COUNT])
+75 -49
View File
@@ -36,15 +36,20 @@ from PyQt5.QtWidgets import (
from novelwriter.enum import nwAlert from novelwriter.enum import nwAlert
from novelwriter.common import simplified from novelwriter.common import simplified
from novelwriter.gui.custom import QSwitch, PagedDialog, QConfigLayout from novelwriter.custom import QSwitch, PagedDialog, QConfigLayout
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GuiProjectSettings(PagedDialog): class GuiProjectSettings(PagedDialog):
def __init__(self, mainGui): TAB_MAIN = 0
PagedDialog.__init__(self, mainGui) TAB_STATUS = 1
TAB_IMPORT = 2
TAB_REPLACE = 3
def __init__(self, mainGui, focusTab=TAB_MAIN):
super().__init__(parent=mainGui)
logger.debug("Initialising GuiProjectSettings ...") logger.debug("Initialising GuiProjectSettings ...")
self.setObjectName("GuiProjectSettings") self.setObjectName("GuiProjectSettings")
@@ -67,10 +72,10 @@ class GuiProjectSettings(PagedDialog):
self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH)) self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH))
) )
self.tabMain = GuiProjectEditMain(self.mainGui, self.theProject) self.tabMain = GuiProjectEditMain(self)
self.tabStatus = GuiProjectEditStatus(self.mainGui, self.theProject, True) self.tabStatus = GuiProjectEditStatus(self, True)
self.tabImport = GuiProjectEditStatus(self.mainGui, self.theProject, False) self.tabImport = GuiProjectEditStatus(self, False)
self.tabReplace = GuiProjectEditReplace(self.mainGui, self.theProject) self.tabReplace = GuiProjectEditReplace(self)
self.addTab(self.tabMain, self.tr("Settings")) self.addTab(self.tabMain, self.tr("Settings"))
self.addTab(self.tabStatus, self.tr("Status")) self.addTab(self.tabStatus, self.tr("Status"))
@@ -83,12 +88,19 @@ class GuiProjectSettings(PagedDialog):
self.addControls(self.buttonBox) self.addControls(self.buttonBox)
# Flags # Flags
self.spellChanged = False self._spellChanged = False
# Focus Tab
self._focusTab(focusTab)
logger.debug("GuiProjectSettings initialisation complete") logger.debug("GuiProjectSettings initialisation complete")
return return
@property
def spellChanged(self):
return self._spellChanged
## ##
# Slots # Slots
## ##
@@ -96,21 +108,19 @@ class GuiProjectSettings(PagedDialog):
def _doSave(self): def _doSave(self):
"""Save settings and close dialog. """Save settings and close dialog.
""" """
logger.verbose("GuiProjectSettings save button clicked")
projName = self.tabMain.editName.text() projName = self.tabMain.editName.text()
bookTitle = self.tabMain.editTitle.text() bookTitle = self.tabMain.editTitle.text()
bookAuthors = self.tabMain.editAuthors.toPlainText() bookAuthors = self.tabMain.editAuthors.toPlainText()
spellLang = self.tabMain.spellLang.currentData() spellLang = self.tabMain.spellLang.currentData()
doBackup = not self.tabMain.doBackup.isChecked() doBackup = not self.tabMain.doBackup.isChecked()
self.theProject.setProjectName(projName) self.theProject.data.setName(projName)
self.theProject.setBookTitle(bookTitle) self.theProject.data.setTitle(bookTitle)
self.theProject.setBookAuthors(bookAuthors) self.theProject.data.setAuthors(bookAuthors)
self.theProject.setProjBackup(doBackup) self.theProject.data.setDoBackup(doBackup)
# Remember this as updating spell dictionary can be expensive # Remember this as updating spell dictionary can be expensive
self.spellChanged = self.theProject.setSpellLang(spellLang) self._spellChanged = self.theProject.data.setSpellLang(spellLang)
if self.tabStatus.colChanged: if self.tabStatus.colChanged:
newList, delList = self.tabStatus.getNewList() newList, delList = self.tabStatus.getNewList()
@@ -125,7 +135,7 @@ class GuiProjectSettings(PagedDialog):
if self.tabReplace.arChanged: if self.tabReplace.arChanged:
newList = self.tabReplace.getNewList() newList = self.tabReplace.getNewList()
self.theProject.setAutoReplace(newList) self.theProject.data.setAutoReplace(newList)
self._saveGuiSettings() self._saveGuiSettings()
self.accept() self.accept()
@@ -143,6 +153,19 @@ class GuiProjectSettings(PagedDialog):
# Internal Functions # Internal Functions
## ##
def _focusTab(self, tab):
"""Change which is the focused tab.
"""
if tab == self.TAB_MAIN:
self.setCurrentWidget(self.tabMain)
elif tab == self.TAB_STATUS:
self.setCurrentWidget(self.tabStatus)
elif tab == self.TAB_IMPORT:
self.setCurrentWidget(self.tabImport)
elif tab == self.TAB_REPLACE:
self.setCurrentWidget(self.tabReplace)
return
def _saveGuiSettings(self): def _saveGuiSettings(self):
"""Save GUI settings. """Save GUI settings.
""" """
@@ -166,12 +189,12 @@ class GuiProjectSettings(PagedDialog):
class GuiProjectEditMain(QWidget): class GuiProjectEditMain(QWidget):
def __init__(self, mainGui, theProject): def __init__(self, projGui):
QWidget.__init__(self, mainGui) super().__init__(parent=projGui)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui self.mainGui = projGui.mainGui
self.theProject = theProject self.theProject = projGui.theProject
# The Form # The Form
self.mainForm = QConfigLayout() self.mainForm = QConfigLayout()
@@ -186,7 +209,7 @@ class GuiProjectEditMain(QWidget):
self.editName = QLineEdit() self.editName = QLineEdit()
self.editName.setMaxLength(200) self.editName.setMaxLength(200)
self.editName.setMaximumWidth(xW) self.editName.setMaximumWidth(xW)
self.editName.setText(self.theProject.projName) self.editName.setText(self.theProject.data.name)
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Project name"), self.tr("Project name"),
self.editName, self.editName,
@@ -196,7 +219,7 @@ class GuiProjectEditMain(QWidget):
self.editTitle = QLineEdit() self.editTitle = QLineEdit()
self.editTitle.setMaxLength(200) self.editTitle.setMaxLength(200)
self.editTitle.setMaximumWidth(xW) self.editTitle.setMaximumWidth(xW)
self.editTitle.setText(self.theProject.bookTitle) self.editTitle.setText(self.theProject.data.title)
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Novel title"), self.tr("Novel title"),
self.editTitle, self.editTitle,
@@ -206,7 +229,7 @@ class GuiProjectEditMain(QWidget):
self.editAuthors = QPlainTextEdit() self.editAuthors = QPlainTextEdit()
self.editAuthors.setMaximumHeight(xH) self.editAuthors.setMaximumHeight(xH)
self.editAuthors.setMaximumWidth(xW) self.editAuthors.setMaximumWidth(xW)
self.editAuthors.setPlainText("\n".join(self.theProject.bookAuthors)) self.editAuthors.setPlainText("\n".join(self.theProject.data.authors))
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Author(s)"), self.tr("Author(s)"),
self.editAuthors, self.editAuthors,
@@ -229,13 +252,13 @@ class GuiProjectEditMain(QWidget):
) )
spellIdx = 0 spellIdx = 0
if self.theProject.projSpell is not None: if self.theProject.data.spellLang is not None:
spellIdx = self.spellLang.findData(self.theProject.projSpell) spellIdx = self.spellLang.findData(self.theProject.data.spellLang)
if spellIdx != -1: if spellIdx != -1:
self.spellLang.setCurrentIndex(spellIdx) self.spellLang.setCurrentIndex(spellIdx)
self.doBackup = QSwitch(self) self.doBackup = QSwitch(self)
self.doBackup.setChecked(not self.theProject.doBackup) self.doBackup.setChecked(not self.theProject.data.doBackup)
self.mainForm.addRow( self.mainForm.addRow(
self.tr("No backup on close"), self.tr("No backup on close"),
self.doBackup, self.doBackup,
@@ -256,20 +279,20 @@ class GuiProjectEditStatus(QWidget):
COL_ROLE = Qt.UserRole + 1 COL_ROLE = Qt.UserRole + 1
NUM_ROLE = Qt.UserRole + 2 NUM_ROLE = Qt.UserRole + 2
def __init__(self, mainGui, theProject, isStatus): def __init__(self, projGui, isStatus):
QWidget.__init__(self, mainGui) super().__init__(parent=projGui)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui self.mainGui = projGui.mainGui
self.theProject = theProject self.theProject = projGui.theProject
self.mainTheme = mainGui.mainTheme self.mainTheme = projGui.mainGui.mainTheme
if isStatus: if isStatus:
self.theStatus = self.theProject.statusItems self.theStatus = self.theProject.data.itemStatus
pageLabel = self.tr("Novel File Status Levels") pageLabel = self.tr("Novel File Status Levels")
colSetting = "statusColW" colSetting = "statusColW"
else: else:
self.theStatus = self.theProject.importItems self.theStatus = self.theProject.data.itemImport
pageLabel = self.tr("Note File Importance Levels") pageLabel = self.tr("Note File Importance Levels")
colSetting = "importColW" colSetting = "importColW"
@@ -367,11 +390,12 @@ class GuiProjectEditStatus(QWidget):
newList = [] newList = []
for n in range(self.listBox.topLevelItemCount()): for n in range(self.listBox.topLevelItemCount()):
item = self.listBox.topLevelItem(n) item = self.listBox.topLevelItem(n)
newList.append({ if item is not None:
"key": item.data(self.COL_LABEL, self.KEY_ROLE), newList.append({
"name": item.text(self.COL_LABEL), "key": item.data(self.COL_LABEL, self.KEY_ROLE),
"cols": item.data(self.COL_LABEL, self.COL_ROLE), "name": item.text(self.COL_LABEL),
}) "cols": item.data(self.COL_LABEL, self.COL_ROLE),
})
return newList, self.colDeleted return newList, self.colDeleted
return [], [] return [], []
@@ -470,7 +494,8 @@ class GuiProjectEditStatus(QWidget):
self.listBox.insertTopLevelItem(nIndex, cItem) self.listBox.insertTopLevelItem(nIndex, cItem)
self.listBox.clearSelection() self.listBox.clearSelection()
cItem.setSelected(True) if cItem is not None:
cItem.setSelected(True)
self.colChanged = True self.colChanged = True
return return
@@ -527,13 +552,13 @@ class GuiProjectEditReplace(QWidget):
COL_KEY = 0 COL_KEY = 0
COL_REPL = 1 COL_REPL = 1
def __init__(self, mainGui, theProject): def __init__(self, projGui):
QWidget.__init__(self, mainGui) super().__init__(parent=projGui)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui self.mainGui = projGui.mainGui
self.mainTheme = mainGui.mainTheme self.mainTheme = projGui.mainGui.mainTheme
self.theProject = theProject self.theProject = projGui.theProject
self.arChanged = False self.arChanged = False
wCol0 = self.mainConf.pxInt( wCol0 = self.mainConf.pxInt(
@@ -553,7 +578,7 @@ class GuiProjectEditReplace(QWidget):
self.listBox.setColumnWidth(self.COL_KEY, wCol0) self.listBox.setColumnWidth(self.COL_KEY, wCol0)
self.listBox.setIndentation(0) self.listBox.setIndentation(0)
for aKey, aVal in self.theProject.autoReplace.items(): for aKey, aVal in self.theProject.data.autoReplace.items():
newItem = QTreeWidgetItem(["<%s>" % aKey, aVal]) newItem = QTreeWidgetItem(["<%s>" % aKey, aVal])
self.listBox.addTopLevelItem(newItem) self.listBox.addTopLevelItem(newItem)
@@ -619,10 +644,11 @@ class GuiProjectEditReplace(QWidget):
newList = {} newList = {}
for n in range(self.listBox.topLevelItemCount()): for n in range(self.listBox.topLevelItemCount()):
tItem = self.listBox.topLevelItem(n) tItem = self.listBox.topLevelItem(n)
aKey = self._stripNotAllowed(tItem.text(0)) if tItem is not None:
aVal = tItem.text(1) aKey = self._stripNotAllowed(tItem.text(0))
if len(aKey) > 0: aVal = tItem.text(1)
newList[aKey] = aVal if len(aKey) > 0:
newList[aKey] = aVal
return newList return newList
+1 -1
View File
@@ -43,7 +43,7 @@ class GuiQuoteSelect(QDialog):
selectedQuote = "" selectedQuote = ""
def __init__(self, parent=None, currentQuote='"'): def __init__(self, parent=None, currentQuote='"'):
QDialog.__init__(self, parent=parent) super().__init__(parent=parent)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
+2 -2
View File
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
class GuiUpdates(QDialog): class GuiUpdates(QDialog):
def __init__(self, mainGui): def __init__(self, mainGui):
QDialog.__init__(self, mainGui) super().__init__(parent=mainGui)
logger.debug("Initialising GuiUpdates ...") logger.debug("Initialising GuiUpdates ...")
self.setObjectName("GuiUpdates") self.setObjectName("GuiUpdates")
@@ -135,7 +135,7 @@ class GuiUpdates(QDialog):
logException() logException()
relVersion = rawData.get("tag_name", "Unknown") relVersion = rawData.get("tag_name", "Unknown")
relDate = rawData.get("created_at", None) relDate = rawData.get("created_at", "")
try: try:
relDate = datetime.strptime(relDate[:10], "%Y-%m-%d").strftime("%x") relDate = datetime.strptime(relDate[:10], "%Y-%m-%d").strftime("%x")
+17 -11
View File
@@ -23,10 +23,11 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os
import logging import logging
import novelwriter import novelwriter
from pathlib import Path
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget, QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget,
@@ -43,7 +44,7 @@ logger = logging.getLogger(__name__)
class GuiWordList(QDialog): class GuiWordList(QDialog):
def __init__(self, mainGui): def __init__(self, mainGui):
QDialog.__init__(self, mainGui) super().__init__(parent=mainGui)
logger.debug("Initialising GuiWordList ...") logger.debug("Initialising GuiWordList ...")
self.setObjectName("GuiWordList") self.setObjectName("GuiWordList")
@@ -150,13 +151,19 @@ class GuiWordList(QDialog):
""" """
self._saveGuiSettings() self._saveGuiSettings()
dctFile = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT) dctFile = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT)
tmpFile = dctFile + "~" if not isinstance(dctFile, Path):
return False
tmpFile = dctFile.with_suffix(".tmp")
try: try:
with open(tmpFile, mode="w", encoding="utf-8") as outFile: with open(tmpFile, mode="w", encoding="utf-8") as outFile:
for i in range(self.listBox.count()): 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")
tmpFile.replace(dctFile)
except Exception: except Exception:
logger.error("Could not save new word list") logger.error("Could not save new word list")
@@ -164,9 +171,6 @@ class GuiWordList(QDialog):
self.reject() self.reject()
return False return False
if os.path.isfile(dctFile):
os.unlink(dctFile)
os.rename(tmpFile, dctFile)
self.accept() self.accept()
return True return True
@@ -185,10 +189,12 @@ class GuiWordList(QDialog):
def _loadWordList(self): def _loadWordList(self):
"""Load the project's word list, if it exists. """Load the project's word list, if it exists.
""" """
self.listBox.clear() wordList = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT)
if not isinstance(wordList, Path):
return False
wordList = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT) self.listBox.clear()
if not os.path.isfile(wordList): if not wordList.exists():
logger.debug("No project dictionary file found") logger.debug("No project dictionary file found")
return False return False
+4 -3
View File
@@ -112,9 +112,10 @@ class nwDocInsert(Enum):
QUOTE_RS = 2 QUOTE_RS = 2
QUOTE_LD = 3 QUOTE_LD = 3
QUOTE_RD = 4 QUOTE_RD = 4
NEW_PAGE = 5 SYNOPSIS = 5
VSPACE_S = 6 NEW_PAGE = 6
VSPACE_M = 7 VSPACE_S = 7
VSPACE_M = 8
# END Enum nwDocInsert # END Enum nwDocInsert
+6 -5
View File
@@ -44,7 +44,8 @@ def logException():
"""Log the content of an exception message. """Log the content of an exception message.
""" """
exType, exValue, _ = sys.exc_info() 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): def formatException(exc):
@@ -61,7 +62,7 @@ def formatException(exc):
class NWErrorMessage(QDialog): class NWErrorMessage(QDialog):
def __init__(self, parent): def __init__(self, parent):
QDialog.__init__(self, parent=parent) super().__init__(parent=parent)
self.setObjectName("NWErrorMessage") self.setObjectName("NWErrorMessage")
# Widgets # Widgets
@@ -131,7 +132,7 @@ class NWErrorMessage(QDialog):
try: try:
import lxml import lxml
lxmlVersion = lxml.__version__ lxmlVersion = lxml.__version__ # type: ignore
except Exception: except Exception:
lxmlVersion = "Unknown" lxmlVersion = "Unknown"
@@ -198,8 +199,8 @@ def exceptionHandler(exType, exValue, exTrace):
try: try:
# Try a controlled shutdown # Try a controlled shutdown
nwGUI.closeProject(isYes=True) nwGUI.closeProject(isYes=True) # type: ignore
nwGUI.closeMain() nwGUI.closeMain() # type: ignore
logger.info("Emergency shutdown successful") logger.info("Emergency shutdown successful")
except Exception as exc: except Exception as exc:
File diff suppressed because it is too large Load Diff
+8 -6
View File
@@ -47,7 +47,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
BLOCK_TITLE = 4 BLOCK_TITLE = 4
def __init__(self, theDoc, mainGui, spEnchant): def __init__(self, theDoc, mainGui, spEnchant):
QSyntaxHighlighter.__init__(self, theDoc) super().__init__(theDoc)
logger.debug("Initialising GuiDocHighlighter ...") logger.debug("Initialising GuiDocHighlighter ...")
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
@@ -151,11 +151,13 @@ class GuiDocHighlighter(QSyntaxHighlighter):
# Quoted Strings # Quoted Strings
if self.mainConf.highlightQuotes: if self.mainConf.highlightQuotes:
fmtDbl = self.mainConf.fmtDoubleQuotes fmtDblO = self.mainConf.fmtDQuoteOpen
fmtSng = self.mainConf.fmtSingleQuotes fmtDblC = self.mainConf.fmtDQuoteClose
fmtSngO = self.mainConf.fmtSQuoteOpen
fmtSngC = self.mainConf.fmtSQuoteClose
# Straight Quotes # Straight Quotes
if fmtDbl != ["\"", "\""]: if not (fmtDblO == fmtDblC == "\""):
self.hRules.append(( self.hRules.append((
"(\\B\")(.*?)(\"\\B)", { "(\\B\")(.*?)(\"\\B)", {
0: self.hStyles["dialogue1"], 0: self.hStyles["dialogue1"],
@@ -165,7 +167,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
# Double Quotes # Double Quotes
dblEnd = "|$" if self.mainConf.allowOpenDQuote else "" dblEnd = "|$" if self.mainConf.allowOpenDQuote else ""
self.hRules.append(( self.hRules.append((
f"(\\B{fmtDbl[0]})(.*?)({fmtDbl[1]}\\B{dblEnd})", { f"(\\B{fmtDblO})(.*?)({fmtDblC}\\B{dblEnd})", {
0: self.hStyles["dialogue2"], 0: self.hStyles["dialogue2"],
} }
)) ))
@@ -173,7 +175,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
# Single Quotes # Single Quotes
sngEnd = "|$" if self.mainConf.allowOpenSQuote else "" sngEnd = "|$" if self.mainConf.allowOpenSQuote else ""
self.hRules.append(( self.hRules.append((
f"(\\B{fmtSng[0]})(.*?)({fmtSng[1]}\\B{sngEnd})", { f"(\\B{fmtSngO})(.*?)({fmtSngC}\\B{sngEnd})", {
0: self.hStyles["dialogue3"], 0: self.hStyles["dialogue3"],
} }
)) ))
+103 -73
View File
@@ -54,7 +54,7 @@ class GuiDocViewer(QTextBrowser):
loadDocumentTagRequest = pyqtSignal(str, Enum) loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, mainGui): def __init__(self, mainGui):
QTextBrowser.__init__(self, mainGui) super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocViewer ...") logger.debug("Initialising GuiDocViewer ...")
@@ -102,6 +102,13 @@ class GuiDocViewer(QTextBrowser):
self.docHeader.setTitleFromHandle(self._docHandle) self.docHeader.setTitleFromHandle(self._docHandle)
return True return True
def updateTheme(self):
"""Update theme elements.
"""
self.docHeader.updateTheme()
self.docFooter.updateTheme()
return
def initViewer(self): def initViewer(self):
"""Set editor settings from main config. """Set editor settings from main config.
""" """
@@ -150,10 +157,7 @@ class GuiDocViewer(QTextBrowser):
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
# Refresh the tab stops # Refresh the tab stops
if self.mainConf.verQtValue >= 51000: self.setTabStopDistance(self.mainConf.getTabWidth())
self.setTabStopDistance(self.mainConf.getTabWidth())
else:
self.setTabStopWidth(self.mainConf.getTabWidth())
# If we have a document open, we should reload it in case the font changed # If we have a document open, we should reload it in case the font changed
if self._docHandle is not None: if self._docHandle is not None:
@@ -193,10 +197,7 @@ class GuiDocViewer(QTextBrowser):
return False return False
# Refresh the tab stops # Refresh the tab stops
if self.mainConf.verQtValue >= 51000: self.setTabStopDistance(self.mainConf.getTabWidth())
self.setTabStopDistance(self.mainConf.getTabWidth())
else:
self.setTabStopWidth(self.mainConf.getTabWidth())
# Must be before setHtml # Must be before setHtml
if updateHistory: if updateHistory:
@@ -216,7 +217,7 @@ class GuiDocViewer(QTextBrowser):
self.verticalScrollBar().setValue(sPos) self.verticalScrollBar().setValue(sPos)
self._docHandle = tHandle self._docHandle = tHandle
self.theProject.setLastViewed(tHandle) self.theProject._data.setLastHandle(tHandle, "viewer")
self.docHeader.setTitleFromHandle(self._docHandle) self.docHeader.setTitleFromHandle(self._docHandle)
self.updateDocMargins() self.updateDocMargins()
@@ -247,7 +248,7 @@ class GuiDocViewer(QTextBrowser):
"""Wrapper function for various document actions on the current """Wrapper function for various document actions on the current
document. document.
""" """
logger.verbose("Requesting action: '%s'", theAction.name) logger.debug("Requesting action: '%s'", theAction.name)
if self._docHandle is None: if self._docHandle is None:
logger.error("No document open") logger.error("No document open")
return False return False
@@ -270,7 +271,7 @@ class GuiDocViewer(QTextBrowser):
if not isinstance(tAnchor, str): if not isinstance(tAnchor, str):
return False return False
if tAnchor.startswith("#"): if tAnchor.startswith("#"):
logger.verbose("Moving to anchor '%s'", tAnchor) logger.debug("Moving to anchor '%s'", tAnchor)
self.setSource(QUrl(tAnchor)) self.setSource(QUrl(tAnchor))
return True return True
@@ -356,7 +357,7 @@ class GuiDocViewer(QTextBrowser):
theBlock = self.document().findBlockByLineNumber(theLine) theBlock = self.document().findBlockByLineNumber(theLine)
if theBlock: if theBlock:
self.setCursorPosition(theBlock.position()) self.setCursorPosition(theBlock.position())
logger.verbose("Cursor moved to line %d", theLine) logger.debug("Cursor moved to line %d", theLine)
return True return True
def setScrollPosition(self, thePos): def setScrollPosition(self, thePos):
@@ -402,7 +403,7 @@ class GuiDocViewer(QTextBrowser):
"""Process a clicked link internally in the document. """Process a clicked link internally in the document.
""" """
theLink = theURL.url() theLink = theURL.url()
logger.verbose("Clicked link: '%s'", theLink) logger.debug("Clicked link: '%s'", theLink)
if len(theLink) > 0: if len(theLink) > 0:
theBits = theLink.split("=") theBits = theLink.split("=")
if len(theBits) == 2: if len(theBits) == 2:
@@ -461,7 +462,7 @@ class GuiDocViewer(QTextBrowser):
has its margins adjusted according to user preferences. has its margins adjusted according to user preferences.
""" """
self.updateDocMargins() self.updateDocMargins()
QTextBrowser.resizeEvent(self, theEvent) super().resizeEvent(theEvent)
return return
def mouseReleaseEvent(self, theEvent): def mouseReleaseEvent(self, theEvent):
@@ -472,7 +473,7 @@ class GuiDocViewer(QTextBrowser):
elif theEvent.button() == Qt.ForwardButton: elif theEvent.button() == Qt.ForwardButton:
self.navForward() self.navForward()
else: else:
QTextBrowser.mouseReleaseEvent(self, theEvent) super().mouseReleaseEvent(theEvent)
return return
## ##
@@ -568,7 +569,7 @@ class GuiDocViewer(QTextBrowser):
# END Class GuiDocViewer # END Class GuiDocViewer
class GuiDocViewHistory(): class GuiDocViewHistory:
def __init__(self, docViewer): def __init__(self, docViewer):
@@ -584,7 +585,7 @@ class GuiDocViewHistory():
def clear(self): def clear(self):
"""Clear the view history. """Clear the view history.
""" """
logger.verbose("View history cleared") logger.debug("View history cleared")
self._navHistory = [] self._navHistory = []
self._posHistory = [] self._posHistory = []
self._currPos = -1 self._currPos = -1
@@ -598,7 +599,7 @@ class GuiDocViewHistory():
""" """
if self._currPos >= 0 and self._currPos < len(self._navHistory): if self._currPos >= 0 and self._currPos < len(self._navHistory):
if tHandle == self._navHistory[self._currPos]: if tHandle == self._navHistory[self._currPos]:
logger.verbose("Not updating view hsitory") logger.debug("Not updating view hsitory")
return False return False
self._truncateHistory(self._currPos) self._truncateHistory(self._currPos)
@@ -613,7 +614,7 @@ class GuiDocViewHistory():
self._dumpHistory() self._dumpHistory()
logger.verbose("Added '%s' to view history", tHandle) logger.debug("Added '%s' to view history", tHandle)
return True return True
@@ -622,7 +623,7 @@ class GuiDocViewHistory():
""" """
newPos = self._currPos + 1 newPos = self._currPos + 1
if newPos < len(self._navHistory): if newPos < len(self._navHistory):
logger.verbose("Move forward in view history") logger.debug("Move forward in view history")
self._prevPos = self._currPos self._prevPos = self._currPos
self._updateScrollBar() self._updateScrollBar()
@@ -640,7 +641,7 @@ class GuiDocViewHistory():
""" """
newPos = self._currPos - 1 newPos = self._currPos - 1
if newPos >= 0: if newPos >= 0:
logger.verbose("Move backward in view history") logger.debug("Move backward in view history")
self._prevPos = self._currPos self._prevPos = self._currPos
self._updateScrollBar() self._updateScrollBar()
@@ -686,11 +687,11 @@ class GuiDocViewHistory():
def _dumpHistory(self): def _dumpHistory(self):
"""Debug function to dump history to the logger. Since it is a """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)): for i, (h, p) in enumerate(zip(self._navHistory, self._posHistory)):
logger.verbose( logger.debug(
"History %02d: %s %13s [x:%d]" % ( "History %02d: %s %13s [x:%d]" % (
i + 1, ">" if i == self._currPos else " ", h, p i + 1, ">" if i == self._currPos else " ", h, p
) )
@@ -708,7 +709,7 @@ class GuiDocViewHistory():
class GuiDocViewHeader(QWidget): class GuiDocViewHeader(QWidget):
def __init__(self, docViewer): def __init__(self, docViewer):
QWidget.__init__(self, docViewer) super().__init__(parent=docViewer)
logger.debug("Initialising GuiDocViewHeader ...") logger.debug("Initialising GuiDocViewHeader ...")
@@ -741,51 +742,38 @@ class GuiDocViewHeader(QWidget):
lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize) lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize)
self.theTitle.setFont(lblFont) self.theTitle.setFont(lblFont)
buttonStyle = (
"QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
).format(*self.mainTheme.colText)
# Buttons # Buttons
self.backButton = QToolButton(self) self.backButton = QToolButton(self)
self.backButton.setIcon(self.mainTheme.getIcon("backward"))
self.backButton.setContentsMargins(0, 0, 0, 0) self.backButton.setContentsMargins(0, 0, 0, 0)
self.backButton.setIconSize(QSize(fPx, fPx)) self.backButton.setIconSize(QSize(fPx, fPx))
self.backButton.setFixedSize(fPx, fPx) self.backButton.setFixedSize(fPx, fPx)
self.backButton.setStyleSheet(buttonStyle)
self.backButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.backButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.backButton.setVisible(False) self.backButton.setVisible(False)
self.backButton.setToolTip(self.tr("Go backward")) self.backButton.setToolTip(self.tr("Go backward"))
self.backButton.clicked.connect(self.docViewer.navBackward) self.backButton.clicked.connect(self.docViewer.navBackward)
self.forwardButton = QToolButton(self) self.forwardButton = QToolButton(self)
self.forwardButton.setIcon(self.mainTheme.getIcon("forward"))
self.forwardButton.setContentsMargins(0, 0, 0, 0) self.forwardButton.setContentsMargins(0, 0, 0, 0)
self.forwardButton.setIconSize(QSize(fPx, fPx)) self.forwardButton.setIconSize(QSize(fPx, fPx))
self.forwardButton.setFixedSize(fPx, fPx) self.forwardButton.setFixedSize(fPx, fPx)
self.forwardButton.setStyleSheet(buttonStyle)
self.forwardButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.forwardButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.forwardButton.setVisible(False) self.forwardButton.setVisible(False)
self.forwardButton.setToolTip(self.tr("Go forward")) self.forwardButton.setToolTip(self.tr("Go forward"))
self.forwardButton.clicked.connect(self.docViewer.navForward) self.forwardButton.clicked.connect(self.docViewer.navForward)
self.refreshButton = QToolButton(self) self.refreshButton = QToolButton(self)
self.refreshButton.setIcon(self.mainTheme.getIcon("refresh"))
self.refreshButton.setContentsMargins(0, 0, 0, 0) self.refreshButton.setContentsMargins(0, 0, 0, 0)
self.refreshButton.setIconSize(QSize(fPx, fPx)) self.refreshButton.setIconSize(QSize(fPx, fPx))
self.refreshButton.setFixedSize(fPx, fPx) self.refreshButton.setFixedSize(fPx, fPx)
self.refreshButton.setStyleSheet(buttonStyle)
self.refreshButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.refreshButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.refreshButton.setVisible(False) self.refreshButton.setVisible(False)
self.refreshButton.setToolTip(self.tr("Reload the document")) self.refreshButton.setToolTip(self.tr("Reload the document"))
self.refreshButton.clicked.connect(self._refreshDocument) self.refreshButton.clicked.connect(self._refreshDocument)
self.closeButton = QToolButton(self) self.closeButton = QToolButton(self)
self.closeButton.setIcon(self.mainTheme.getIcon("close"))
self.closeButton.setContentsMargins(0, 0, 0, 0) self.closeButton.setContentsMargins(0, 0, 0, 0)
self.closeButton.setIconSize(QSize(fPx, fPx)) self.closeButton.setIconSize(QSize(fPx, fPx))
self.closeButton.setFixedSize(fPx, fPx) self.closeButton.setFixedSize(fPx, fPx)
self.closeButton.setStyleSheet(buttonStyle)
self.closeButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.closeButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.closeButton.setVisible(False) self.closeButton.setVisible(False)
self.closeButton.setToolTip(self.tr("Close the document")) self.closeButton.setToolTip(self.tr("Close the document"))
@@ -809,7 +797,7 @@ class GuiDocViewHeader(QWidget):
self.setMinimumHeight(fPx + 2*cM) self.setMinimumHeight(fPx + 2*cM)
# Fix the Colours # Fix the Colours
self.matchColours() self.updateTheme()
logger.debug("GuiDocViewHeader initialisation complete") logger.debug("GuiDocViewHeader initialisation complete")
@@ -819,6 +807,28 @@ class GuiDocViewHeader(QWidget):
# Methods # Methods
## ##
def updateTheme(self):
"""Update theme elements.
"""
self.backButton.setIcon(self.mainTheme.getIcon("backward"))
self.forwardButton.setIcon(self.mainTheme.getIcon("forward"))
self.refreshButton.setIcon(self.mainTheme.getIcon("refresh"))
self.closeButton.setIcon(self.mainTheme.getIcon("close"))
buttonStyle = (
"QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
).format(*self.mainTheme.colText)
self.backButton.setStyleSheet(buttonStyle)
self.forwardButton.setStyleSheet(buttonStyle)
self.refreshButton.setStyleSheet(buttonStyle)
self.closeButton.setStyleSheet(buttonStyle)
self.matchColours()
return
def matchColours(self): def matchColours(self):
"""Update the colours of the widget to match those of the syntax """Update the colours of the widget to match those of the syntax
theme rather than the main GUI. theme rather than the main GUI.
@@ -879,12 +889,14 @@ class GuiDocViewHeader(QWidget):
# Slots # Slots
## ##
@pyqtSlot()
def _closeDocument(self): def _closeDocument(self):
"""Trigger the close editor/viewer on the main window. """Trigger the close editor/viewer on the main window.
""" """
self.mainGui.closeDocViewer() self.mainGui.closeDocViewer()
return return
@pyqtSlot()
def _refreshDocument(self): def _refreshDocument(self):
"""Reload the content of the document. """Reload the content of the document.
""" """
@@ -915,7 +927,7 @@ class GuiDocViewHeader(QWidget):
class GuiDocViewFooter(QWidget): class GuiDocViewFooter(QWidget):
def __init__(self, docViewer): def __init__(self, docViewer):
QWidget.__init__(self, docViewer) super().__init__(parent=docViewer)
logger.debug("Initialising GuiDocViewFooter ...") logger.debug("Initialising GuiDocViewFooter ...")
@@ -932,33 +944,13 @@ class GuiDocViewFooter(QWidget):
bSp = self.mainConf.pxInt(2) bSp = self.mainConf.pxInt(2)
hSp = self.mainConf.pxInt(8) hSp = self.mainConf.pxInt(8)
# Icons
stickyOn = self.mainTheme.getPixmap("sticky-on", (fPx, fPx))
stickyOff = self.mainTheme.getPixmap("sticky-off", (fPx, fPx))
stickyIcon = QIcon()
stickyIcon.addPixmap(stickyOn, QIcon.Normal, QIcon.On)
stickyIcon.addPixmap(stickyOff, QIcon.Normal, QIcon.Off)
bulletOn = self.mainTheme.getPixmap("bullet-on", (fPx, fPx))
bulletOff = self.mainTheme.getPixmap("bullet-off", (fPx, fPx))
bulletIcon = QIcon()
bulletIcon.addPixmap(bulletOn, QIcon.Normal, QIcon.On)
bulletIcon.addPixmap(bulletOff, QIcon.Normal, QIcon.Off)
# Main Widget Settings # Main Widget Settings
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
self.setAutoFillBackground(True) self.setAutoFillBackground(True)
buttonStyle = (
"QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
).format(*self.mainTheme.colText)
# Show/Hide Details # Show/Hide Details
self.showHide = QToolButton(self) self.showHide = QToolButton(self)
self.showHide.setToolButtonStyle(Qt.ToolButtonIconOnly) self.showHide.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.showHide.setStyleSheet(buttonStyle)
self.showHide.setIcon(self.mainTheme.getIcon("reference"))
self.showHide.setIconSize(QSize(fPx, fPx)) self.showHide.setIconSize(QSize(fPx, fPx))
self.showHide.setFixedSize(QSize(fPx, fPx)) self.showHide.setFixedSize(QSize(fPx, fPx))
self.showHide.clicked.connect(self._doShowHide) self.showHide.clicked.connect(self._doShowHide)
@@ -968,8 +960,6 @@ class GuiDocViewFooter(QWidget):
self.stickyRefs = QToolButton(self) self.stickyRefs = QToolButton(self)
self.stickyRefs.setCheckable(True) self.stickyRefs.setCheckable(True)
self.stickyRefs.setToolButtonStyle(Qt.ToolButtonIconOnly) self.stickyRefs.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.stickyRefs.setStyleSheet(buttonStyle)
self.stickyRefs.setIcon(stickyIcon)
self.stickyRefs.setIconSize(QSize(fPx, fPx)) self.stickyRefs.setIconSize(QSize(fPx, fPx))
self.stickyRefs.setFixedSize(QSize(fPx, fPx)) self.stickyRefs.setFixedSize(QSize(fPx, fPx))
self.stickyRefs.toggled.connect(self._doToggleSticky) self.stickyRefs.toggled.connect(self._doToggleSticky)
@@ -982,8 +972,6 @@ class GuiDocViewFooter(QWidget):
self.showComments.setCheckable(True) self.showComments.setCheckable(True)
self.showComments.setChecked(self.mainConf.viewComments) self.showComments.setChecked(self.mainConf.viewComments)
self.showComments.setToolButtonStyle(Qt.ToolButtonIconOnly) self.showComments.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.showComments.setStyleSheet(buttonStyle)
self.showComments.setIcon(bulletIcon)
self.showComments.setIconSize(QSize(fPx, fPx)) self.showComments.setIconSize(QSize(fPx, fPx))
self.showComments.setFixedSize(QSize(fPx, fPx)) self.showComments.setFixedSize(QSize(fPx, fPx))
self.showComments.toggled.connect(self._doToggleComments) self.showComments.toggled.connect(self._doToggleComments)
@@ -994,8 +982,6 @@ class GuiDocViewFooter(QWidget):
self.showSynopsis.setCheckable(True) self.showSynopsis.setCheckable(True)
self.showSynopsis.setChecked(self.mainConf.viewSynopsis) self.showSynopsis.setChecked(self.mainConf.viewSynopsis)
self.showSynopsis.setToolButtonStyle(Qt.ToolButtonIconOnly) self.showSynopsis.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.showSynopsis.setStyleSheet(buttonStyle)
self.showSynopsis.setIcon(bulletIcon)
self.showSynopsis.setIconSize(QSize(fPx, fPx)) self.showSynopsis.setIconSize(QSize(fPx, fPx))
self.showSynopsis.setFixedSize(QSize(fPx, fPx)) self.showSynopsis.setFixedSize(QSize(fPx, fPx))
self.showSynopsis.toggled.connect(self._doToggleSynopsis) self.showSynopsis.toggled.connect(self._doToggleSynopsis)
@@ -1069,7 +1055,7 @@ class GuiDocViewFooter(QWidget):
self.setMinimumHeight(fPx + 2*cM) self.setMinimumHeight(fPx + 2*cM)
# Fix the Colours # Fix the Colours
self.matchColours() self.updateTheme()
logger.debug("GuiDocViewFooter initialisation complete") logger.debug("GuiDocViewFooter initialisation complete")
@@ -1079,6 +1065,46 @@ class GuiDocViewFooter(QWidget):
# Methods # Methods
## ##
def updateTheme(self):
"""Update theme elements.
"""
# Icons
fPx = int(0.9*self.mainTheme.fontPixelSize)
stickyOn = self.mainTheme.getPixmap("sticky-on", (fPx, fPx))
stickyOff = self.mainTheme.getPixmap("sticky-off", (fPx, fPx))
stickyIcon = QIcon()
stickyIcon.addPixmap(stickyOn, QIcon.Normal, QIcon.On)
stickyIcon.addPixmap(stickyOff, QIcon.Normal, QIcon.Off)
bulletOn = self.mainTheme.getPixmap("bullet-on", (fPx, fPx))
bulletOff = self.mainTheme.getPixmap("bullet-off", (fPx, fPx))
bulletIcon = QIcon()
bulletIcon.addPixmap(bulletOn, QIcon.Normal, QIcon.On)
bulletIcon.addPixmap(bulletOff, QIcon.Normal, QIcon.Off)
self.showHide.setIcon(self.mainTheme.getIcon("reference"))
self.stickyRefs.setIcon(stickyIcon)
self.showComments.setIcon(bulletIcon)
self.showSynopsis.setIcon(bulletIcon)
# StyleSheets
buttonStyle = (
"QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
).format(*self.mainTheme.colText)
self.showHide.setStyleSheet(buttonStyle)
self.stickyRefs.setStyleSheet(buttonStyle)
self.showComments.setStyleSheet(buttonStyle)
self.showSynopsis.setStyleSheet(buttonStyle)
self.matchColours()
return
def matchColours(self): def matchColours(self):
"""Update the colours of the widget to match those of the syntax """Update the colours of the widget to match those of the syntax
theme rather than the main GUI. theme rather than the main GUI.
@@ -1100,6 +1126,7 @@ class GuiDocViewFooter(QWidget):
# Slots # Slots
## ##
@pyqtSlot()
def _doShowHide(self): def _doShowHide(self):
"""Toggle the expand/collapse of the panel. """Toggle the expand/collapse of the panel.
""" """
@@ -1107,26 +1134,29 @@ class GuiDocViewFooter(QWidget):
self.viewMeta.setVisible(not isVisible) self.viewMeta.setVisible(not isVisible)
return return
@pyqtSlot(bool)
def _doToggleSticky(self, theState): def _doToggleSticky(self, theState):
"""Toggle the sticky flag for the reference panel. """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 self.docViewer.stickyRef = theState
if not theState and self.docViewer.docHandle() is not None: if not theState and self.docViewer.docHandle() is not None:
self.viewMeta.refreshReferences(self.docViewer.docHandle()) self.viewMeta.refreshReferences(self.docViewer.docHandle())
return return
@pyqtSlot(bool)
def _doToggleComments(self, theState): def _doToggleComments(self, theState):
"""Toggle the view comment button and reload the document. """Toggle the view comment button and reload the document.
""" """
self.mainConf.setViewComments(theState) self.mainConf.viewComments = theState
self.docViewer.reloadText() self.docViewer.reloadText()
return return
@pyqtSlot(bool)
def _doToggleSynopsis(self, theState): def _doToggleSynopsis(self, theState):
"""Toggle the view synopsis button and reload the document. """Toggle the view synopsis button and reload the document.
""" """
self.mainConf.setViewSynopsis(theState) self.mainConf.viewSynopsis = theState
self.docViewer.reloadText() self.docViewer.reloadText()
return return
@@ -1141,7 +1171,7 @@ class GuiDocViewFooter(QWidget):
class GuiDocViewDetails(QScrollArea): class GuiDocViewDetails(QScrollArea):
def __init__(self, mainGui): def __init__(self, mainGui):
QScrollArea.__init__(self, mainGui) super().__init__(parent=mainGui)
logger.debug("Initialising GuiDocViewDetails ...") logger.debug("Initialising GuiDocViewDetails ...")
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
@@ -1205,7 +1235,7 @@ class GuiDocViewDetails(QScrollArea):
"""Capture the link-click and forward it to the document viewer """Capture the link-click and forward it to the document viewer
class for handling. class for handling.
""" """
logger.verbose("Clicked link: '%s'", theLink) logger.debug("Clicked link: '%s'", theLink)
if len(theLink) == 21: if len(theLink) == 21:
tHandle = theLink[:13] tHandle = theLink[:13]
tAnchor = theLink[13:] tAnchor = theLink[13:]
+17 -15
View File
@@ -30,7 +30,6 @@ from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtGui import QFont, QPixmap from PyQt5.QtGui import QFont, QPixmap
from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel
from novelwriter.enum import nwItemType
from novelwriter.constants import trConst, nwLabels from novelwriter.constants import trConst, nwLabels
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -39,7 +38,7 @@ logger = logging.getLogger(__name__)
class GuiItemDetails(QWidget): class GuiItemDetails(QWidget):
def __init__(self, mainGui): def __init__(self, mainGui):
QWidget.__init__(self, mainGui) super().__init__(parent=mainGui)
logger.debug("Initialising GuiItemDetails ...") logger.debug("Initialising GuiItemDetails ...")
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
@@ -54,12 +53,8 @@ class GuiItemDetails(QWidget):
hSp = self.mainConf.pxInt(6) hSp = self.mainConf.pxInt(6)
vSp = self.mainConf.pxInt(1) vSp = self.mainConf.pxInt(1)
mPx = self.mainConf.pxInt(6) mPx = self.mainConf.pxInt(6)
iPx = self.mainTheme.baseIconSize
fPt = self.mainTheme.fontPointSize fPt = self.mainTheme.fontPointSize
self._expCheck = self.mainTheme.getPixmap("check", (iPx, iPx))
self._expCross = self.mainTheme.getPixmap("cross", (iPx, iPx))
fntLabel = QFont() fntLabel = QFont()
fntLabel.setBold(True) fntLabel.setBold(True)
fntLabel.setPointSizeF(0.9*fPt) fntLabel.setPointSizeF(0.9*fPt)
@@ -180,6 +175,8 @@ class GuiItemDetails(QWidget):
self.setLayout(self.mainBox) self.setLayout(self.mainBox)
self.updateTheme()
# Make sure the columns for flags and counts don't resize too often # Make sure the columns for flags and counts don't resize too often
flagWidth = self.mainTheme.getTextWidth("Mm", fntValue) flagWidth = self.mainTheme.getTextWidth("Mm", fntValue)
countWidth = self.mainTheme.getTextWidth("99,999", fntValue) countWidth = self.mainTheme.getTextWidth("99,999", fntValue)
@@ -220,6 +217,12 @@ class GuiItemDetails(QWidget):
""" """
self.updateViewBox(self._itemHandle) self.updateViewBox(self._itemHandle)
def updateTheme(self):
"""Update theme elements.
"""
self.updateViewBox(self._itemHandle)
return
## ##
# Public Slots # Public Slots
## ##
@@ -247,13 +250,13 @@ class GuiItemDetails(QWidget):
if len(theLabel) > 100: if len(theLabel) > 100:
theLabel = theLabel[:96].rstrip()+" ..." theLabel = theLabel[:96].rstrip()+" ..."
if nwItem.itemType == nwItemType.FILE: if nwItem.isFileType():
if nwItem.isExported: if nwItem.isActive:
self.labelIcon.setPixmap(self._expCheck) self.labelIcon.setPixmap(self.mainTheme.getPixmap("checked", (iPx, iPx)))
else: else:
self.labelIcon.setPixmap(self._expCross) self.labelIcon.setPixmap(self.mainTheme.getPixmap("unchecked", (iPx, iPx)))
else: else:
self.labelIcon.setPixmap(QPixmap(1, 1)) self.labelIcon.setPixmap(self.mainTheme.getPixmap("noncheckable", (iPx, iPx)))
self.labelData.setText(theLabel) self.labelData.setText(theLabel)
@@ -274,17 +277,16 @@ class GuiItemDetails(QWidget):
# Layout # Layout
# ====== # ======
hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
usageIcon = self.mainTheme.getItemIcon( 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.usageIcon.setPixmap(usageIcon.pixmap(iPx, iPx))
self.usageData.setText(nwItem.describeMe(hLevel)) self.usageData.setText(nwItem.describeMe())
# Counts # Counts
# ====== # ======
if nwItem.itemType == nwItemType.FILE: if nwItem.isFileType():
self.cCountData.setText(f"{nwItem.charCount:n}") self.cCountData.setText(f"{nwItem.charCount:n}")
self.wCountData.setText(f"{nwItem.wordCount:n}") self.wCountData.setText(f"{nwItem.wordCount:n}")
self.pCountData.setText(f"{nwItem.paraCount:n}") self.pCountData.setText(f"{nwItem.paraCount:n}")
+25 -28
View File
@@ -26,6 +26,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging import logging
import novelwriter import novelwriter
from pathlib import Path
from urllib.parse import urljoin from urllib.parse import urljoin
from urllib.request import pathname2url from urllib.request import pathname2url
@@ -40,9 +41,13 @@ logger = logging.getLogger(__name__)
class GuiMainMenu(QMenuBar): 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): def __init__(self, mainGui):
QMenuBar.__init__(self, mainGui) super().__init__(parent=mainGui)
logger.debug("Initialising GuiMainMenu ...") logger.debug("Initialising GuiMainMenu ...")
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
@@ -79,12 +84,6 @@ class GuiMainMenu(QMenuBar):
self.aSpellCheck.setChecked(theMode) self.aSpellCheck.setChecked(theMode)
return return
def setFocusMode(self, theMode):
"""Forward focus mode check state to its action.
"""
self.aFocusMode.setChecked(theMode)
return
## ##
# Slots # Slots
## ##
@@ -106,10 +105,11 @@ class GuiMainMenu(QMenuBar):
def _openUserManualFile(self): def _openUserManualFile(self):
"""Open the documentation in PDF format. """Open the documentation in PDF format.
""" """
if self.mainConf.pdfDocs is None: if isinstance(self.mainConf.pdfDocs, Path):
return False QDesktopServices.openUrl(
QDesktopServices.openUrl(QUrl(urljoin("file:", pathname2url(self.mainConf.pdfDocs)))) QUrl(urljoin("file:", pathname2url(str(self.mainConf.pdfDocs))))
return True )
return
## ##
# Menu Builders # Menu Builders
@@ -164,14 +164,14 @@ class GuiMainMenu(QMenuBar):
# Project > Edit # Project > Edit
self.aEditItem = QAction(self.tr("Rename Item"), self) 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.aEditItem.triggered.connect(lambda: self.mainGui.editItemLabel(None))
self.projMenu.addAction(self.aEditItem) self.projMenu.addAction(self.aEditItem)
# Project > Delete # Project > Delete
self.aDeleteItem = QAction(self.tr("Delete Item"), self) self.aDeleteItem = QAction(self.tr("Delete Item"), self)
self.aDeleteItem.setShortcut("Ctrl+Shift+Del") 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) self.projMenu.addAction(self.aDeleteItem)
# Project > Empty Trash # Project > Empty Trash
@@ -244,16 +244,6 @@ class GuiMainMenu(QMenuBar):
self.aImportFile.triggered.connect(lambda: self.mainGui.importDocument()) self.aImportFile.triggered.connect(lambda: self.mainGui.importDocument())
self.docuMenu.addAction(self.aImportFile) 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 return
def _buildEditMenu(self): def _buildEditMenu(self):
@@ -375,8 +365,6 @@ class GuiMainMenu(QMenuBar):
# View > Focus Mode # View > Focus Mode
self.aFocusMode = QAction(self.tr("Focus Mode"), self) self.aFocusMode = QAction(self.tr("Focus Mode"), self)
self.aFocusMode.setShortcut("F8") self.aFocusMode.setShortcut("F8")
self.aFocusMode.setCheckable(True)
self.aFocusMode.setChecked(self.mainGui.isFocusMode)
self.aFocusMode.triggered.connect(lambda: self.mainGui.toggleFocusMode()) self.aFocusMode.triggered.connect(lambda: self.mainGui.toggleFocusMode())
self.viewMenu.addAction(self.aFocusMode) self.viewMenu.addAction(self.aFocusMode)
@@ -568,6 +556,15 @@ class GuiMainMenu(QMenuBar):
) )
self.mInsKeywords.addAction(self.mInsKWItems[keyWord][0]) self.mInsKeywords.addAction(self.mInsKWItems[keyWord][0])
# Insert > Special Comments
self.mInsComments = self.insertMenu.addMenu(self.tr("Special Comments"))
# Insert > Synopsis Comment
self.aInsSynopsis = QAction(self.tr("Synopsis Comment"), self)
self.aInsSynopsis.setShortcut("Ctrl+K, S")
self.aInsSynopsis.triggered.connect(lambda: self._docInsert(nwDocInsert.SYNOPSIS))
self.mInsComments.addAction(self.aInsSynopsis)
# Insert > Symbols # Insert > Symbols
self.mInsBreaks = self.insertMenu.addMenu(self.tr("Page Break and Space")) self.mInsBreaks = self.insertMenu.addMenu(self.tr("Page Break and Space"))
@@ -799,7 +796,7 @@ class GuiMainMenu(QMenuBar):
# Tools > Check Spelling # Tools > Check Spelling
self.aSpellCheck = QAction(self.tr("Check Spelling"), self) self.aSpellCheck = QAction(self.tr("Check Spelling"), self)
self.aSpellCheck.setCheckable(True) self.aSpellCheck.setCheckable(True)
self.aSpellCheck.setChecked(self.theProject.spellCheck) self.aSpellCheck.setChecked(self.theProject.data.spellCheck)
self.aSpellCheck.triggered.connect(self._toggleSpellCheck) # triggered, not toggled! self.aSpellCheck.triggered.connect(self._toggleSpellCheck) # triggered, not toggled!
self.aSpellCheck.setShortcut("Ctrl+F7") self.aSpellCheck.setShortcut("Ctrl+F7")
self.toolsMenu.addAction(self.aSpellCheck) self.toolsMenu.addAction(self.aSpellCheck)
@@ -829,7 +826,7 @@ class GuiMainMenu(QMenuBar):
# Tools > Backup # Tools > Backup
self.aBackupProject = QAction(self.tr("Backup Project"), self) self.aBackupProject = QAction(self.tr("Backup Project"), self)
self.aBackupProject.triggered.connect(lambda: self.theProject.zipIt(True)) self.aBackupProject.triggered.connect(lambda: self.theProject.backupProject(True))
self.toolsMenu.addAction(self.aBackupProject) self.toolsMenu.addAction(self.aBackupProject)
# Tools > Export Project # Tools > Export Project
@@ -886,7 +883,7 @@ class GuiMainMenu(QMenuBar):
self.helpMenu.addAction(self.aHelpDocs) self.helpMenu.addAction(self.aHelpDocs)
# Help > User Manual (PDF) # Help > User Manual (PDF)
if self.mainConf.pdfDocs is not None: if isinstance(self.mainConf.pdfDocs, Path):
self.aPdfDocs = QAction(self.tr("User Manual (PDF)"), self) self.aPdfDocs = QAction(self.tr("User Manual (PDF)"), self)
self.aPdfDocs.setShortcut("Shift+F1") self.aPdfDocs.setShortcut("Shift+F1")
self.aPdfDocs.triggered.connect(self._openUserManualFile) self.aPdfDocs.triggered.connect(self._openUserManualFile)
+187 -56
View File
@@ -35,11 +35,11 @@ from PyQt5.QtGui import QPalette
from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel, QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel,
QMenu, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QMenu, QSizePolicy, QToolButton, QToolTip, QTreeWidget, QTreeWidgetItem,
QWidget QVBoxLayout, QWidget
) )
from novelwriter.enum import nwDocMode, nwItemClass from novelwriter.enum import nwDocMode, nwItemClass, nwOutline
from novelwriter.common import checkInt from novelwriter.common import checkInt
from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst
@@ -63,7 +63,7 @@ class GuiNovelView(QWidget):
openDocumentRequest = pyqtSignal(str, Enum, int, str) openDocumentRequest = pyqtSignal(str, Enum, int, str)
def __init__(self, mainGui): def __init__(self, mainGui):
QWidget.__init__(self, mainGui) super().__init__(parent=mainGui)
self.mainGui = mainGui self.mainGui = mainGui
self.theProject = mainGui.theProject self.theProject = mainGui.theProject
@@ -71,6 +71,7 @@ class GuiNovelView(QWidget):
# Build GUI # Build GUI
self.novelTree = GuiNovelTree(self) self.novelTree = GuiNovelTree(self)
self.novelBar = GuiNovelToolBar(self) self.novelBar = GuiNovelToolBar(self)
self.novelBar.setEnabled(False)
# Assemble # Assemble
self.outerBox = QVBoxLayout() self.outerBox = QVBoxLayout()
@@ -84,6 +85,7 @@ class GuiNovelView(QWidget):
# Function Mappings # Function Mappings
self.updateWordCounts = self.novelTree.updateWordCounts self.updateWordCounts = self.novelTree.updateWordCounts
self.getSelectedHandle = self.novelTree.getSelectedHandle self.getSelectedHandle = self.novelTree.getSelectedHandle
self.setActiveHandle = self.novelTree.setActiveHandle
return return
@@ -91,14 +93,24 @@ class GuiNovelView(QWidget):
# Methods # Methods
## ##
def updateTheme(self):
"""Update theme elements.
"""
self.novelBar.updateTheme()
self.novelTree.updateTheme()
self.refreshTree()
return
def initSettings(self): def initSettings(self):
"""Initialise GUI elements that depend on specific settings.
"""
self.novelTree.initSettings() self.novelTree.initSettings()
return return
def refreshTree(self): def refreshTree(self):
"""Refresh the current tree. """Refresh the current tree.
""" """
self.novelTree.refreshTree(rootHandle=self.theProject.lastNovel) self.novelTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("novelTree"))
return return
def clearProject(self): def clearProject(self):
@@ -106,12 +118,13 @@ class GuiNovelView(QWidget):
""" """
self.novelTree.clearContent() self.novelTree.clearContent()
self.novelBar.clearContent() self.novelBar.clearContent()
self.novelBar.setEnabled(False)
return return
def openProjectTasks(self): def openProjectTasks(self):
"""Run opening project tasks. """Run open project tasks.
""" """
lastNovel = self.theProject.lastNovel lastNovel = self.theProject.data.getLastHandle("novelTree")
if lastNovel not in self.theProject.tree: if lastNovel not in self.theProject.tree:
lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL) lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL)
@@ -125,6 +138,7 @@ class GuiNovelView(QWidget):
self.novelBar.buildNovelRootMenu() self.novelBar.buildNovelRootMenu()
self.novelBar.setLastColType(lastCol, doRefresh=False) self.novelBar.setLastColType(lastCol, doRefresh=False)
self.novelBar.setCurrentRoot(lastNovel) self.novelBar.setCurrentRoot(lastNovel)
self.novelBar.setEnabled(True)
return return
@@ -135,8 +149,8 @@ class GuiNovelView(QWidget):
self.theProject.options.setValue("GuiNovelView", "lastCol", lastColType) self.theProject.options.setValue("GuiNovelView", "lastCol", lastColType)
return return
def setFocus(self): def setTreeFocus(self):
"""Forward the set focus call to the tree widget. """Set the focus to the tree widget.
""" """
self.novelTree.setFocus() self.novelTree.setFocus()
return return
@@ -163,7 +177,7 @@ class GuiNovelView(QWidget):
class GuiNovelToolBar(QWidget): class GuiNovelToolBar(QWidget):
def __init__(self, novelView): def __init__(self, novelView):
QTreeWidget.__init__(self, novelView) super().__init__(parent=novelView)
logger.debug("Initialising GuiNovelToolBar ...") logger.debug("Initialising GuiNovelToolBar ...")
@@ -173,21 +187,11 @@ class GuiNovelToolBar(QWidget):
self.mainTheme = novelView.mainGui.mainTheme self.mainTheme = novelView.mainGui.mainTheme
iPx = self.mainTheme.baseIconSize iPx = self.mainTheme.baseIconSize
mPx = self.mainConf.pxInt(3) mPx = self.mainConf.pxInt(2)
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
self.setAutoFillBackground(True) self.setAutoFillBackground(True)
qPalette = self.palette()
qPalette.setBrush(QPalette.Window, qPalette.base())
self.setPalette(qPalette)
fadeCol = qPalette.text().color()
buttonStyle = (
"QToolButton {{padding: {0}px; border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
).format(mPx, fadeCol.red(), fadeCol.green(), fadeCol.blue())
# Widget Label # Widget Label
self.viewLabel = QLabel("<b>%s</b>" % self.tr("Novel Outline")) self.viewLabel = QLabel("<b>%s</b>" % self.tr("Novel Outline"))
self.viewLabel.setContentsMargins(0, 0, 0, 0) self.viewLabel.setContentsMargins(0, 0, 0, 0)
@@ -196,9 +200,7 @@ class GuiNovelToolBar(QWidget):
# Refresh Button # Refresh Button
self.tbRefresh = QToolButton(self) self.tbRefresh = QToolButton(self)
self.tbRefresh.setToolTip(self.tr("Refresh")) self.tbRefresh.setToolTip(self.tr("Refresh"))
self.tbRefresh.setIcon(self.mainTheme.getIcon("refresh"))
self.tbRefresh.setIconSize(QSize(iPx, iPx)) self.tbRefresh.setIconSize(QSize(iPx, iPx))
self.tbRefresh.setStyleSheet(buttonStyle)
self.tbRefresh.clicked.connect(self._refreshNovelTree) self.tbRefresh.clicked.connect(self._refreshNovelTree)
# Novel Root Menu # Novel Root Menu
@@ -208,9 +210,7 @@ class GuiNovelToolBar(QWidget):
self.tbRoot = QToolButton(self) self.tbRoot = QToolButton(self)
self.tbRoot.setToolTip(self.tr("Novel Root")) self.tbRoot.setToolTip(self.tr("Novel Root"))
self.tbRoot.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]))
self.tbRoot.setIconSize(QSize(iPx, iPx)) self.tbRoot.setIconSize(QSize(iPx, iPx))
self.tbRoot.setStyleSheet(buttonStyle)
self.tbRoot.setMenu(self.mRoot) self.tbRoot.setMenu(self.mRoot)
self.tbRoot.setPopupMode(QToolButton.InstantPopup) self.tbRoot.setPopupMode(QToolButton.InstantPopup)
@@ -227,9 +227,7 @@ class GuiNovelToolBar(QWidget):
self.tbMore = QToolButton(self) self.tbMore = QToolButton(self)
self.tbMore.setToolTip(self.tr("More Options")) self.tbMore.setToolTip(self.tr("More Options"))
self.tbMore.setIcon(self.mainTheme.getIcon("menu"))
self.tbMore.setIconSize(QSize(iPx, iPx)) self.tbMore.setIconSize(QSize(iPx, iPx))
self.tbMore.setStyleSheet(buttonStyle)
self.tbMore.setMenu(self.mMore) self.tbMore.setMenu(self.mMore)
self.tbMore.setPopupMode(QToolButton.InstantPopup) self.tbMore.setPopupMode(QToolButton.InstantPopup)
@@ -244,6 +242,8 @@ class GuiNovelToolBar(QWidget):
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
self.updateTheme()
logger.debug("GuiNovelToolBar initialisation complete") logger.debug("GuiNovelToolBar initialisation complete")
return return
@@ -252,6 +252,31 @@ class GuiNovelToolBar(QWidget):
# Methods # Methods
## ##
def updateTheme(self):
"""Update theme elements.
"""
# Icons
self.tbRefresh.setIcon(self.mainTheme.getIcon("refresh"))
self.tbRoot.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]))
self.tbMore.setIcon(self.mainTheme.getIcon("menu"))
qPalette = self.palette()
qPalette.setBrush(QPalette.Window, qPalette.base())
self.setPalette(qPalette)
# StyleSheets
fadeCol = qPalette.text().color()
buttonStyle = (
"QToolButton {{padding: {0}px; border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
).format(self.mainConf.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue())
self.tbRefresh.setStyleSheet(buttonStyle)
self.tbRoot.setStyleSheet(buttonStyle)
self.tbMore.setStyleSheet(buttonStyle)
return
def clearContent(self): def clearContent(self):
"""Run clearing project tasks. """Run clearing project tasks.
""" """
@@ -297,7 +322,7 @@ class GuiNovelToolBar(QWidget):
def _refreshNovelTree(self): def _refreshNovelTree(self):
"""Rebuild the current tree. """Rebuild the current tree.
""" """
rootHandle = self.theProject.lastNovel rootHandle = self.theProject.data.getLastHandle("novelTree")
self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True)
return return
@@ -322,10 +347,15 @@ class GuiNovelTree(QTreeWidget):
C_TITLE = 0 C_TITLE = 0
C_WORDS = 1 C_WORDS = 1
C_LAST = 2 C_EXTRA = 2
C_MORE = 3
D_HANDLE = Qt.UserRole
D_TITLE = Qt.UserRole + 1
D_KEY = Qt.UserRole + 2
def __init__(self, novelView): def __init__(self, novelView):
QTreeWidget.__init__(self, novelView) super().__init__(parent=novelView)
logger.debug("Initialising GuiNovelTree ...") logger.debug("Initialising GuiNovelTree ...")
@@ -339,6 +369,7 @@ class GuiNovelTree(QTreeWidget):
self._treeMap = {} self._treeMap = {}
self._lastBuild = 0 self._lastBuild = 0
self._lastCol = NovelTreeColumn.POV self._lastCol = NovelTreeColumn.POV
self._actHandle = None
# Cached Strings # Cached Strings
self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]) self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
@@ -353,9 +384,11 @@ class GuiNovelTree(QTreeWidget):
self.setIconSize(QSize(iPx, iPx)) self.setIconSize(QSize(iPx, iPx))
self.setFrameStyle(QFrame.NoFrame) self.setFrameStyle(QFrame.NoFrame)
self.setUniformRowHeights(True)
self.setAllColumnsShowFocus(True)
self.setHeaderHidden(True) self.setHeaderHidden(True)
self.setIndentation(0) self.setIndentation(0)
self.setColumnCount(3) self.setColumnCount(4)
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.SingleSelection) self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setExpandsOnDoubleClick(False) self.setExpandsOnDoubleClick(False)
@@ -367,7 +400,8 @@ class GuiNovelTree(QTreeWidget):
treeHeader.setMinimumSectionSize(iPx + cMg) treeHeader.setMinimumSectionSize(iPx + cMg)
treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.Stretch) treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.Stretch)
treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents) treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents)
treeHeader.setSectionResizeMode(self.C_LAST, QHeaderView.ResizeToContents) treeHeader.setSectionResizeMode(self.C_EXTRA, QHeaderView.ResizeToContents)
treeHeader.setSectionResizeMode(self.C_MORE, QHeaderView.ResizeToContents)
# Pre-Generate Tree Formatting # Pre-Generate Tree Formatting
fH1 = self.font() fH1 = self.font()
@@ -378,20 +412,15 @@ class GuiNovelTree(QTreeWidget):
fH2.setBold(True) fH2.setBold(True)
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()] 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),
]
# Connect signals # Connect signals
self.clicked.connect(self._treeItemClicked)
self.itemDoubleClicked.connect(self._treeDoubleClick) self.itemDoubleClicked.connect(self._treeDoubleClick)
self.itemSelectionChanged.connect(self._treeSelectionChange) self.itemSelectionChanged.connect(self._treeSelectionChange)
# Set custom settings # Set custom settings
self.initSettings() self.initSettings()
self.updateTheme()
logger.debug("GuiNovelTree initialisation complete") logger.debug("GuiNovelTree initialisation complete")
@@ -413,6 +442,13 @@ class GuiNovelTree(QTreeWidget):
return return
def updateTheme(self):
"""Update theme elements.
"""
iPx = self.mainTheme.baseIconSize
self._pMore = self.mainTheme.loadDecoration("deco_doc_more", pxH=iPx)
return
## ##
# Properties # Properties
## ##
@@ -436,23 +472,23 @@ class GuiNovelTree(QTreeWidget):
def refreshTree(self, rootHandle=None, overRide=False): def refreshTree(self, rootHandle=None, overRide=False):
"""Called whenever the Novel tab is activated. """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: if rootHandle is None:
rootHandle = self.theProject.tree.findRoot(nwItemClass.NOVEL) rootHandle = self.theProject.tree.findRoot(nwItemClass.NOVEL)
treeChanged = self.mainGui.projView.changedSince(self._lastBuild) treeChanged = self.mainGui.projView.changedSince(self._lastBuild)
indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild) indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
if not (treeChanged or indexChanged or overRide): 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 return
selItem = self.selectedItems() selItem = self.selectedItems()
titleKey = None titleKey = None
if selItem: if selItem:
titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2] titleKey = selItem[0].data(self.C_TITLE, self.D_KEY)
self._populateTree(rootHandle) self._populateTree(rootHandle)
self.theProject.setLastNovelViewed(rootHandle) self.theProject.data.setLastHandle(rootHandle, "novelTree")
if titleKey is not None and titleKey in self._treeMap: if titleKey is not None and titleKey in self._treeMap:
self._treeMap[titleKey].setSelected(True) self._treeMap[titleKey].setSelected(True)
@@ -476,8 +512,9 @@ class GuiNovelTree(QTreeWidget):
tHandle = None tHandle = None
tLine = 0 tLine = 0
if selItem: if selItem:
tHandle = selItem[0].data(self.C_TITLE, Qt.UserRole)[0] tHandle = selItem[0].data(self.C_TITLE, self.D_HANDLE)
tLine = checkInt(selItem[0].data(self.C_TITLE, Qt.UserRole)[1], 1) - 1 sTitle = selItem[0].data(self.C_TITLE, self.D_TITLE)
tLine = checkInt(sTitle[1:], 1) - 1
return tHandle, tLine return tHandle, tLine
@@ -487,9 +524,34 @@ class GuiNovelTree(QTreeWidget):
if self._lastCol != colType: if self._lastCol != colType:
logger.debug("Changing last column to %s", colType.name) logger.debug("Changing last column to %s", colType.name)
self._lastCol = colType self._lastCol = colType
self.setColumnHidden(self.C_LAST, colType == NovelTreeColumn.HIDDEN) self.setColumnHidden(self.C_EXTRA, colType == NovelTreeColumn.HIDDEN)
if doRefresh: if doRefresh:
self.refreshTree(rootHandle=self.theProject.lastNovel, overRide=True) lastNovel = self.theProject.data.getLastHandle("novelTree")
self.refreshTree(rootHandle=lastNovel, overRide=True)
return
def setActiveHandle(self, tHandle):
"""Highlight the rows associated with a given handle.
"""
tStart = time()
self._actHandle = tHandle
for i in range(self.topLevelItemCount()):
tItem = self.topLevelItem(i)
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.debug("Highlighted Novel Tree in %.3f ms", (time() - tStart)*1000)
return return
## ##
@@ -501,7 +563,7 @@ class GuiNovelTree(QTreeWidget):
mouse in a blank area of the tree view, and to load a document mouse in a blank area of the tree view, and to load a document
for viewing if the user middle-clicked. for viewing if the user middle-clicked.
""" """
QTreeWidget.mousePressEvent(self, theEvent) super().mousePressEvent(theEvent)
if theEvent.button() == Qt.LeftButton: if theEvent.button() == Qt.LeftButton:
selItem = self.indexAt(theEvent.pos()) selItem = self.indexAt(theEvent.pos())
@@ -521,10 +583,28 @@ class GuiNovelTree(QTreeWidget):
return return
def focusOutEvent(self, theEvent):
"""Clear the selection when the tree no longer has focus.
"""
super().focusOutEvent(theEvent)
self.clearSelection()
return
## ##
# Private Slots # Private Slots
## ##
@pyqtSlot("QModelIndex")
def _treeItemClicked(self, mIndex):
"""The user clicked on an item in the tree.
"""
if mIndex.column() == self.C_MORE:
tHandle = mIndex.siblingAtColumn(self.C_TITLE).data(self.D_HANDLE)
sTitle = mIndex.siblingAtColumn(self.C_TITLE).data(self.D_TITLE)
tipPos = self.mapToGlobal(self.visualRect(mIndex).topRight())
self._popMetaBox(tipPos, tHandle, sTitle)
return
@pyqtSlot() @pyqtSlot()
def _treeSelectionChange(self): def _treeSelectionChange(self):
"""Extract the handle and line number of the currently selected """Extract the handle and line number of the currently selected
@@ -554,7 +634,7 @@ class GuiNovelTree(QTreeWidget):
""" """
self.clearContent() self.clearContent()
tStart = time() 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) novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
for tKey, tHandle, sTitle, novIdx in novStruct: for tKey, tHandle, sTitle, novIdx in novStruct:
@@ -563,25 +643,31 @@ class GuiNovelTree(QTreeWidget):
if iLevel == 0: if iLevel == 0:
continue continue
newItem = QTreeWidgetItem() hDec = self.mainTheme.getHeaderDecoration(iLevel)
theData = (tHandle, sTitle[1:].lstrip("0"), tKey)
newItem.setData(self.C_TITLE, Qt.DecorationRole, self._pIndent[iLevel]) newItem = QTreeWidgetItem()
newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
newItem.setText(self.C_TITLE, novIdx.title) newItem.setText(self.C_TITLE, novIdx.title)
newItem.setData(self.C_TITLE, Qt.UserRole, theData) newItem.setData(self.C_TITLE, self.D_HANDLE, tHandle)
newItem.setData(self.C_TITLE, self.D_TITLE, sTitle)
newItem.setData(self.C_TITLE, self.D_KEY, tKey)
newItem.setFont(self.C_TITLE, self._hFonts[iLevel]) newItem.setFont(self.C_TITLE, self._hFonts[iLevel])
newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}") newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}")
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
newItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore)
# Custom column
lastText, toolTip = self._getLastColumnText(tHandle, sTitle) lastText, toolTip = self._getLastColumnText(tHandle, sTitle)
newItem.setText(self.C_LAST, lastText) newItem.setText(self.C_EXTRA, lastText)
if lastText: if lastText:
newItem.setToolTip(self.C_LAST, toolTip) newItem.setToolTip(self.C_EXTRA, toolTip)
self._treeMap[tKey] = newItem self._treeMap[tKey] = newItem
self.addTopLevelItem(newItem) self.addTopLevelItem(newItem)
logger.verbose("Novel Tree built in %.3f ms", (time() - tStart)*1000) self.setActiveHandle(self._actHandle)
logger.debug("Novel Tree built in %.3f ms", (time() - tStart)*1000)
self._lastBuild = time() self._lastBuild = time()
return return
@@ -607,4 +693,49 @@ class GuiNovelTree(QTreeWidget):
return "", "" return "", ""
def _popMetaBox(self, qPos, tHandle, sTitle):
"""Show the novel meta data box.
"""
logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle)
pIndex = self.theProject.index
novIdx = pIndex.getNovelData(tHandle, sTitle)
refTags = pIndex.getReferences(tHandle, sTitle)
synopText = novIdx.synopsis
if synopText:
synopLabel = trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP])
synopText = f"<p><b>{synopLabel}</b>: {synopText}</p>"
refLines = []
refLines = self._appendMetaTag(refTags, nwKeyWords.POV_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.FOCUS_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.CHAR_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.PLOT_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.TIME_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.WORLD_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.OBJECT_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.ENTITY_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.CUSTOM_KEY, refLines)
refText = ""
if refLines:
refList = "<br>".join(refLines)
refText = f"<p>{refList}</p>"
ttText = refText + synopText or self.tr("No meta data")
if ttText:
QToolTip.showText(qPos, ttText)
return
@staticmethod
def _appendMetaTag(refs, key, lines):
"""Generate a reference list for a given reference key.
"""
tags = ", ".join(refs.get(key, []))
if tags:
lines.append(f"<b>{trConst(nwLabels.KEY_NAME[key])}</b>: {tags}")
return lines
# END Class GuiNovelTree # END Class GuiNovelTree
+164 -137
View File
@@ -37,16 +37,16 @@ from PyQt5.QtCore import (
Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP
) )
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QAction, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QAbstractItemView, QAction, QComboBox, QFrame, QGridLayout, QGroupBox,
QMenu, QScrollArea, QSplitter, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QHBoxLayout, QLabel, QMenu, QScrollArea, QSizePolicy, QSplitter, QToolBar,
QWidget, QFrame, QToolBar, QSizePolicy, QComboBox, QToolButton QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
) )
from novelwriter.enum import ( from novelwriter.enum import (
nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline
) )
from novelwriter.common import checkInt from novelwriter.common import checkInt
from novelwriter.constants import trConst, nwKeyWords, nwLabels from novelwriter.constants import nwHeaders, trConst, nwKeyWords, nwLabels
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -57,20 +57,22 @@ class GuiOutlineView(QWidget):
loadDocumentTagRequest = pyqtSignal(str, Enum) loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, mainGui): def __init__(self, mainGui):
QWidget.__init__(self, mainGui) super().__init__(parent=mainGui)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui self.mainGui = mainGui
self.theProject = mainGui.theProject
# Build GUI # Build GUI
self.outlineBar = GuiOutlineToolBar(self)
self.outlineTree = GuiOutlineTree(self) self.outlineTree = GuiOutlineTree(self)
self.outlineData = GuiOutlineDetails(self) self.outlineData = GuiOutlineDetails(self)
self.outlineBar = GuiOutlineToolBar(self)
self.outlineBar.setEnabled(False)
self.splitOutline = QSplitter(Qt.Vertical) self.splitOutline = QSplitter(Qt.Vertical)
self.splitOutline.addWidget(self.outlineTree) self.splitOutline.addWidget(self.outlineTree)
self.splitOutline.addWidget(self.outlineData) self.splitOutline.addWidget(self.outlineData)
self.splitOutline.setSizes(self.mainConf.getOutlinePanePos()) self.splitOutline.setSizes(self.mainConf.outlinePanePos)
# Assemble # Assemble
self.outerBox = QVBoxLayout() self.outerBox = QVBoxLayout()
@@ -96,33 +98,67 @@ class GuiOutlineView(QWidget):
# Methods # Methods
## ##
def splitSizes(self): def updateTheme(self):
return self.splitOutline.sizes() """Update theme elements.
"""
self.outlineBar.updateTheme()
self.refreshTree()
return
def clearOutline(self): def initSettings(self):
"""Initialise GUI elements that depend on specific settings.
"""
self.outlineTree.initSettings()
self.outlineData.initSettings()
return
def refreshTree(self):
"""Refresh the current tree.
"""
self.outlineTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("outline"))
return
def clearProject(self):
"""Clear project-related GUI content.
"""
self.outlineData.clearDetails() self.outlineData.clearDetails()
self.outlineBar.setEnabled(False)
return return
def initOutline(self): def openProjectTasks(self):
self.outlineTree.initOutline() """Run open project tasks.
self.outlineData.initDetails() """
lastOutline = self.theProject.data.getLastHandle("outline")
if not (lastOutline in self.theProject.tree or lastOutline is None):
lastOutline = self.theProject.tree.findRoot(nwItemClass.NOVEL)
logger.debug("Setting outline tree to root item '%s'", lastOutline)
self.clearProject()
self.outlineBar.populateNovelList()
self.outlineBar.setCurrentRoot(lastOutline)
self.outlineBar.setEnabled(True)
return return
def closeOutline(self): def closeProjectTasks(self):
self.outlineTree.closeOutline() self.outlineTree.closeProjectTasks()
self.outlineData.updateClasses() self.outlineData.updateClasses()
return return
def refreshView(self, overRide=False, novelChanged=False): def splitSizes(self):
self.outlineTree.refreshTree(overRide=overRide, novelChanged=novelChanged) return self.splitOutline.sizes()
return
def treeHasFocus(self):
return self.outlineTree.hasFocus()
def setTreeFocus(self): def setTreeFocus(self):
"""Set the focus to the tree widget.
"""
return self.outlineTree.setFocus() return self.outlineTree.setFocus()
def treeHasFocus(self):
"""Check if the outline tree has focus.
"""
return self.outlineTree.hasFocus()
## ##
# Public Slots # Public Slots
## ##
@@ -172,7 +208,7 @@ class GuiOutlineToolBar(QToolBar):
viewColumnToggled = pyqtSignal(bool, Enum) viewColumnToggled = pyqtSignal(bool, Enum)
def __init__(self, theOutline): def __init__(self, theOutline):
QTreeWidget.__init__(self, theOutline) super().__init__(parent=theOutline)
logger.debug("Initialising GuiOutlineToolBar ...") logger.debug("Initialising GuiOutlineToolBar ...")
@@ -187,7 +223,6 @@ class GuiOutlineToolBar(QToolBar):
self.setMovable(False) self.setMovable(False)
self.setIconSize(QSize(iPx, iPx)) self.setIconSize(QSize(iPx, iPx))
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
self.setStyleSheet("QToolBar {border: 0px;}")
stretch = QWidget(self) stretch = QWidget(self)
stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
@@ -202,7 +237,6 @@ class GuiOutlineToolBar(QToolBar):
# Actions # Actions
self.aRefresh = QAction(self.tr("Refresh"), self) self.aRefresh = QAction(self.tr("Refresh"), self)
self.aRefresh.setIcon(self.mainTheme.getIcon("refresh"))
self.aRefresh.triggered.connect(self._refreshRequested) self.aRefresh.triggered.connect(self._refreshRequested)
# Column Menu # Column Menu
@@ -212,7 +246,6 @@ class GuiOutlineToolBar(QToolBar):
) )
self.tbColumns = QToolButton(self) self.tbColumns = QToolButton(self)
self.tbColumns.setIcon(self.mainTheme.getIcon("menu"))
self.tbColumns.setMenu(self.mColumns) self.tbColumns.setMenu(self.mColumns)
self.tbColumns.setPopupMode(QToolButton.InstantPopup) self.tbColumns.setPopupMode(QToolButton.InstantPopup)
@@ -224,6 +257,8 @@ class GuiOutlineToolBar(QToolBar):
self.addWidget(self.tbColumns) self.addWidget(self.tbColumns)
self.addWidget(stretch) self.addWidget(stretch)
self.updateTheme()
logger.debug("GuiOutlineToolBar initialisation complete") logger.debug("GuiOutlineToolBar initialisation complete")
return return
@@ -232,6 +267,16 @@ class GuiOutlineToolBar(QToolBar):
# Methods # Methods
## ##
def updateTheme(self):
"""Update theme elements.
"""
self.setStyleSheet("QToolBar {border: 0px;}")
self.aRefresh.setIcon(self.mainTheme.getIcon("refresh"))
self.tbColumns.setIcon(self.mainTheme.getIcon("menu"))
return
def populateNovelList(self): def populateNovelList(self):
"""Fill the novel combo box with a list of all novel folders. """Fill the novel combo box with a list of all novel folders.
""" """
@@ -243,6 +288,17 @@ class GuiOutlineToolBar(QToolBar):
self.novelValue.addItem(tIcon, self.tr("All Novel Folders"), "") self.novelValue.addItem(tIcon, self.tr("All Novel Folders"), "")
return return
def setCurrentRoot(self, rootHandle):
"""Set the current active root handle.
"""
if rootHandle is None:
rootIdx = self.novelValue.count() - 1
else:
rootIdx = self.novelValue.findData(rootHandle)
if rootIdx >= 0:
self.novelValue.setCurrentIndex(rootIdx)
return
def setColumnHiddenState(self, hiddenState): def setColumnHiddenState(self, hiddenState):
"""Forward the change of column hidden states to the menu. """Forward the change of column hidden states to the menu.
""" """
@@ -313,11 +369,14 @@ class GuiOutlineTree(QTreeWidget):
nwOutline.SYNOP: False, nwOutline.SYNOP: False,
} }
D_HANDLE = Qt.UserRole
D_TITLE = Qt.UserRole + 1
hiddenStateChanged = pyqtSignal() hiddenStateChanged = pyqtSignal()
activeItemChanged = pyqtSignal(str, str) activeItemChanged = pyqtSignal(str, str)
def __init__(self, theOutline): def __init__(self, theOutline):
QTreeWidget.__init__(self, theOutline) super().__init__(parent=theOutline)
logger.debug("Initialising GuiOutlineTree ...") logger.debug("Initialising GuiOutlineTree ...")
@@ -326,6 +385,7 @@ class GuiOutlineTree(QTreeWidget):
self.theProject = theOutline.mainGui.theProject self.theProject = theOutline.mainGui.theProject
self.mainTheme = theOutline.mainGui.mainTheme self.mainTheme = theOutline.mainGui.mainTheme
self.setUniformRowHeights(True)
self.setFrameStyle(QFrame.NoFrame) self.setFrameStyle(QFrame.NoFrame)
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.SingleSelection) self.setSelectionMode(QAbstractItemView.SingleSelection)
@@ -336,11 +396,28 @@ class GuiOutlineTree(QTreeWidget):
iPx = self.mainTheme.baseIconSize iPx = self.mainTheme.baseIconSize
self.setIconSize(QSize(iPx, iPx)) self.setIconSize(QSize(iPx, iPx))
self.setIndentation(iPx) self.setIndentation(0)
self.treeHead = self.header() self.treeHead = self.header()
self.treeHead.sectionMoved.connect(self._columnMoved) self.treeHead.sectionMoved.connect(self._columnMoved)
# Pre-Generate Tree Formatting
fH1 = self.font()
fH1.setBold(True)
fH1.setUnderline(True)
fH2 = self.font()
fH2.setBold(True)
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
self._dIcon = {
"H0": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H0"),
"H1": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H1"),
"H2": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H2"),
"H3": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H3"),
"H4": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H4"),
}
# Internals # Internals
self._treeOrder = [] self._treeOrder = []
self._colWidth = {} self._colWidth = {}
@@ -350,8 +427,8 @@ class GuiOutlineTree(QTreeWidget):
self._firstView = True self._firstView = True
self._lastBuild = 0 self._lastBuild = 0
self.initOutline() self.initSettings()
self.clearOutline() self.clearContent()
self.hiddenStateChanged.emit() self.hiddenStateChanged.emit()
@@ -371,7 +448,7 @@ class GuiOutlineTree(QTreeWidget):
# Methods # Methods
## ##
def initOutline(self): def initSettings(self):
"""Set or update outline settings. """Set or update outline settings.
""" """
# Scroll bars # Scroll bars
@@ -387,7 +464,7 @@ class GuiOutlineTree(QTreeWidget):
return return
def clearOutline(self): def clearContent(self):
"""Clear the tree and header and set the default values for the """Clear the tree and header and set the default values for the
columns arrays. columns arrays.
""" """
@@ -425,18 +502,20 @@ class GuiOutlineTree(QTreeWidget):
# If the novel index or novel tree has changed since the tree # If the novel index or novel tree has changed since the tree
# was last built, we rebuild the tree from the updated index. # was last built, we rebuild the tree from the updated index.
indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild) indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline if not (novelChanged or indexChanged or overRide):
if doBuild or overRide: logger.debug("No changes have been made to the novel index")
logger.debug("Rebuilding Project Outline") return
self._populateTree(rootHandle)
self._populateTree(rootHandle)
self.theProject.data.setLastHandle(rootHandle or None, "outline")
return return
def closeOutline(self): def closeProjectTasks(self):
"""Called before a project is closed. """Called before a project is closed.
""" """
self._saveHeaderState() self._saveHeaderState()
self.clearOutline() self.clearContent()
self._firstView = True self._firstView = True
return return
@@ -448,7 +527,7 @@ class GuiOutlineTree(QTreeWidget):
tHandle = None tHandle = None
tLine = 0 tLine = 0
if selItem: if selItem:
tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole) tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
tLine = checkInt(selItem[0].text(self._colIdx[nwOutline.LINE]), 1) - 1 tLine = checkInt(selItem[0].text(self._colIdx[nwOutline.LINE]), 1) - 1
return tHandle, tLine return tHandle, tLine
@@ -474,8 +553,8 @@ class GuiOutlineTree(QTreeWidget):
""" """
selItems = self.selectedItems() selItems = self.selectedItems()
if selItems: if selItems:
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole) tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole) sTitle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
self.activeItemChanged.emit(tHandle, sTitle) self.activeItemChanged.emit(tHandle, sTitle)
return return
@@ -494,11 +573,9 @@ class GuiOutlineTree(QTreeWidget):
"""Receive the changes to column visibility forwarded by the """Receive the changes to column visibility forwarded by the
column selection menu. column selection menu.
""" """
logger.verbose("User toggled Outline column '%s'", theItem.name)
if theItem in self._colIdx: if theItem in self._colIdx:
self.setColumnHidden(self._colIdx[theItem], not isChecked) self.setColumnHidden(self._colIdx[theItem], not isChecked)
self._saveHeaderState() self._saveHeaderState()
return return
## ##
@@ -613,110 +690,60 @@ class GuiOutlineTree(QTreeWidget):
self.setColumnWidth(self._colIdx[hItem], self._colWidth[hItem]) self.setColumnWidth(self._colIdx[hItem], self._colWidth[hItem])
self.setColumnHidden(self._colIdx[hItem], self._colHidden[hItem]) self.setColumnHidden(self._colIdx[hItem], self._colHidden[hItem])
# Make sure title column is always visible, # Make sure title column is always visible
# and handle column always hidden
self.setColumnHidden(self._colIdx[nwOutline.TITLE], False) self.setColumnHidden(self._colIdx[nwOutline.TITLE], False)
headItem = self.headerItem() headItem = self.headerItem()
headItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight) if headItem is not None:
headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight) headItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight) headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
currTitle = None
currChapter = None
currScene = None
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True) novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
for _, tHandle, sTitle, novIdx in novStruct: for _, tHandle, sTitle, novIdx in novStruct:
tItem = self._createTreeItem(tHandle, sTitle, novIdx) iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0)
if iLevel == 0:
continue
tLevel = novIdx.level trItem = QTreeWidgetItem()
if tLevel == "H1": nwItem = self.theProject.tree[tHandle]
self.addTopLevelItem(tItem) hDec = self.mainTheme.getHeaderDecoration(iLevel)
currTitle = tItem
currChapter = None
currScene = None
elif tLevel == "H2": trItem.setData(self._colIdx[nwOutline.TITLE], Qt.DecorationRole, hDec)
if currTitle is None: trItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title)
self.addTopLevelItem(tItem) trItem.setData(self._colIdx[nwOutline.TITLE], self.D_HANDLE, tHandle)
else: trItem.setData(self._colIdx[nwOutline.TITLE], self.D_TITLE, sTitle)
currTitle.addChild(tItem) trItem.setFont(self._colIdx[nwOutline.TITLE], self._hFonts[iLevel])
currChapter = tItem trItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level)
currScene = None 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)
trItem.setText(self._colIdx[nwOutline.CCOUNT], f"{novIdx.charCount:n}")
trItem.setText(self._colIdx[nwOutline.WCOUNT], f"{novIdx.wordCount:n}")
trItem.setText(self._colIdx[nwOutline.PCOUNT], f"{novIdx.paraCount:n}")
trItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
trItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
trItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
elif tLevel == "H3": refs = self.theProject.index.getReferences(tHandle, sTitle)
if currChapter is None: trItem.setText(self._colIdx[nwOutline.POV], ", ".join(refs[nwKeyWords.POV_KEY]))
if currTitle is None: trItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(refs[nwKeyWords.FOCUS_KEY]))
self.addTopLevelItem(tItem) trItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(refs[nwKeyWords.CHAR_KEY]))
else: trItem.setText(self._colIdx[nwOutline.PLOT], ", ".join(refs[nwKeyWords.PLOT_KEY]))
currTitle.addChild(tItem) trItem.setText(self._colIdx[nwOutline.TIME], ", ".join(refs[nwKeyWords.TIME_KEY]))
else: trItem.setText(self._colIdx[nwOutline.WORLD], ", ".join(refs[nwKeyWords.WORLD_KEY]))
currChapter.addChild(tItem) trItem.setText(self._colIdx[nwOutline.OBJECT], ", ".join(refs[nwKeyWords.OBJECT_KEY]))
currScene = tItem trItem.setText(self._colIdx[nwOutline.ENTITY], ", ".join(refs[nwKeyWords.ENTITY_KEY]))
trItem.setText(self._colIdx[nwOutline.CUSTOM], ", ".join(refs[nwKeyWords.CUSTOM_KEY]))
elif tLevel == "H4": self.addTopLevelItem(trItem)
if currScene is None:
if currChapter is None:
if currTitle is None:
self.addTopLevelItem(tItem)
else:
currTitle.addChild(tItem)
else:
currChapter.addChild(tItem)
else:
currScene.addChild(tItem)
tItem.setExpanded(True)
self._lastBuild = time() self._lastBuild = time()
return return
def _createTreeItem(self, tHandle, sTitle, novIdx):
"""Populate a tree item with all the column values.
"""
nwItem = self.theProject.tree[tHandle]
newItem = QTreeWidgetItem()
hIcon = "doc_%s" % novIdx.level.lower()
hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
dIcon = self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, hLevel)
cC = int(novIdx.charCount)
wC = int(novIdx.wordCount)
pC = int(novIdx.paraCount)
newItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title)
newItem.setData(self._colIdx[nwOutline.TITLE], Qt.UserRole, tHandle)
newItem.setIcon(self._colIdx[nwOutline.TITLE], self.mainTheme.getIcon(hIcon))
newItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level)
newItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName)
newItem.setIcon(self._colIdx[nwOutline.LABEL], dIcon)
newItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0"))
newItem.setData(self._colIdx[nwOutline.LINE], Qt.UserRole, sTitle)
newItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis)
newItem.setText(self._colIdx[nwOutline.CCOUNT], f"{cC:n}")
newItem.setText(self._colIdx[nwOutline.WCOUNT], f"{wC:n}")
newItem.setText(self._colIdx[nwOutline.PCOUNT], f"{pC:n}")
newItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
theRefs = self.theProject.index.getReferences(tHandle, sTitle)
newItem.setText(self._colIdx[nwOutline.POV], ", ".join(theRefs[nwKeyWords.POV_KEY]))
newItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(theRefs[nwKeyWords.FOCUS_KEY]))
newItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY]))
newItem.setText(self._colIdx[nwOutline.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY]))
newItem.setText(self._colIdx[nwOutline.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY]))
newItem.setText(self._colIdx[nwOutline.WORLD], ", ".join(theRefs[nwKeyWords.WORLD_KEY]))
newItem.setText(self._colIdx[nwOutline.OBJECT], ", ".join(theRefs[nwKeyWords.OBJECT_KEY]))
newItem.setText(self._colIdx[nwOutline.ENTITY], ", ".join(theRefs[nwKeyWords.ENTITY_KEY]))
newItem.setText(self._colIdx[nwOutline.CUSTOM], ", ".join(theRefs[nwKeyWords.CUSTOM_KEY]))
return newItem
# END Class GuiOutlineTree # END Class GuiOutlineTree
@@ -725,7 +752,7 @@ class GuiOutlineHeaderMenu(QMenu):
columnToggled = pyqtSignal(bool, Enum) columnToggled = pyqtSignal(bool, Enum)
def __init__(self, theOutline): def __init__(self, theOutline):
QMenu.__init__(self, theOutline) super().__init__(parent=theOutline)
self.acceptToggle = True self.acceptToggle = True
@@ -776,7 +803,7 @@ class GuiOutlineDetails(QScrollArea):
itemTagClicked = pyqtSignal(str) itemTagClicked = pyqtSignal(str)
def __init__(self, theOutline): def __init__(self, theOutline):
QScrollArea.__init__(self, theOutline) super().__init__(parent=theOutline)
logger.debug("Initialising GuiOutlineDetails ...") logger.debug("Initialising GuiOutlineDetails ...")
@@ -963,13 +990,13 @@ class GuiOutlineDetails(QScrollArea):
self.setWidgetResizable(True) self.setWidgetResizable(True)
self.setFrameStyle(QFrame.NoFrame) self.setFrameStyle(QFrame.NoFrame)
self.initDetails() self.initSettings()
logger.debug("GuiOutlineDetails initialisation complete") logger.debug("GuiOutlineDetails initialisation complete")
return return
def initDetails(self): def initSettings(self):
"""Set or update outline settings. """Set or update outline settings.
""" """
# Scroll bars # Scroll bars
@@ -1032,7 +1059,7 @@ class GuiOutlineDetails(QScrollArea):
self.titleLabel.setText("<b>%s</b>" % self.tr("Title")) self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText(novIdx.title) self.titleValue.setText(novIdx.title)
itemStatus, _ = nwItem.getImportStatus() itemStatus, _ = nwItem.getImportStatus(incIcon=False)
self.fileValue.setText(nwItem.itemName) self.fileValue.setText(nwItem.itemName)
self.itemValue.setText(itemStatus) self.itemValue.setText(itemStatus)

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