Merge branch 'main' into i18n-de_DE-created
@@ -14,7 +14,7 @@ jobs:
|
||||
testLinux:
|
||||
strategy:
|
||||
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
|
||||
steps:
|
||||
- name: Python Setup
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Install Packages (apt)
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install libenchant-dev qttools5-dev-tools
|
||||
sudo apt install libenchant-dev qttools5-dev-tools aspell-en
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Dependencies (pip)
|
||||
|
||||
@@ -1,7 +1,237 @@
|
||||
# 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]
|
||||
|
||||
**Note:** The 1.7 release cycle was renamed 2.0 on 2022-10-06. See #1144.
|
||||
|
||||
### Release Notes
|
||||
|
||||
This is a beta release of the next release version, and is intended for testing purposes. Please be
|
||||
@@ -89,6 +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]
|
||||
|
||||
### Release Notes
|
||||
@@ -3468,8 +3797,8 @@ helpful feedback and issue reports for the new features added in this, and previ
|
||||
|
||||
**User Interface**
|
||||
|
||||
* Added a preferences dialog for the program settings. No longer necessary to edit the config file.
|
||||
PR #30.
|
||||
* Added a preferences dialog for the program settings. It is no longer necessary to edit the config
|
||||
file. PR #30.
|
||||
* The document viewer remembers scroll bar position when pressing `Ctrl+R` on a document already
|
||||
being viewed. 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**
|
||||
|
||||
* 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
|
||||
#21.
|
||||
* Project word count is written to the project file, which is needed for the session word count.
|
||||
PR #21.
|
||||
* Closing a project now clears the status bar. PR #21.
|
||||
|
||||
**Editor**
|
||||
|
||||
@@ -33,7 +33,7 @@ You can also follow novelWriter on Mastodon at [fosstodon.org/@novelwriter](http
|
||||
|
||||
## Implementation
|
||||
|
||||
The application is written with Python 3 (3.7+) using Qt5 and PyQt5 (5.3+). It is developed on
|
||||
The application is written with Python 3 (3.7+) using Qt5 and PyQt5 (5.10+). It is developed on
|
||||
Linux, but should in principle work fine on other operating systems as well as long as dependencies
|
||||
are met. It is regularly tested on Debian and Ubuntu Linux, Windows, and macOS.
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 423 KiB After Width: | Height: | Size: 534 KiB |
|
Before Width: | Height: | Size: 431 KiB After Width: | Height: | Size: 539 KiB |
|
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 379 KiB |
@@ -71,5 +71,5 @@ On Windows, file extensions may not be visible by default, so make sure you only
|
||||
extension, and don't end up with two.
|
||||
|
||||
The QSS files are Qt Style Sheet files. See Qt's
|
||||
`The Style Sheet Syntax <https://doc.qt.io/qt-5/stylesheet-syntax.html>`_` documentation for more
|
||||
`The Style Sheet Syntax <https://doc.qt.io/qt-5/stylesheet-syntax.html>`_ documentation for more
|
||||
details.
|
||||
|
||||
@@ -34,7 +34,7 @@ The following Python packages are needed to run novelWriter:
|
||||
* ``lxml`` – needed for full XML support.
|
||||
* ``PyEnchant`` – needed for spell checking (optional).
|
||||
|
||||
PyQt/Qt should be at least 5.3, but ideally 5.10 or higher for nearly all features to work. For
|
||||
PyQt/Qt should be at least 5.10, but ideally 5.13 or higher for nearly all features to work. For
|
||||
instance, searching using regular expressions with full Unicode support requires 5.13. There is no
|
||||
known minimum version requirement for package ``lxml``, but the code was originally written with
|
||||
4.2, which is therefore set as the minimum. It may work on lower versions. You have to test it.
|
||||
|
||||
@@ -40,8 +40,7 @@ Application Data
|
||||
|
||||
novelWriter also stores a bit of data that is generated by the user's actions. This includes the
|
||||
list of recent projects form the :guilabel:`Open Project` dialog. Custom themes are also saved
|
||||
here. The system paths are provided by the Qt QStandardPaths_ class and its AppDataLocation value
|
||||
on Qt 5.4 or greater, or DataLocation for earlier versions.
|
||||
here. The system paths are provided by the Qt QStandardPaths_ class and its AppDataLocation.
|
||||
|
||||
The standard paths are:
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ The main shorcuts are as follows:
|
||||
":kbd:`Ctrl`:kbd:`H`", "Open the search and replace bar and search for the selected word, if any is selected. (On Mac, this is :kbd:`Cmd`:kbd:`=`.)"
|
||||
":kbd:`Ctrl`:kbd:`I`", "Format selected text, or word under cursor, with emphasis (italic)."
|
||||
":kbd:`Ctrl`:kbd:`K`", "Activate the insert commands. The commands are listed in :ref:`a_kb_ins`."
|
||||
":kbd:`Ctrl`:kbd:`L`", "Open the :guilabel:`Quick Links` menu in the Project Tree."
|
||||
":kbd:`Ctrl`:kbd:`N`", "Create new project item."
|
||||
":kbd:`Ctrl`:kbd:`O`", "Open selected document."
|
||||
":kbd:`Ctrl`:kbd:`Q`", "Exit novelWriter."
|
||||
@@ -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:`C`", "Insert a ``@char`` 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:`L`", "Insert a ``@location`` keyword."
|
||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`O`", "Insert an ``@object`` keyword."
|
||||
|
||||
@@ -27,7 +27,6 @@ import sys
|
||||
import getopt
|
||||
import logging
|
||||
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QApplication, QErrorMessage
|
||||
|
||||
from novelwriter.error import exceptionHandler, logException
|
||||
@@ -60,9 +59,9 @@ __license__ = "GPLv3"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
__version__ = "1.7-beta1"
|
||||
__hexversion__ = "0x010700b1"
|
||||
__date__ = "2022-05-17"
|
||||
__version__ = "2.0-rc2"
|
||||
__hexversion__ = "0x020000c2"
|
||||
__date__ = "2022-11-13"
|
||||
__status__ = "Stable"
|
||||
__domain__ = "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"
|
||||
__docurl__ = "https://novelwriter.readthedocs.io"
|
||||
|
||||
##
|
||||
# Logging
|
||||
# =========
|
||||
# Standard used for logging levels in novelWriter:
|
||||
# CRITICAL Use for errors that result in termination of the program
|
||||
# ERROR Use when an action fails, but execution continues
|
||||
# WARNING When something unexpected, but non-critical happens
|
||||
# INFO Any useful user information like open, save, exit initiated
|
||||
# ----------- SPAM Threshold : Output above should be minimal -----------------
|
||||
# DEBUG Use for descriptions of main program flow
|
||||
# VERBOSE Use for outputting values and program flow details
|
||||
##
|
||||
|
||||
# Add verbose logging level
|
||||
VERBOSE = 5
|
||||
logging.addLevelName(VERBOSE, "VERBOSE")
|
||||
|
||||
|
||||
def logVerbose(self, message, *args, **kws):
|
||||
if self.isEnabledFor(VERBOSE):
|
||||
self._log(VERBOSE, message, args, **kws)
|
||||
|
||||
|
||||
logging.Logger.verbose = logVerbose
|
||||
|
||||
# Initiating logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -122,7 +95,6 @@ def main(sysArgs=None):
|
||||
"version",
|
||||
"info",
|
||||
"debug",
|
||||
"verbose",
|
||||
"style=",
|
||||
"config=",
|
||||
"data=",
|
||||
@@ -143,7 +115,6 @@ def main(sysArgs=None):
|
||||
" -v, --version Print program version and exit.\n"
|
||||
" --info Print additional runtime information.\n"
|
||||
" --debug Print debug output. Includes --info.\n"
|
||||
" --verbose Increase verbosity of debug output. Includes --debug.\n"
|
||||
" --style= Sets Qt5 style flag. Defaults to 'Fusion'.\n"
|
||||
" --config= Alternative config file.\n"
|
||||
" --data= Alternative user data path.\n"
|
||||
@@ -181,9 +152,6 @@ def main(sysArgs=None):
|
||||
elif inOpt == "--debug":
|
||||
logLevel = logging.DEBUG
|
||||
logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}"
|
||||
elif inOpt == "--verbose":
|
||||
logLevel = VERBOSE
|
||||
logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}"
|
||||
elif inOpt == "--style":
|
||||
qtStyle = inArg
|
||||
elif inOpt == "--config":
|
||||
@@ -193,9 +161,6 @@ def main(sysArgs=None):
|
||||
elif inOpt == "--testmode":
|
||||
testMode = True
|
||||
|
||||
# Set Config Options
|
||||
CONFIG.cmdOpen = cmdOpen
|
||||
|
||||
# Set Logging
|
||||
cHandle = logging.StreamHandler()
|
||||
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
|
||||
)
|
||||
errorCode |= 0x04
|
||||
if CONFIG.verQtValue < 50300:
|
||||
if CONFIG.verQtValue < 51000:
|
||||
errorData.append(
|
||||
"At least Qt5 version 5.3 is required, found %s" % CONFIG.verQtString
|
||||
"At least Qt5 version 5.10 is required, found %s" % CONFIG.verQtString
|
||||
)
|
||||
errorCode |= 0x08
|
||||
if CONFIG.verPyQtValue < 50300:
|
||||
if CONFIG.verPyQtValue < 51000:
|
||||
errorData.append(
|
||||
"At least PyQt5 version 5.3 is required, found %s" % CONFIG.verPyQtString
|
||||
"At least PyQt5 version 5.10 is required, found %s" % CONFIG.verPyQtString
|
||||
)
|
||||
errorCode |= 0x10
|
||||
|
||||
@@ -280,7 +245,6 @@ def main(sysArgs=None):
|
||||
nwApp = QApplication([CONFIG.appName, (f"-style={qtStyle}")])
|
||||
nwApp.setApplicationName(CONFIG.appName)
|
||||
nwApp.setApplicationVersion(__version__)
|
||||
nwApp.setWindowIcon(QIcon(CONFIG.appIcon))
|
||||
nwApp.setOrganizationDomain(__domain__)
|
||||
|
||||
# Connect the exception handler before making the main GUI
|
||||
@@ -289,9 +253,7 @@ def main(sysArgs=None):
|
||||
# Launch main GUI
|
||||
CONFIG.initLocalisation(nwApp)
|
||||
nwGUI = GuiMain()
|
||||
if not nwGUI.hasProject:
|
||||
nwGUI.showProjectLoadDialog()
|
||||
nwGUI.releaseNotes()
|
||||
nwGUI.postLaunchTasks(cmdOpen)
|
||||
|
||||
sys.exit(nwApp.exec_())
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
||||
[Map]
|
||||
add = typ_plus.svg
|
||||
backward = typ_chevron-left.svg
|
||||
bookmark = typ_bookmark.svg
|
||||
bullet-off = typ_media-record-outline.svg
|
||||
bullet-on = typ_media-record.svg
|
||||
check = typ_tick.svg
|
||||
clear = typ_backspace.svg
|
||||
checked = mixed_input-checked.svg
|
||||
close = typ_times.svg
|
||||
cls_archive = typ_delete.svg
|
||||
cls_character = typ_user.svg
|
||||
@@ -35,32 +35,25 @@ cls_timeline = typ_calendar.svg
|
||||
cls_trash = typ_trash.svg
|
||||
cls_world = typ_location.svg
|
||||
cross = typ_times.svg
|
||||
delete = typ_delete.svg
|
||||
doc_h0 = mixed_heading0.svg
|
||||
doc_h1 = mixed_heading1.svg
|
||||
doc_h2 = mixed_heading2.svg
|
||||
doc_h3 = mixed_heading3.svg
|
||||
doc_h4 = mixed_heading4.svg
|
||||
done = typ_input-checked.svg
|
||||
down = typ_chevron-down.svg
|
||||
edit = typ_pencil.svg
|
||||
forward = typ_chevron-right.svg
|
||||
hash = typ_hash.svg
|
||||
maximise = typ_arrow-maximise.svg
|
||||
menu = typ_th-menu.svg
|
||||
minimise = typ_arrow-minimise.svg
|
||||
noncheckable = mixed_input-none.svg
|
||||
proj_chapter = mixed_document-chapter.svg
|
||||
proj_details = typ_th-list-grey.svg
|
||||
proj_document = typ_document-text.svg
|
||||
proj_folder = typ_folder.svg
|
||||
proj_note = mixed_document-note.svg
|
||||
proj_scene = mixed_document-scene.svg
|
||||
proj_section = mixed_document-section.svg
|
||||
proj_stats = typ_chart-bar-grey.svg
|
||||
proj_title = mixed_document-title.svg
|
||||
reference = typ_at.svg
|
||||
refresh = typ_refresh.svg
|
||||
remove = typ_minus.svg
|
||||
save = typ_download.svg
|
||||
search = typ_search.svg
|
||||
search_cancel = typ_cancel-grey.svg
|
||||
search_case = nw_search-case.svg
|
||||
@@ -78,13 +71,16 @@ status_stats = typ_chart-bar-grey.svg
|
||||
status_time = typ_stopwatch-grey.svg
|
||||
sticky-off = typ_pin-outline.svg
|
||||
sticky-on = typ_pin.svg
|
||||
unchecked = mixed_input-unchecked.svg
|
||||
up = typ_chevron-up.svg
|
||||
view_build = typ_export.svg
|
||||
view_editor = mixed_edit.svg
|
||||
view_novel = typ_book-grey.svg
|
||||
view_outline = typ_puzzle-outline.svg
|
||||
|
||||
deco_doc_h0 = nw_deco-h0.svg
|
||||
deco_doc_h1 = nw_deco-h1.svg
|
||||
deco_doc_h2 = nw_deco-h2.svg
|
||||
deco_doc_h3 = nw_deco-h3.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"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg2608">
|
||||
id="svg901">
|
||||
<metadata
|
||||
id="metadata2614">
|
||||
id="metadata907">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
@@ -23,9 +23,9 @@
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs2612" />
|
||||
id="defs905" />
|
||||
<path
|
||||
d="M 16.203226,2 H 7.796774 C 6.05874,2 4.6443545,3.4143855 4.6443545,5.1524195 V 19.86371 c 0,0.540115 0.1092839,0.994063 0.3236484,1.350287 0.5926549,0.982504 1.9072138,1.059213 2.9559187,0.0084 L 11.25708,17.889239 c 0.394052,-0.393002 1.091787,-0.393002 1.485841,0 l 3.333158,3.333158 C 16.592024,21.738349 17.128987,22 17.673304,22 c 0.837492,0 1.682342,-0.660957 1.682342,-2.13629 V 5.1524195 C 19.355646,3.4143855 17.941259,2 16.203226,2 Z M 7.796774,4.101613 h 8.406452 c 0.578994,0 1.050806,0.4718121 1.050806,1.0508065 V 15.560658 l -2.575527,-2.361162 c -1.477434,-1.35449 -3.880628,-1.353439 -5.3580615,0 l -2.574476,2.361162 V 5.1524195 c 0,-0.5789944 0.4718121,-1.0508065 1.0508065,-1.0508065 z m 6.431987,12.301792 C 13.635055,15.809699 12.843798,15.482898 12,15.482898 c -0.843797,0 -1.635054,0.327852 -2.2287605,0.920507 l -3.025272,3.025272 v -2.442075 l 3.2848215,-3.011611 c 1.085483,-0.995114 2.851889,-0.995114 3.937372,0 l 3.285871,3.011611 v 2.442075 z"
|
||||
id="path2606"
|
||||
style="stroke-width:1.05081;fill:#6699cc;fill-opacity:1" />
|
||||
id="path899"
|
||||
style="fill:#6699cc;fill-opacity:1;stroke-width:1.05081" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg13801">
|
||||
<metadata
|
||||
id="metadata13807">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs13805" />
|
||||
<path
|
||||
style="display:inline;fill:#cda66f;fill-opacity:1"
|
||||
id="path6288"
|
||||
d="m 20.987,16 c 0,-0.105 -0.004,-0.211 -0.039,-0.316 l -2,-6 C 18.812,9.275 18.431,9 18,9 h -0.219 c -0.094,0.188 -0.21,0.368 -0.367,0.525 L 15.932,11 h 1.348 l 1.667,5 H 5.054 L 6.721,11 H 8.069 L 6.586,9.525 C 6.429,9.368 6.312,9.188 6.219,9 H 6 C 5.569,9 5.188,9.275 5.052,9.684 l -2,6 C 3.017,15.789 3.013,15.895 3.013,16 3,16 3,21 3,21 c 0,0.553 0.447,1 1,1 h 16 c 0.553,0 1,-0.447 1,-1 0,0 0,-5 -0.013,-5 z" />
|
||||
<path
|
||||
d="M 16.707,7.404 C 16.518,7.216 16.259,7.121 16,7.121 c -0.259,0 -0.518,0.095 -0.707,0.283 L 13,9.697 V 3 C 13,2.448 12.552,2 12,2 11.448,2 11,2.448 11,3 V 9.697 L 8.707,7.404 C 8.518,7.216 8.267,7.111 8,7.111 c -0.267,0 -0.518,0.105 -0.707,0.293 -0.39,0.39 -0.39,1.024 0,1.414 L 12,13.5 16.709,8.816 C 17.097,8.429 17.097,7.794 16.707,7.404 Z"
|
||||
id="path13799"
|
||||
style="display:inline;fill:#99cc99;fill-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg3369">
|
||||
<metadata
|
||||
id="metadata3375">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs3373" />
|
||||
<path
|
||||
d="m 10.828125,20 h -4 c -0.553,0 -1,0.447 -1,1 0,0.553 0.447,1 1,1 h 10 c 0.553,0 1,-0.447 1,-1 0,-0.553 -0.447,-1 -1,-1 h -4 v -1.23 c 1.64,-0.371 3.146,-1.188 4.363,-2.406 1.7,-1.7 2.637,-3.96 2.637,-6.364 0,-2.067 -0.692,-4.029 -1.968,-5.619 l 0.675,-0.673 c 0.391,-0.391 0.391,-1.023 10e-4,-1.415 -0.391,-0.391 -1.024,-0.39 -1.415,-0.001 l -2.052,2.049 0.708,0.708 c 1.322,1.322 2.051,3.081 2.051,4.951 0,1.87 -0.729,3.627 -2.051,4.949 -1.322,1.322 -3.079,2.051 -4.949,2.051 -1.87,0 -3.627,-0.729 -4.949,-2.051 -0.391,-0.391 -1.023,-0.391 -1.414,0 -0.391,0.39 -0.391,1.023 0,1.414 1.699,1.7 3.959,2.637 6.363,2.637 z m 0,-16 c 1.657,0 3.157,0.672 4.243,1.757 1.085,1.086 1.757,2.586 1.757,4.243 0,1.656 -0.672,3.156 -1.757,4.242 -1.086,1.086 -2.586,1.758 -4.243,1.758 -1.658,0 -3.157,-0.672 -4.242,-1.757 -1.085,-1.086 -1.756,-2.586 -1.756,-4.243 0,-1.657 0.671,-3.157 1.756,-4.243 1.085,-1.085 2.584,-1.757 4.242,-1.757 z"
|
||||
id="path3367"
|
||||
style="fill:#aeaeae;fill-opacity:1;stroke-width:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg5204">
|
||||
<metadata
|
||||
id="metadata5210">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs5208" />
|
||||
<path
|
||||
id="rect928"
|
||||
d="M 7.75 2 C 6.7805 2 6 2.7805 6 3.75 L 6 5.5 L 4.1621094 5.5 C 3.2410844 5.5 2.5 6.2805 2.5 7.25 C 2.5 8.2195 3.2410844 9 4.1621094 9 L 6 9 L 6 15 L 4.1621094 15 C 3.2410844 15 2.5 15.7805 2.5 16.75 C 2.5 17.7195 3.2410844 18.5 4.1621094 18.5 L 6 18.5 L 6 20.25 C 6 21.2195 6.7805 22 7.75 22 C 8.7195 22 9.5 21.2195 9.5 20.25 L 9.5 18.5 L 14.5 18.5 L 14.5 20.25 C 14.5 21.2195 15.2805 22 16.25 22 C 17.2195 22 18 21.2195 18 20.25 L 18 18.5 L 19.837891 18.5 C 20.758916 18.5 21.5 17.7195 21.5 16.75 C 21.5 15.7805 20.758916 15 19.837891 15 L 18 15 L 18 9 L 19.837891 9 C 20.758916 9 21.5 8.2195 21.5 7.25 C 21.5 6.2805 20.758916 5.5 19.837891 5.5 L 18 5.5 L 18 3.75 C 18 2.7805 17.2195 2 16.25 2 C 15.2805 2 14.5 2.7805 14.5 3.75 L 14.5 5.5 L 9.5 5.5 L 9.5 3.75 C 9.5 2.7805 8.7195 2 7.75 2 z M 9.5 9 L 14.5 9 L 14.5 15 L 9.5 15 L 9.5 9 z "
|
||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.7544229;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg11367"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata11373">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs11371" />
|
||||
<path
|
||||
style="display:inline;fill:#99cc99;fill-opacity:1;stroke-width:1.28570998"
|
||||
id="path11365"
|
||||
d="m 13.499143,15.642429 c -0.45,0 -0.885857,-0.178715 -1.209857,-0.502715 L 8.859,11.710714 c -0.668571,-0.669857 -0.668571,-1.755 0,-2.423571 0.669857,-0.669857 1.755,-0.669857 2.426143,0 L 13.105714,11.109 17.573571,4.0954286 c 0.459,-0.828 1.504286,-1.1275715 2.332286,-0.6672857 0.826714,0.4602857 1.125,1.5042857 0.666,2.331 l -5.572286,9.0000001 c -0.261,0.470571 -0.727714,0.790714 -1.26,0.865286 z" />
|
||||
<path
|
||||
d="M 17.142857,21 H 6.8571429 C 4.7305714,21 3,19.269429 3,17.142857 V 6.8571429 C 3,4.7305714 4.7305714,3 6.8571429,3 h 6.4285711 c 0.711,0 1.285715,0.576 1.285715,1.2857143 0,0.7097143 -0.574715,1.2857143 -1.285715,1.2857143 H 6.8571429 c -0.7097143,0 -1.2857143,0.5772857 -1.2857143,1.2857143 V 17.142857 c 0,0.708429 0.576,1.285714 1.2857143,1.285714 H 17.142857 c 0.709714,0 1.285714,-0.577285 1.285714,-1.285714 V 13.285714 C 18.428571,12.576 19.003286,12 19.714286,12 20.425286,12 21,12.576 21,13.285714 v 3.857143 C 21,19.269429 19.269429,21 17.142857,21 Z"
|
||||
id="path16196"
|
||||
style="display:inline;fill:#aeaeae;fill-opacity:1;stroke-width:1.28570998" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg873">
|
||||
<metadata
|
||||
id="metadata879">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs877" />
|
||||
<path
|
||||
id="path871"
|
||||
style="display:inline;fill:#6699cc;fill-opacity:1;stroke-width:1.19007"
|
||||
d="m 10.330622,1.9999997 c -4.5924663,0 -8.3306193,3.738153 -8.3306193,8.3306193 0,1.391929 0.3467825,2.702587 0.9532665,3.857242 0.084308,-0.108411 0.1590865,-0.228838 0.2534294,-0.323181 L 4.7482003,12.323178 C 4.5237338,11.697056 4.378519,11.032991 4.3785189,10.330619 c 0,-3.2810129 2.6710902,-5.949778 5.9521031,-5.949778 0.702303,0 1.36649,0.1429324 1.992559,0.3673563 L 14.141363,2.9300158 C 12.997884,2.3384803 11.704084,1.9999997 10.330622,1.9999997 Z m 5.635898,3.4852355 -0.60451,0.6045104 2.545919,2.5482442 0.460357,-0.4626829 C 18.332278,8.0413114 18.299136,7.9066594 18.256684,7.7754 Z m 0.292955,4.4245521 c 0.0083,0.1189097 0.01733,0.2373427 0.01861,0.3580557 l 0.169728,-0.169728 z m 2.059986,2.6993717 -5.710299,5.710299 c 0.105352,-0.03032 0.217183,-0.04484 0.320855,-0.07906 l 0.892816,0.89049 1.799581,1.797261 0.07208,0.07208 0.07672,0.06743 C 16.486443,21.66984 17.375591,22 18.275281,22 c 2.054056,0 3.724716,-1.671795 3.724716,-3.72704 0,-0.999657 -0.390465,-1.936781 -1.099745,-2.638921 l -1.129969,-1.174145 -0.639387,-0.639386 -0.890489,-0.892815 c 0.03403,-0.103138 0.04885,-0.213757 0.07906,-0.318531 z m -11.7600542,2.28784 -0.4929085,0.492908 0.5417343,0.541734 0.57196,-0.571959 C 6.960254,15.221393 6.7588057,15.062002 6.5594068,14.896999 Z m 2.7179722,1.285747 -1.209021,1.209021 0.5417345,0.541734 1.6577535,-1.655429 c -0.3372847,-0.0036 -0.668706,-0.03764 -0.990467,-0.09533 z" />
|
||||
<path
|
||||
d="m 15.96652,2.4766329 c -0.396657,0 -0.792437,0.1507259 -1.095095,0.4533829 L 3.57173,14.229712 c -0.3026571,0.302657 -0.5759103,0.751428 -0.7742385,1.227621 -0.1983282,0.47826 -0.3208556,0.99063 -0.3208556,1.418274 v 4.647756 h 4.6477558 c 0.427645,0 0.9376894,-0.122526 1.4159495,-0.320855 0.4782601,-0.198328 0.9272894,-0.471583 1.2299463,-0.774239 L 21.069984,9.1285734 c 0.302656,-0.302657 0.453382,-0.7017958 0.453382,-1.0974191 0,-0.3966562 -0.150726,-0.7924369 -0.453382,-1.0950939 L 17.063939,2.9300158 C 16.761281,2.6273588 16.362143,2.4766329 15.96652,2.4766329 Z m 0,2.2785396 3.275981,3.2759818 -1.334572,1.3368983 -3.275982,-3.2759819 z M 13.901884,6.8221334 15.173682,8.0939305 6.6082326,16.661704 5.3364357,15.387582 Z m 2.004185,2.0041848 1.271796,1.2717968 -8.5654475,8.565448 -1.2741221,-1.271796 z M 4.7063497,16.250176 c 0.010333,-0.02065 3.0644031,3.034179 3.0644031,3.034179 C 7.43814,19.422771 7.2039295,19.456408 7.1243917,19.456408 H 5.5759147 L 4.5435968,18.424084 v -1.548477 c 0,-0.07953 0.033633,-0.315548 0.1627529,-0.625436 z"
|
||||
style="display:inline;fill:#99cc99;fill-opacity:1;stroke-width:1.19007"
|
||||
id="path15590" />
|
||||
<path
|
||||
id="rect19553"
|
||||
d="m 2.4766359,18.937425 v 2.585938 h 2.5605469 z"
|
||||
style="display:inline;opacity:0.95;fill:#0a0a0a;fill-opacity:1;stroke:none;stroke-width:1.88249397;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg7030"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata7036">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs7034" />
|
||||
<g
|
||||
style="fill:#aeaeae;fill-opacity:1"
|
||||
transform="matrix(0.95238095,0,0,0.95238095,0.5714286,0.09523812)"
|
||||
id="g7028">
|
||||
<path
|
||||
style="fill:#aeaeae;fill-opacity:1"
|
||||
id="path7026"
|
||||
d="M 19.414,8.902 C 19.518,8.854 19.62,8.794 19.707,8.707 l 0.5,-0.5 c 0.391,-0.391 0.391,-1.023 0,-1.414 -0.391,-0.391 -1.023,-0.391 -1.414,0 l -0.5,0.5 -0.115,0.173 C 16.791,6.154 14.99,5.276 12.989,5.056 L 13,5 V 4 h 1 C 14.55,4 15,3.55 15,3 15,2.45 14.55,2 14,2 H 10 C 9.45,2 9,2.45 9,3 9,3.55 9.45,4 10,4 h 1 v 1 l 0.012,0.057 C 6.506,5.549 3,9.364 3,14 c 0,4.971 4.029,9 9,9 4.971,0 9,-4.029 9,-9 0,-1.894 -0.588,-3.648 -1.586,-5.098 z M 12,21 C 8.141,21 5,17.86 5,14 5,10.14 8.141,7 12,7 c 3.859,0 7,3.14 7,7 0,3.86 -3.141,7 -7,7 z m 1,-8 v -2 c 0,-0.55 -0.45,-1 -1,-1 -0.55,0 -1,0.45 -1,1 v 3 c 0,0.55 0.45,1 1,1 h 3 c 0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1 z M 12,8 c -3.312,0 -6,2.688 -6,6 0,3.312 2.688,6 6,6 3.312,0 6,-2.688 6,-6 0,-3.312 -2.688,-6 -6,-6 z m 0,11 C 9.243,19 7,16.757 7,14 7,11.243 9.243,9 12,9 c 2.757,0 5,2.243 5,5 0,2.757 -2.243,5 -5,5 z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -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]
|
||||
add = typ_plus.svg
|
||||
backward = typ_chevron-left.svg
|
||||
bookmark = typ_bookmark.svg
|
||||
bullet-off = typ_media-record-outline.svg
|
||||
bullet-on = typ_media-record.svg
|
||||
check = typ_tick.svg
|
||||
clear = typ_backspace.svg
|
||||
checked = mixed_input-checked.svg
|
||||
close = typ_times.svg
|
||||
cls_archive = typ_delete.svg
|
||||
cls_character = typ_user.svg
|
||||
@@ -35,32 +35,25 @@ cls_timeline = typ_calendar.svg
|
||||
cls_trash = typ_trash.svg
|
||||
cls_world = typ_location.svg
|
||||
cross = typ_times.svg
|
||||
delete = typ_delete.svg
|
||||
doc_h0 = mixed_heading0.svg
|
||||
doc_h1 = mixed_heading1.svg
|
||||
doc_h2 = mixed_heading2.svg
|
||||
doc_h3 = mixed_heading3.svg
|
||||
doc_h4 = mixed_heading4.svg
|
||||
done = typ_input-checked.svg
|
||||
down = typ_chevron-down.svg
|
||||
edit = typ_pencil.svg
|
||||
forward = typ_chevron-right.svg
|
||||
hash = typ_hash.svg
|
||||
maximise = typ_arrow-maximise.svg
|
||||
menu = typ_th-menu.svg
|
||||
minimise = typ_arrow-minimise.svg
|
||||
noncheckable = mixed_input-none.svg
|
||||
proj_chapter = mixed_document-chapter.svg
|
||||
proj_details = typ_th-list-grey.svg
|
||||
proj_document = typ_document-text.svg
|
||||
proj_folder = typ_folder.svg
|
||||
proj_note = mixed_document-note.svg
|
||||
proj_scene = mixed_document-scene.svg
|
||||
proj_section = mixed_document-section.svg
|
||||
proj_stats = typ_chart-bar-grey.svg
|
||||
proj_title = mixed_document-title.svg
|
||||
reference = typ_at.svg
|
||||
refresh = typ_refresh.svg
|
||||
remove = typ_minus.svg
|
||||
save = typ_download.svg
|
||||
search = typ_search.svg
|
||||
search_cancel = typ_cancel-grey.svg
|
||||
search_case = nw_search-case.svg
|
||||
@@ -78,13 +71,16 @@ status_stats = typ_chart-bar-grey.svg
|
||||
status_time = typ_stopwatch-grey.svg
|
||||
sticky-off = typ_pin-outline.svg
|
||||
sticky-on = typ_pin.svg
|
||||
unchecked = mixed_input-unchecked.svg
|
||||
up = typ_chevron-up.svg
|
||||
view_build = typ_export.svg
|
||||
view_editor = mixed_edit.svg
|
||||
view_novel = typ_book-grey.svg
|
||||
view_outline = typ_puzzle-outline.svg
|
||||
|
||||
deco_doc_h0 = nw_deco-h0.svg
|
||||
deco_doc_h1 = nw_deco-h1.svg
|
||||
deco_doc_h2 = nw_deco-h2.svg
|
||||
deco_doc_h3 = nw_deco-h3.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:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg2608"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
version="1.2"
|
||||
width="24"
|
||||
version="1.2">
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg901">
|
||||
<metadata
|
||||
id="metadata2614">
|
||||
id="metadata907">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
@@ -23,9 +23,9 @@
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs2612" />
|
||||
id="defs905" />
|
||||
<path
|
||||
style="stroke-width:1.05081;fill:#4271ae;fill-opacity:1"
|
||||
id="path2606"
|
||||
d="M 16.203226,2 H 7.796774 C 6.05874,2 4.6443545,3.4143855 4.6443545,5.1524195 V 19.86371 c 0,0.540115 0.1092839,0.994063 0.3236484,1.350287 0.5926549,0.982504 1.9072138,1.059213 2.9559187,0.0084 L 11.25708,17.889239 c 0.394052,-0.393002 1.091787,-0.393002 1.485841,0 l 3.333158,3.333158 C 16.592024,21.738349 17.128987,22 17.673304,22 c 0.837492,0 1.682342,-0.660957 1.682342,-2.13629 V 5.1524195 C 19.355646,3.4143855 17.941259,2 16.203226,2 Z M 7.796774,4.101613 h 8.406452 c 0.578994,0 1.050806,0.4718121 1.050806,1.0508065 V 15.560658 l -2.575527,-2.361162 c -1.477434,-1.35449 -3.880628,-1.353439 -5.3580615,0 l -2.574476,2.361162 V 5.1524195 c 0,-0.5789944 0.4718121,-1.0508065 1.0508065,-1.0508065 z m 6.431987,12.301792 C 13.635055,15.809699 12.843798,15.482898 12,15.482898 c -0.843797,0 -1.635054,0.327852 -2.2287605,0.920507 l -3.025272,3.025272 v -2.442075 l 3.2848215,-3.011611 c 1.085483,-0.995114 2.851889,-0.995114 3.937372,0 l 3.285871,3.011611 v 2.442075 z" />
|
||||
d="M 16.203226,2 H 7.796774 C 6.05874,2 4.6443545,3.4143855 4.6443545,5.1524195 V 19.86371 c 0,0.540115 0.1092839,0.994063 0.3236484,1.350287 0.5926549,0.982504 1.9072138,1.059213 2.9559187,0.0084 L 11.25708,17.889239 c 0.394052,-0.393002 1.091787,-0.393002 1.485841,0 l 3.333158,3.333158 C 16.592024,21.738349 17.128987,22 17.673304,22 c 0.837492,0 1.682342,-0.660957 1.682342,-2.13629 V 5.1524195 C 19.355646,3.4143855 17.941259,2 16.203226,2 Z M 7.796774,4.101613 h 8.406452 c 0.578994,0 1.050806,0.4718121 1.050806,1.0508065 V 15.560658 l -2.575527,-2.361162 c -1.477434,-1.35449 -3.880628,-1.353439 -5.3580615,0 l -2.574476,2.361162 V 5.1524195 c 0,-0.5789944 0.4718121,-1.0508065 1.0508065,-1.0508065 z m 6.431987,12.301792 C 13.635055,15.809699 12.843798,15.482898 12,15.482898 c -0.843797,0 -1.635054,0.327852 -2.2287605,0.920507 l -3.025272,3.025272 v -2.442075 l 3.2848215,-3.011611 c 1.085483,-0.995114 2.851889,-0.995114 3.937372,0 l 3.285871,3.011611 v 2.442075 z"
|
||||
id="path899"
|
||||
style="fill:#4271ae;fill-opacity:1;stroke-width:1.05081" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg13801">
|
||||
<metadata
|
||||
id="metadata13807">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs13805" />
|
||||
<path
|
||||
style="display:inline;fill:#a2694f;fill-opacity:1"
|
||||
id="path6288"
|
||||
d="m 20.987,16 c 0,-0.105 -0.004,-0.211 -0.039,-0.316 l -2,-6 C 18.812,9.275 18.431,9 18,9 h -0.219 c -0.094,0.188 -0.21,0.368 -0.367,0.525 L 15.932,11 h 1.348 l 1.667,5 H 5.054 L 6.721,11 H 8.069 L 6.586,9.525 C 6.429,9.368 6.312,9.188 6.219,9 H 6 C 5.569,9 5.188,9.275 5.052,9.684 l -2,6 C 3.017,15.789 3.013,15.895 3.013,16 3,16 3,21 3,21 c 0,0.553 0.447,1 1,1 h 16 c 0.553,0 1,-0.447 1,-1 0,0 0,-5 -0.013,-5 z" />
|
||||
<path
|
||||
d="M 16.707,7.404 C 16.518,7.216 16.259,7.121 16,7.121 c -0.259,0 -0.518,0.095 -0.707,0.283 L 13,9.697 V 3 C 13,2.448 12.552,2 12,2 11.448,2 11,2.448 11,3 V 9.697 L 8.707,7.404 C 8.518,7.216 8.267,7.111 8,7.111 c -0.267,0 -0.518,0.105 -0.707,0.293 -0.39,0.39 -0.39,1.024 0,1.414 L 12,13.5 16.709,8.816 C 17.097,8.429 17.097,7.794 16.707,7.404 Z"
|
||||
id="path13799"
|
||||
style="display:inline;fill:#718c00;fill-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="svg3369"
|
||||
viewBox="0 0 24 24"
|
||||
height="24"
|
||||
width="24"
|
||||
version="1.2">
|
||||
<metadata
|
||||
id="metadata3375">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs3373" />
|
||||
<path
|
||||
style="fill:#333333;fill-opacity:1;stroke-width:1"
|
||||
id="path3367"
|
||||
d="m 10.828125,20 h -4 c -0.553,0 -1,0.447 -1,1 0,0.553 0.447,1 1,1 h 10 c 0.553,0 1,-0.447 1,-1 0,-0.553 -0.447,-1 -1,-1 h -4 v -1.23 c 1.64,-0.371 3.146,-1.188 4.363,-2.406 1.7,-1.7 2.637,-3.96 2.637,-6.364 0,-2.067 -0.692,-4.029 -1.968,-5.619 l 0.675,-0.673 c 0.391,-0.391 0.391,-1.023 10e-4,-1.415 -0.391,-0.391 -1.024,-0.39 -1.415,-0.001 l -2.052,2.049 0.708,0.708 c 1.322,1.322 2.051,3.081 2.051,4.951 0,1.87 -0.729,3.627 -2.051,4.949 -1.322,1.322 -3.079,2.051 -4.949,2.051 -1.87,0 -3.627,-0.729 -4.949,-2.051 -0.391,-0.391 -1.023,-0.391 -1.414,0 -0.391,0.39 -0.391,1.023 0,1.414 1.699,1.7 3.959,2.637 6.363,2.637 z m 0,-16 c 1.657,0 3.157,0.672 4.243,1.757 1.085,1.086 1.757,2.586 1.757,4.243 0,1.656 -0.672,3.156 -1.757,4.242 -1.086,1.086 -2.586,1.758 -4.243,1.758 -1.658,0 -3.157,-0.672 -4.242,-1.757 -1.085,-1.086 -1.756,-2.586 -1.756,-4.243 0,-1.657 0.671,-3.157 1.756,-4.243 1.085,-1.085 2.584,-1.757 4.242,-1.757 z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,31 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg5204">
|
||||
<metadata
|
||||
id="metadata5210">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs5208" />
|
||||
<path
|
||||
id="rect928"
|
||||
d="M 7.75 2 C 6.7805 2 6 2.7805 6 3.75 L 6 5.5 L 4.1621094 5.5 C 3.2410844 5.5 2.5 6.2805 2.5 7.25 C 2.5 8.2195 3.2410844 9 4.1621094 9 L 6 9 L 6 15 L 4.1621094 15 C 3.2410844 15 2.5 15.7805 2.5 16.75 C 2.5 17.7195 3.2410844 18.5 4.1621094 18.5 L 6 18.5 L 6 20.25 C 6 21.2195 6.7805 22 7.75 22 C 8.7195 22 9.5 21.2195 9.5 20.25 L 9.5 18.5 L 14.5 18.5 L 14.5 20.25 C 14.5 21.2195 15.2805 22 16.25 22 C 17.2195 22 18 21.2195 18 20.25 L 18 18.5 L 19.837891 18.5 C 20.758916 18.5 21.5 17.7195 21.5 16.75 C 21.5 15.7805 20.758916 15 19.837891 15 L 18 15 L 18 9 L 19.837891 9 C 20.758916 9 21.5 8.2195 21.5 7.25 C 21.5 6.2805 20.758916 5.5 19.837891 5.5 L 18 5.5 L 18 3.75 C 18 2.7805 17.2195 2 16.25 2 C 15.2805 2 14.5 2.7805 14.5 3.75 L 14.5 5.5 L 9.5 5.5 L 9.5 3.75 C 9.5 2.7805 8.7195 2 7.75 2 z M 9.5 9 L 14.5 9 L 14.5 15 L 9.5 15 L 9.5 9 z "
|
||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.7544229;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,35 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg11367">
|
||||
<metadata
|
||||
id="metadata11373">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs11371" />
|
||||
<path
|
||||
d="m 13.499143,15.642429 c -0.45,0 -0.885857,-0.178715 -1.209857,-0.502715 L 8.859,11.710714 c -0.668571,-0.669857 -0.668571,-1.755 0,-2.423571 0.669857,-0.669857 1.755,-0.669857 2.426143,0 L 13.105714,11.109 17.573571,4.0954286 c 0.459,-0.828 1.504286,-1.1275715 2.332286,-0.6672857 0.826714,0.4602857 1.125,1.5042857 0.666,2.331 l -5.572286,9.0000001 c -0.261,0.470571 -0.727714,0.790714 -1.26,0.865286 z"
|
||||
id="path11365"
|
||||
style="display:inline;fill:#718c00;fill-opacity:1;stroke-width:1.28570998" />
|
||||
<path
|
||||
style="display:inline;fill:#333333;fill-opacity:1;stroke-width:1.28570998"
|
||||
id="path16196"
|
||||
d="M 17.142857,21 H 6.8571429 C 4.7305714,21 3,19.269429 3,17.142857 V 6.8571429 C 3,4.7305714 4.7305714,3 6.8571429,3 h 6.4285711 c 0.711,0 1.285715,0.576 1.285715,1.2857143 0,0.7097143 -0.574715,1.2857143 -1.285715,1.2857143 H 6.8571429 c -0.7097143,0 -1.2857143,0.5772857 -1.2857143,1.2857143 V 17.142857 c 0,0.708429 0.576,1.285714 1.2857143,1.285714 H 17.142857 c 0.709714,0 1.285714,-0.577285 1.285714,-1.285714 V 13.285714 C 18.428571,12.576 19.003286,12 19.714286,12 20.425286,12 21,12.576 21,13.285714 v 3.857143 C 21,19.269429 19.269429,21 17.142857,21 Z" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,36 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.2"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg7030">
|
||||
<metadata
|
||||
id="metadata7036">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs7034" />
|
||||
<g
|
||||
id="g7028"
|
||||
transform="matrix(0.95238095,0,0,0.95238095,0.5714286,0.09523812)"
|
||||
style="fill:#333333;fill-opacity:1">
|
||||
<path
|
||||
d="M 19.414,8.902 C 19.518,8.854 19.62,8.794 19.707,8.707 l 0.5,-0.5 c 0.391,-0.391 0.391,-1.023 0,-1.414 -0.391,-0.391 -1.023,-0.391 -1.414,0 l -0.5,0.5 -0.115,0.173 C 16.791,6.154 14.99,5.276 12.989,5.056 L 13,5 V 4 h 1 C 14.55,4 15,3.55 15,3 15,2.45 14.55,2 14,2 H 10 C 9.45,2 9,2.45 9,3 9,3.55 9.45,4 10,4 h 1 v 1 l 0.012,0.057 C 6.506,5.549 3,9.364 3,14 c 0,4.971 4.029,9 9,9 4.971,0 9,-4.029 9,-9 0,-1.894 -0.588,-3.648 -1.586,-5.098 z M 12,21 C 8.141,21 5,17.86 5,14 5,10.14 8.141,7 12,7 c 3.859,0 7,3.14 7,7 0,3.86 -3.141,7 -7,7 z m 1,-8 v -2 c 0,-0.55 -0.45,-1 -1,-1 -0.55,0 -1,0.45 -1,1 v 3 c 0,0.55 0.45,1 1,1 h 3 c 0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1 z M 12,8 c -3.312,0 -6,2.688 -6,6 0,3.312 2.688,6 6,6 3.312,0 6,-2.688 6,-6 0,-3.312 -2.688,-6 -6,-6 z m 0,11 C 9.243,19 7,16.757 7,14 7,11.243 9.243,9 12,9 c 2.757,0 5,2.243 5,5 0,2.757 -2.243,5 -5,5 z"
|
||||
id="path7026"
|
||||
style="fill:#333333;fill-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -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 |
@@ -2,11 +2,11 @@
|
||||
<html>
|
||||
<body>
|
||||
|
||||
<h2>Release Notes for 1.7 Beta 1</h2>
|
||||
<p><i>Released on 17 May 2022</i></p>
|
||||
<h2>Release Notes for 2.0 RC 2</h2>
|
||||
<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
|
||||
be careful when using this version on live writing projects, and make sure you take frequent
|
||||
<p>This is a release candidate of the next release version, and is intended for testing purposes.
|
||||
Please be careful when using this version on live writing projects, and make sure you take frequent
|
||||
backups.</p>
|
||||
<p>Please check the changelog for an overview of changes. The full release notes will be added to
|
||||
the final release.</p>
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
[Main]
|
||||
name = Default System Theme
|
||||
name = Default Theme
|
||||
description = Qt standard colours
|
||||
icontheme = typicons_light
|
||||
@@ -1,16 +1,18 @@
|
||||
[Main]
|
||||
name = Default Dark Theme
|
||||
author = Veronica Berglyd Olsen
|
||||
credit = Veronica Berglyd Olsen
|
||||
url = https://github.com/vkbo/novelWriter
|
||||
license = CC BY-SA 4.0
|
||||
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
||||
name = Default Dark Theme
|
||||
description = The novelWriter standard dark theme
|
||||
author = Veronica Berglyd Olsen
|
||||
credit = Veronica Berglyd Olsen
|
||||
url = https://github.com/vkbo/novelWriter
|
||||
license = CC BY-SA 4.0
|
||||
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
||||
icontheme = typicons_dark
|
||||
|
||||
[Palette]
|
||||
window = 54, 54, 54
|
||||
windowtext = 174, 174, 174
|
||||
base = 62, 62, 62
|
||||
alternatebase = 67, 67, 67
|
||||
alternatebase = 78, 78, 78
|
||||
text = 174, 174, 174
|
||||
tooltipbase = 255, 255, 192
|
||||
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/
|
||||
license = MIT
|
||||
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
|
||||
icontheme = typicons_dark
|
||||
|
||||
[Palette]
|
||||
window = 0, 43, 54
|
||||
windowtext = 253, 246, 227
|
||||
base = 7, 54, 66
|
||||
alternatebase = 67, 67, 67
|
||||
alternatebase = 0, 43, 54
|
||||
text = 253, 246, 227
|
||||
tooltipbase = 133, 153, 0
|
||||
tooltiptext = 0, 43, 54
|
||||
|
||||
@@ -5,6 +5,7 @@ credit = Ethan Schoonover
|
||||
url = https://ethanschoonover.com/solarized/
|
||||
license = MIT
|
||||
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
|
||||
icontheme = typicons_light
|
||||
|
||||
[Palette]
|
||||
window = 238, 232, 213
|
||||
|
||||
@@ -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/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
import hashlib
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from configparser import ConfigParser
|
||||
|
||||
@@ -45,52 +46,55 @@ logger = logging.getLogger(__name__)
|
||||
# Checker Functions
|
||||
# =============================================================================================== #
|
||||
|
||||
def checkString(value, default, allowNone=False):
|
||||
def checkStringNone(value, default):
|
||||
"""Check if a variable is a string or a None.
|
||||
"""
|
||||
if allowNone and (value is None or value == "None"):
|
||||
if value is None or value == "None":
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
return str(value)
|
||||
return default
|
||||
|
||||
|
||||
def checkInt(value, default, allowNone=False):
|
||||
"""Check if a variable is an integer or a None.
|
||||
def checkString(value, default):
|
||||
"""Check if a variable is a string.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
return str(value)
|
||||
return default
|
||||
|
||||
|
||||
def checkInt(value, default):
|
||||
"""Check if a variable is an integer.
|
||||
"""
|
||||
if allowNone and (value is None or value == "None"):
|
||||
return None
|
||||
try:
|
||||
return int(value)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def checkFloat(value, default, allowNone=False):
|
||||
"""Check if a variable is a float or a None.
|
||||
def checkFloat(value, default):
|
||||
"""Check if a variable is a float.
|
||||
"""
|
||||
if allowNone and (value is None or value == "None"):
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def checkBool(value, default, allowNone=False):
|
||||
"""Check if a variable is a boolean or a None.
|
||||
def checkBool(value, default):
|
||||
"""Check if a variable is a boolean.
|
||||
"""
|
||||
if allowNone and (value is None or value == "None"):
|
||||
return None
|
||||
|
||||
if isinstance(value, str):
|
||||
if value == "True":
|
||||
if isinstance(value, bool):
|
||||
return value
|
||||
elif isinstance(value, str):
|
||||
check = value.lower()
|
||||
if check in ("true", "yes", "on"):
|
||||
return True
|
||||
elif value == "False":
|
||||
elif check in ("false", "no", "off"):
|
||||
return False
|
||||
else:
|
||||
return default
|
||||
|
||||
elif isinstance(value, int):
|
||||
if value == 1:
|
||||
return True
|
||||
@@ -98,7 +102,6 @@ def checkBool(value, default, allowNone=False):
|
||||
return False
|
||||
else:
|
||||
return default
|
||||
|
||||
return default
|
||||
|
||||
|
||||
@@ -112,6 +115,26 @@ def checkHandle(value, default, allowNone=False):
|
||||
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
|
||||
# =============================================================================================== #
|
||||
@@ -174,16 +197,6 @@ def hexToInt(value, default=0):
|
||||
return default
|
||||
|
||||
|
||||
def checkIntRange(value, first, last, default):
|
||||
"""Check that an int is in a given range. If it isn't, return the
|
||||
default value.
|
||||
"""
|
||||
if isinstance(value, int):
|
||||
if value >= first and value <= last:
|
||||
return value
|
||||
return default
|
||||
|
||||
|
||||
def minmax(value, minVal, maxVal):
|
||||
"""Make sure an integer is between min and max value (inclusive).
|
||||
"""
|
||||
@@ -225,25 +238,25 @@ def formatInt(value):
|
||||
return str(value)
|
||||
|
||||
|
||||
def formatTimeStamp(theTime, fileSafe=False):
|
||||
def formatTimeStamp(value, fileSafe=False):
|
||||
"""Take a number (on the format returned by time.time()) and convert
|
||||
it to a timestamp string.
|
||||
"""
|
||||
if fileSafe:
|
||||
return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_FSTAMP)
|
||||
return datetime.fromtimestamp(value).strftime(nwConst.FMT_FSTAMP)
|
||||
else:
|
||||
return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_TSTAMP)
|
||||
return datetime.fromtimestamp(value).strftime(nwConst.FMT_TSTAMP)
|
||||
|
||||
|
||||
def formatTime(tS):
|
||||
def formatTime(t):
|
||||
"""Format a time in seconds in HH:MM:SS format or d-HH:MM:SS format
|
||||
if a full day or longer.
|
||||
"""
|
||||
if isinstance(tS, int):
|
||||
if tS >= 86400:
|
||||
return f"{tS//86400:d}-{tS%86400//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}"
|
||||
if isinstance(t, int):
|
||||
if t >= 86400:
|
||||
return f"{t//86400:d}-{t%86400//3600:02d}:{t%3600//60:02d}:{t%60:02d}"
|
||||
else:
|
||||
return f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}"
|
||||
return f"{t//3600:02d}:{t%3600//60:02d}:{t%60:02d}"
|
||||
return "ERROR"
|
||||
|
||||
|
||||
@@ -258,12 +271,18 @@ def simplified(string):
|
||||
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):
|
||||
"""Split a version string on the form aa.bb.cc into major, minor
|
||||
and patch, and computes an integer value aabbcc.
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
return [0, 0, 0, 0]
|
||||
return 0, 0, 0, 0
|
||||
|
||||
vMajor = 0
|
||||
vMinor = 0
|
||||
@@ -282,114 +301,114 @@ def splitVersionNumber(value):
|
||||
|
||||
vInt = vMajor*10000 + vMinor*100 + vPatch
|
||||
|
||||
return [vMajor, vMinor, vPatch, vInt]
|
||||
return vMajor, vMinor, vPatch, vInt
|
||||
|
||||
|
||||
def transferCase(theSource, theTarget):
|
||||
def transferCase(source, target):
|
||||
"""Transfers the case of the source word to the target word. This
|
||||
will consider all upper or lower, and first char capitalisation.
|
||||
"""
|
||||
theResult = theTarget
|
||||
theResult = target
|
||||
|
||||
if not isinstance(theSource, str) or not isinstance(theTarget, str):
|
||||
if not isinstance(source, str) or not isinstance(target, str):
|
||||
return theResult
|
||||
if len(theTarget) < 1 or len(theSource) < 1:
|
||||
if len(target) < 1 or len(source) < 1:
|
||||
return theResult
|
||||
|
||||
if theSource.istitle():
|
||||
theResult = theTarget.title()
|
||||
if source.istitle():
|
||||
theResult = target.title()
|
||||
|
||||
if theSource.isupper():
|
||||
theResult = theTarget.upper()
|
||||
elif theSource.islower():
|
||||
theResult = theTarget.lower()
|
||||
if source.isupper():
|
||||
theResult = target.upper()
|
||||
elif source.islower():
|
||||
theResult = target.lower()
|
||||
|
||||
return theResult
|
||||
|
||||
|
||||
def fuzzyTime(secDiff):
|
||||
def fuzzyTime(seconds):
|
||||
"""Converts a time difference in seconds into a fuzzy time string.
|
||||
"""
|
||||
if secDiff < 0:
|
||||
if seconds < 0:
|
||||
return QCoreApplication.translate(
|
||||
"Common", "in the future"
|
||||
)
|
||||
elif secDiff < 30:
|
||||
elif seconds < 30:
|
||||
return QCoreApplication.translate(
|
||||
"Common", "just now"
|
||||
)
|
||||
elif secDiff < 90:
|
||||
elif seconds < 90:
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a minute ago"
|
||||
)
|
||||
elif secDiff < 3300: # 55 minutes
|
||||
elif seconds < 3300: # 55 minutes
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} minutes ago"
|
||||
).format(int(round(secDiff/60)))
|
||||
elif secDiff < 5400: # 90 minutes
|
||||
).format(int(round(seconds/60)))
|
||||
elif seconds < 5400: # 90 minutes
|
||||
return QCoreApplication.translate(
|
||||
"Common", "an hour ago"
|
||||
)
|
||||
elif secDiff < 84600: # 23.5 hours
|
||||
elif seconds < 84600: # 23.5 hours
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} hours ago"
|
||||
).format(int(round(secDiff/3600)))
|
||||
elif secDiff < 129600: # 1.5 days
|
||||
).format(int(round(seconds/3600)))
|
||||
elif seconds < 129600: # 1.5 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a day ago"
|
||||
)
|
||||
elif secDiff < 561600: # 6.5 days
|
||||
elif seconds < 561600: # 6.5 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} days ago"
|
||||
).format(int(round(secDiff/86400)))
|
||||
elif secDiff < 907200: # 10.5 days
|
||||
).format(int(round(seconds/86400)))
|
||||
elif seconds < 907200: # 10.5 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a week ago"
|
||||
)
|
||||
elif secDiff < 2419200: # 28 days
|
||||
elif seconds < 2419200: # 28 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} weeks ago"
|
||||
).format(int(round(secDiff/604800)))
|
||||
elif secDiff < 3888000: # 45 days
|
||||
).format(int(round(seconds/604800)))
|
||||
elif seconds < 3888000: # 45 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a month ago"
|
||||
)
|
||||
elif secDiff < 29808000: # 345 days
|
||||
elif seconds < 29808000: # 345 days
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} months ago"
|
||||
).format(int(round(secDiff/2592000)))
|
||||
elif secDiff < 47336400: # 1.5 years
|
||||
).format(int(round(seconds/2592000)))
|
||||
elif seconds < 47336400: # 1.5 years
|
||||
return QCoreApplication.translate(
|
||||
"Common", "a year ago"
|
||||
)
|
||||
else:
|
||||
return QCoreApplication.translate(
|
||||
"Common", "{0} years ago"
|
||||
).format(int(round(secDiff/31557600)))
|
||||
).format(int(round(seconds/31557600)))
|
||||
|
||||
|
||||
def numberToRoman(numVal, toLower=False):
|
||||
def numberToRoman(value, toLower=False):
|
||||
"""Convert an integer to a Roman number.
|
||||
"""
|
||||
if not isinstance(numVal, int):
|
||||
if not isinstance(value, int):
|
||||
return "NAN"
|
||||
if numVal < 1 or numVal > 4999:
|
||||
if value < 1 or value > 4999:
|
||||
return "OOR"
|
||||
|
||||
theValues = [
|
||||
lookup = [
|
||||
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"),
|
||||
(50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
|
||||
]
|
||||
|
||||
romNum = ""
|
||||
for theDiv, theSym in theValues:
|
||||
n = numVal//theDiv
|
||||
romNum += n*theSym
|
||||
numVal -= n*theDiv
|
||||
if numVal <= 0:
|
||||
roman = ""
|
||||
for divisor, symbol in lookup:
|
||||
n = value//divisor
|
||||
roman += n*symbol
|
||||
value -= n*divisor
|
||||
if value <= 0:
|
||||
break
|
||||
|
||||
return romNum.lower() if toLower else romNum
|
||||
return roman.lower() if toLower else roman
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
@@ -447,51 +466,47 @@ def jsonEncode(data, n=0, nmax=0):
|
||||
# File and File System Functions
|
||||
# =============================================================================================== #
|
||||
|
||||
def readTextFile(filePath):
|
||||
def readTextFile(path):
|
||||
"""Read the content of a text file in a robust manner.
|
||||
"""
|
||||
if not os.path.isfile(filePath):
|
||||
path = Path(path)
|
||||
if not path.is_file():
|
||||
return ""
|
||||
|
||||
fileText = ""
|
||||
try:
|
||||
with open(filePath, mode="r", encoding="utf-8") as inFile:
|
||||
fileText = inFile.read()
|
||||
return path.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
logger.error("Could not read file: %s", filePath)
|
||||
logger.error("Could not read file: %s", path)
|
||||
logException()
|
||||
return ""
|
||||
|
||||
return fileText
|
||||
|
||||
|
||||
def makeFileNameSafe(value):
|
||||
"""Returns a filename safe string of the value.
|
||||
"""
|
||||
cleanName = ""
|
||||
clean = ""
|
||||
for c in str(value).strip():
|
||||
if c.isalpha() or c.isdigit() or c == " ":
|
||||
cleanName += c
|
||||
return cleanName
|
||||
clean += c
|
||||
return clean
|
||||
|
||||
|
||||
def sha256sum(filePath):
|
||||
def sha256sum(path):
|
||||
"""Make a shasum of a file using a buffer.
|
||||
Based on: https://stackoverflow.com/a/44873382/5825851
|
||||
"""
|
||||
hDigest = hashlib.sha256()
|
||||
digest = hashlib.sha256()
|
||||
bData = bytearray(65536)
|
||||
mData = memoryview(bData)
|
||||
try:
|
||||
with open(filePath, mode="rb", buffering=0) as inFile:
|
||||
with open(path, mode="rb", buffering=0) as inFile:
|
||||
for n in iter(lambda: inFile.readinto(mData), 0):
|
||||
hDigest.update(mData[:n])
|
||||
digest.update(mData[:n])
|
||||
except Exception:
|
||||
logger.error("Could not create sha256sum of: %s", filePath)
|
||||
logger.error("Could not create sha256sum of: %s", path)
|
||||
logException()
|
||||
return None
|
||||
|
||||
return hDigest.hexdigest()
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
# =============================================================================================== #
|
||||
@@ -513,87 +528,64 @@ def getGuiItem(objName):
|
||||
|
||||
class NWConfigParser(ConfigParser):
|
||||
|
||||
CNF_STR = 0
|
||||
CNF_INT = 1
|
||||
CNF_FLOAT = 2
|
||||
CNF_BOOL = 3
|
||||
CNF_S_LST = 4
|
||||
CNF_I_LST = 5
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def rdStr(self, section, option, default):
|
||||
"""Read string value.
|
||||
"""
|
||||
return self._parseLine(section, option, default, self.CNF_STR)
|
||||
return self.get(section, option, fallback=default)
|
||||
|
||||
def rdInt(self, section, option, default):
|
||||
"""Read integer value.
|
||||
"""
|
||||
return self._parseLine(section, option, default, self.CNF_INT)
|
||||
try:
|
||||
return self.getint(section, option, fallback=default)
|
||||
except ValueError:
|
||||
logger.error("Could not read '%s':'%s' from config", section, option)
|
||||
return default
|
||||
|
||||
def rdFlt(self, section, option, default):
|
||||
"""Read float value.
|
||||
"""
|
||||
return self._parseLine(section, option, default, self.CNF_FLOAT)
|
||||
try:
|
||||
return self.getfloat(section, option, fallback=default)
|
||||
except ValueError:
|
||||
logger.error("Could not read '%s':'%s' from config", section, option)
|
||||
return default
|
||||
|
||||
def rdBool(self, section, option, default):
|
||||
"""Read boolean value.
|
||||
"""
|
||||
return self._parseLine(section, option, default, self.CNF_BOOL)
|
||||
try:
|
||||
return self.getboolean(section, option, fallback=default)
|
||||
except ValueError:
|
||||
logger.error("Could not read '%s':'%s' from config", section, option)
|
||||
return default
|
||||
|
||||
def rdPath(self, section, option, default):
|
||||
"""Read a path value.
|
||||
"""
|
||||
return checkPath(self.get(section, option, fallback=default), default)
|
||||
|
||||
def rdStrList(self, section, option, default):
|
||||
"""Read string list.
|
||||
"""
|
||||
return self._parseLine(section, option, default, self.CNF_S_LST)
|
||||
result = default.copy() if isinstance(default, list) else []
|
||||
if self.has_option(section, option):
|
||||
data = self.get(section, option, fallback="").split(",")
|
||||
for i in range(min(len(data), len(result))):
|
||||
result[i] = data[i].strip()
|
||||
return result
|
||||
|
||||
def rdIntList(self, section, option, default):
|
||||
"""Read integer list.
|
||||
"""
|
||||
return self._parseLine(section, option, default, self.CNF_I_LST)
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _unpackList(self, value, default, type):
|
||||
"""Unpack a comma-separated string of items into a list.
|
||||
"""
|
||||
inList = value.split(",")
|
||||
outList = []
|
||||
if isinstance(default, list):
|
||||
outList = default.copy()
|
||||
for i in range(min(len(inList), len(outList))):
|
||||
try:
|
||||
if type == self.CNF_S_LST:
|
||||
outList[i] = inList[i].strip()
|
||||
elif type == self.CNF_I_LST:
|
||||
outList[i] = int(inList[i].strip())
|
||||
except Exception:
|
||||
continue
|
||||
return outList
|
||||
|
||||
def _parseLine(self, section, option, default, type):
|
||||
"""Parse a line and return the correct datatype.
|
||||
"""
|
||||
result = default.copy() if isinstance(default, list) else []
|
||||
if self.has_option(section, option):
|
||||
try:
|
||||
if type == self.CNF_STR:
|
||||
return self.get(section, option)
|
||||
elif type == self.CNF_INT:
|
||||
return self.getint(section, option)
|
||||
elif type == self.CNF_FLOAT:
|
||||
return self.getfloat(section, option)
|
||||
elif type == self.CNF_BOOL:
|
||||
return self.getboolean(section, option)
|
||||
elif type in (self.CNF_I_LST, self.CNF_S_LST):
|
||||
return self._unpackList(self.get(section, option), default, type)
|
||||
except ValueError:
|
||||
logger.error("Could not read '%s':'%s' from config", str(section), str(option))
|
||||
logException()
|
||||
return default
|
||||
|
||||
return default
|
||||
data = self.get(section, option, fallback="").split(",")
|
||||
for i in range(min(len(data), len(result))):
|
||||
result[i] = checkInt(data[i].strip(), result[i])
|
||||
return result
|
||||
|
||||
# END Class NWConfigParser
|
||||
|
||||
@@ -68,6 +68,7 @@ class nwHeaders:
|
||||
|
||||
class nwFiles:
|
||||
|
||||
CONF_FILE = "novelwriter.conf"
|
||||
PROJ_FILE = "nwProject.nwx"
|
||||
PROJ_DICT = "wordlist.txt"
|
||||
PROJ_LOCK = "nwProject.lock"
|
||||
@@ -157,6 +158,7 @@ class nwLabels:
|
||||
"doc_h1": QT_TRANSLATE_NOOP("Constant", "Novel Title Page"),
|
||||
"doc_h2": QT_TRANSLATE_NOOP("Constant", "Novel Chapter"),
|
||||
"doc_h3": QT_TRANSLATE_NOOP("Constant", "Novel Scene"),
|
||||
"doc_h4": QT_TRANSLATE_NOOP("Constant", "Novel Section"),
|
||||
"note": QT_TRANSLATE_NOOP("Constant", "Project Note"),
|
||||
}
|
||||
KEY_NAME = {
|
||||
|
||||
@@ -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/>.
|
||||
"""
|
||||
|
||||
from novelwriter.core.document import NWDoc
|
||||
from novelwriter.core.coretools import DocMerger, DocSplitter, ProjectBuilder
|
||||
from novelwriter.core.index import countWords
|
||||
from novelwriter.core.project import NWProject
|
||||
from novelwriter.core.spellcheck import NWSpellEnchant
|
||||
@@ -28,8 +28,10 @@ from novelwriter.core.toodt import ToOdt
|
||||
from novelwriter.core.tomd import ToMarkdown
|
||||
|
||||
__all__ = [
|
||||
"DocMerger",
|
||||
"DocSplitter",
|
||||
"ProjectBuilder",
|
||||
"countWords",
|
||||
"NWDoc",
|
||||
"NWProject",
|
||||
"NWSpellEnchant",
|
||||
"ToHtml",
|
||||
|
||||
@@ -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 2018–2022, 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
|
||||
@@ -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/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from novelwriter.enum import nwItemLayout, nwItemClass
|
||||
from novelwriter.error import formatException
|
||||
from novelwriter.common import isHandle, sha256sum
|
||||
@@ -33,7 +34,7 @@ from novelwriter.common import isHandle, sha256sum
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWDoc():
|
||||
class NWDocument:
|
||||
|
||||
def __init__(self, theProject, theHandle):
|
||||
|
||||
@@ -57,7 +58,7 @@ class NWDoc():
|
||||
return
|
||||
|
||||
def __repr__(self):
|
||||
return f"<NWDoc handle={self._docHandle}>"
|
||||
return f"<NWDocument handle={self._docHandle}>"
|
||||
|
||||
def __bool__(self):
|
||||
return self._docHandle is not None and bool(self._theItem)
|
||||
@@ -73,7 +74,7 @@ class NWDoc():
|
||||
empty string. If something went wrong, return None.
|
||||
"""
|
||||
self._docError = ""
|
||||
if self._docHandle is None:
|
||||
if not isinstance(self._docHandle, str):
|
||||
logger.error("No document handle set")
|
||||
return None
|
||||
|
||||
@@ -81,17 +82,22 @@ class NWDoc():
|
||||
logger.error("Unknown novelWriter document")
|
||||
return None
|
||||
|
||||
contentPath = self.theProject.storage.contentPath
|
||||
if not isinstance(contentPath, Path):
|
||||
logger.error("No content path set")
|
||||
return None
|
||||
|
||||
docFile = self._docHandle+".nwd"
|
||||
logger.debug("Opening document: %s", docFile)
|
||||
|
||||
docPath = os.path.join(self.theProject.projContent, docFile)
|
||||
docPath = contentPath / docFile
|
||||
self._fileLoc = docPath
|
||||
|
||||
theText = ""
|
||||
self._docMeta = {}
|
||||
self._prevHash = None
|
||||
|
||||
if os.path.isfile(docPath):
|
||||
if docPath.exists():
|
||||
self._prevHash = sha256sum(docPath)
|
||||
try:
|
||||
with open(docPath, mode="r", encoding="utf-8") as inFile:
|
||||
@@ -125,17 +131,20 @@ class NWDoc():
|
||||
if not.
|
||||
"""
|
||||
self._docError = ""
|
||||
if self._docHandle is None:
|
||||
if not isinstance(self._docHandle, str):
|
||||
logger.error("No document handle set")
|
||||
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"
|
||||
logger.debug("Saving document: %s", docFile)
|
||||
|
||||
docPath = os.path.join(self.theProject.projContent, docFile)
|
||||
docTemp = os.path.join(self.theProject.projContent, docFile+"~")
|
||||
docPath = contentPath / docFile
|
||||
docTemp = docPath.with_suffix(".tmp")
|
||||
|
||||
if self._prevHash is not None and not forceWrite:
|
||||
self._currHash = sha256sum(docPath)
|
||||
@@ -164,7 +173,7 @@ class NWDoc():
|
||||
# If we're here, the file was successfully saved, so we can
|
||||
# replace the temp file with the actual file
|
||||
try:
|
||||
os.replace(docTemp, docPath)
|
||||
docTemp.replace(docPath)
|
||||
except OSError as exc:
|
||||
self._docError = formatException(exc)
|
||||
return False
|
||||
@@ -179,23 +188,28 @@ class NWDoc():
|
||||
from the project data folder.
|
||||
"""
|
||||
self._docError = ""
|
||||
if self._docHandle is None:
|
||||
if not isinstance(self._docHandle, str):
|
||||
logger.error("No document handle set")
|
||||
return False
|
||||
|
||||
chkList = [
|
||||
os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd"),
|
||||
os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd~"),
|
||||
]
|
||||
contentPath = self.theProject.storage.contentPath
|
||||
if not isinstance(contentPath, Path):
|
||||
logger.error("No content path set")
|
||||
return False
|
||||
|
||||
for chkFile in chkList:
|
||||
if os.path.isfile(chkFile):
|
||||
try:
|
||||
os.unlink(chkFile)
|
||||
logger.debug("Deleted: %s", chkFile)
|
||||
except Exception as exc:
|
||||
self._docError = formatException(exc)
|
||||
return False
|
||||
docPath = contentPath / f"{self._docHandle}.nwd"
|
||||
docTemp = docPath.with_suffix(".tmp")
|
||||
|
||||
try:
|
||||
# ToDo: When Python 3.7 is dropped, these can be changed to
|
||||
# path.unlink(missing_ok=True)
|
||||
if docPath.exists():
|
||||
docPath.unlink()
|
||||
if docTemp.exists():
|
||||
docTemp.unlink()
|
||||
except Exception as exc:
|
||||
self._docError = formatException(exc)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -206,7 +220,7 @@ class NWDoc():
|
||||
def getFileLocation(self):
|
||||
"""Return the file location of the current document.
|
||||
"""
|
||||
return self._fileLoc
|
||||
return str(self._fileLoc)
|
||||
|
||||
def getCurrentItem(self):
|
||||
"""Return a pointer to the currently open NWItem.
|
||||
@@ -263,4 +277,4 @@ class NWDoc():
|
||||
|
||||
return
|
||||
|
||||
# END Class NWDoc
|
||||
# END Class NWDocument
|
||||
|
||||
@@ -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/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
|
||||
from time import time
|
||||
from pathlib import Path
|
||||
|
||||
from novelwriter.enum import nwItemType, nwItemLayout
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode, nwHeaders
|
||||
from novelwriter.core.document import NWDoc
|
||||
from novelwriter.common import (
|
||||
checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
|
||||
)
|
||||
@@ -59,23 +58,23 @@ class NWIndex:
|
||||
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
|
||||
self._tagsIndex = TagsIndex()
|
||||
self._itemIndex = ItemIndex(theProject)
|
||||
self._itemIndex = ItemIndex(project)
|
||||
self._indexBroken = False
|
||||
|
||||
# TimeStamps
|
||||
self._indexChange = 0
|
||||
self._indexChange = 0.0
|
||||
self._rootChange = {}
|
||||
|
||||
return
|
||||
|
||||
def __repr__(self):
|
||||
return f"<NWIndex project='{self.theProject.projName}'>"
|
||||
return f"<NWIndex project='{self._project.data.name}'>"
|
||||
|
||||
##
|
||||
# Properties
|
||||
@@ -94,10 +93,22 @@ class NWIndex:
|
||||
"""
|
||||
self._tagsIndex.clear()
|
||||
self._itemIndex.clear()
|
||||
self._indexChange = 0
|
||||
self._indexChange = 0.0
|
||||
self._rootChange = {}
|
||||
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):
|
||||
"""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
|
||||
project.
|
||||
"""
|
||||
if not self.theProject.tree.checkType(tHandle, nwItemType.FILE):
|
||||
if not self._project.tree.checkType(tHandle, nwItemType.FILE):
|
||||
return False
|
||||
|
||||
logger.debug("Re-indexing item '%s'", tHandle)
|
||||
theDoc = NWDoc(self.theProject, tHandle)
|
||||
theDoc = self._project.storage.getDocument(tHandle)
|
||||
self.scanText(tHandle, theDoc.readDocument() or "")
|
||||
|
||||
return True
|
||||
@@ -126,13 +137,13 @@ class NWIndex:
|
||||
def indexChangedSince(self, checkTime):
|
||||
"""Check if the index has changed since a given time.
|
||||
"""
|
||||
return self._indexChange > checkTime
|
||||
return self._indexChange > float(checkTime)
|
||||
|
||||
def rootChangedSince(self, rootHandle, checkTime):
|
||||
"""Check if the index has changed since a given time for a
|
||||
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
|
||||
@@ -141,12 +152,15 @@ class NWIndex:
|
||||
def loadIndex(self):
|
||||
"""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 = {}
|
||||
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
|
||||
tStart = time()
|
||||
|
||||
self._indexBroken = False
|
||||
if os.path.isfile(indexFile):
|
||||
if indexFile.exists():
|
||||
logger.debug("Loading index file")
|
||||
try:
|
||||
with open(indexFile, mode="r", encoding="utf-8") as inFile:
|
||||
@@ -169,14 +183,14 @@ class NWIndex:
|
||||
logger.debug("Checking index")
|
||||
|
||||
# Check that all files are indexed
|
||||
for fHandle in self.theProject.projFiles:
|
||||
for fHandle in self._project.projFiles:
|
||||
if fHandle not in self._itemIndex:
|
||||
logger.warning("Item '%s' is not in the index", 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
|
||||
|
||||
@@ -184,8 +198,11 @@ class NWIndex:
|
||||
"""Save the current index as a json file in the project meta
|
||||
data folder.
|
||||
"""
|
||||
indexFile = self._project.storage.getMetaFile(nwFiles.INDEX_FILE)
|
||||
if not isinstance(indexFile, Path):
|
||||
return False
|
||||
|
||||
logger.debug("Saving index file")
|
||||
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
|
||||
tStart = time()
|
||||
|
||||
try:
|
||||
@@ -202,7 +219,7 @@ class NWIndex:
|
||||
logException()
|
||||
return False
|
||||
|
||||
logger.verbose("Index saved in %.3f ms", (time() - tStart)*1000)
|
||||
logger.debug("Index saved in %.3f ms", (time() - tStart)*1000)
|
||||
|
||||
return True
|
||||
|
||||
@@ -217,11 +234,11 @@ class NWIndex:
|
||||
files before we save them, in which case we already have the
|
||||
text.
|
||||
"""
|
||||
theItem = self.theProject.tree[tHandle]
|
||||
theItem = self._project.tree[tHandle]
|
||||
if theItem is None:
|
||||
logger.info("Not indexing unknown item '%s'", tHandle)
|
||||
return False
|
||||
if theItem.itemType != nwItemType.FILE:
|
||||
if not theItem.isFileType():
|
||||
logger.info("Not indexing non-file item '%s'", tHandle)
|
||||
return False
|
||||
|
||||
@@ -243,20 +260,43 @@ class NWIndex:
|
||||
if theItem.itemParent is None:
|
||||
logger.info("Not indexing orphaned item '%s'", tHandle)
|
||||
return False
|
||||
if theItem.isInactive():
|
||||
logger.debug("Not indexing inactive item '%s'", tHandle)
|
||||
return False
|
||||
|
||||
logger.debug("Indexing item with handle '%s'", tHandle)
|
||||
if theItem.isInactive():
|
||||
self._scanInactive(theItem, theText)
|
||||
else:
|
||||
self._scanActive(tHandle, theItem, theText, itemTags)
|
||||
|
||||
# Scan the text content
|
||||
# Update timestamps for index changes
|
||||
nowTime = time()
|
||||
self._indexChange = nowTime
|
||||
self._rootChange[theItem.itemRoot] = nowTime
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Internal Indexer Helpers
|
||||
##
|
||||
|
||||
def _scanActive(self, tHandle, theItem, theText, itemTags):
|
||||
"""Scan an active document for meta data.
|
||||
"""
|
||||
nTitle = 0
|
||||
findHeader = True
|
||||
theLines = theText.splitlines()
|
||||
|
||||
for nLine, aLine in enumerate(theLines, start=1):
|
||||
|
||||
if len(aLine.strip()) == 0:
|
||||
continue
|
||||
|
||||
if aLine.startswith("#"):
|
||||
if findHeader:
|
||||
hDepth, _ = self._splitHeading(aLine)
|
||||
if hDepth != "H0":
|
||||
theItem.setMainHeading(hDepth)
|
||||
findHeader = False
|
||||
|
||||
isTitle = self._indexTitle(tHandle, aLine, nLine)
|
||||
if isTitle and nLine > 0:
|
||||
if nTitle > 0:
|
||||
@@ -289,48 +329,49 @@ class NWIndex:
|
||||
# Prune no longer used tags
|
||||
for tTag, isActive in itemTags.items():
|
||||
if not isActive:
|
||||
logger.verbose("Deleting removed tag '%s'", tTag)
|
||||
logger.debug("Deleting removed tag '%s'", tTag)
|
||||
del self._tagsIndex[tTag]
|
||||
|
||||
# Update timestamps for index changes
|
||||
nowTime = round(time())
|
||||
self._indexChange = nowTime
|
||||
self._rootChange[theItem.itemRoot] = nowTime
|
||||
return
|
||||
|
||||
return True
|
||||
def _scanInactive(self, theItem, theText):
|
||||
"""Scan an inactive document for meta data.
|
||||
"""
|
||||
for aLine in theText.splitlines():
|
||||
if aLine.startswith("#"):
|
||||
hDepth, _ = self._splitHeading(aLine)
|
||||
if hDepth != "H0":
|
||||
theItem.setMainHeading(hDepth)
|
||||
break
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Indexer Helpers
|
||||
##
|
||||
def _splitHeading(self, aLine):
|
||||
"""Split a heading into its header level and text value.
|
||||
"""
|
||||
if aLine.startswith("# "):
|
||||
return "H1", aLine[2:].strip()
|
||||
elif aLine.startswith("## "):
|
||||
return "H2", aLine[3:].strip()
|
||||
elif aLine.startswith("### "):
|
||||
return "H3", aLine[4:].strip()
|
||||
elif aLine.startswith("#### "):
|
||||
return "H4", aLine[5:].strip()
|
||||
elif aLine.startswith("#! "):
|
||||
return "H1", aLine[3:].strip()
|
||||
elif aLine.startswith("##! "):
|
||||
return "H2", aLine[4:].strip()
|
||||
return "H0", ""
|
||||
|
||||
def _indexTitle(self, tHandle, aLine, nTitle):
|
||||
"""Save information about the title and its location in the
|
||||
file to the index.
|
||||
"""
|
||||
if aLine.startswith("# "):
|
||||
hDepth = "H1"
|
||||
hText = aLine[2:].strip()
|
||||
elif aLine.startswith("## "):
|
||||
hDepth = "H2"
|
||||
hText = aLine[3:].strip()
|
||||
elif aLine.startswith("### "):
|
||||
hDepth = "H3"
|
||||
hText = aLine[4:].strip()
|
||||
elif aLine.startswith("#### "):
|
||||
hDepth = "H4"
|
||||
hText = aLine[5:].strip()
|
||||
elif aLine.startswith("#! "):
|
||||
hDepth = "H1"
|
||||
hText = aLine[3:].strip()
|
||||
elif aLine.startswith("##! "):
|
||||
hDepth = "H2"
|
||||
hText = aLine[4:].strip()
|
||||
else:
|
||||
hDepth, hText = self._splitHeading(aLine)
|
||||
if hDepth == "H0":
|
||||
return False
|
||||
|
||||
sTitle = f"T{nTitle:06d}"
|
||||
self._itemIndex.addItemHeading(tHandle, sTitle, hDepth, hText)
|
||||
|
||||
return True
|
||||
|
||||
def _indexWordCounts(self, tHandle, theText, nTitle):
|
||||
@@ -493,18 +534,15 @@ class NWIndex:
|
||||
for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle)
|
||||
]
|
||||
|
||||
def getHandleHeaderLevel(self, tHandle):
|
||||
"""Get the header level of the first header of a handle.
|
||||
"""
|
||||
return self._itemIndex.mainItemHeader(tHandle)
|
||||
|
||||
def getTableOfContents(self, maxDepth, skipExcl=True):
|
||||
def getTableOfContents(self, rootHandle, maxDepth, skipExcl=True):
|
||||
"""Generate a table of contents up to a maximum depth.
|
||||
"""
|
||||
tOrder = []
|
||||
tData = {}
|
||||
pKey = None
|
||||
for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl):
|
||||
for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(
|
||||
rootHandle=rootHandle, skipExcl=skipExcl
|
||||
):
|
||||
tKey = f"{tHandle}:{sTitle}"
|
||||
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
|
||||
if iLevel > maxDepth:
|
||||
@@ -647,23 +685,17 @@ class TagsIndex:
|
||||
def tagHandle(self, tagKey):
|
||||
"""Get the handle of a given tag.
|
||||
"""
|
||||
if tagKey in self._tags:
|
||||
return self._tags.get(tagKey).get("handle")
|
||||
return None
|
||||
return self._tags.get(tagKey, {}).get("handle", None)
|
||||
|
||||
def tagHeading(self, tagKey):
|
||||
"""Get the heading of a given tag.
|
||||
"""
|
||||
if tagKey in self._tags:
|
||||
return self._tags.get(tagKey).get("heading")
|
||||
return nwHeaders.TT_NONE
|
||||
return self._tags.get(tagKey, {}).get("heading", nwHeaders.TT_NONE)
|
||||
|
||||
def tagClass(self, tagKey):
|
||||
"""Get the class of a given tag.
|
||||
"""
|
||||
if tagKey in self._tags:
|
||||
return self._tags.get(tagKey).get("class")
|
||||
return None
|
||||
return self._tags.get(tagKey, {}).get("class", None)
|
||||
|
||||
##
|
||||
# Pack/Unpack
|
||||
@@ -717,8 +749,8 @@ class ItemIndex:
|
||||
IndexHeading object for each header of the text.
|
||||
"""
|
||||
|
||||
def __init__(self, theProject):
|
||||
self.theProject = theProject
|
||||
def __init__(self, project):
|
||||
self._project = project
|
||||
self._items = {}
|
||||
return
|
||||
|
||||
@@ -755,13 +787,6 @@ class ItemIndex:
|
||||
self._items[tHandle] = IndexItem(tHandle, tItem)
|
||||
return
|
||||
|
||||
def mainItemHeader(self, tHandle):
|
||||
"""Return the primary item header for an item.
|
||||
"""
|
||||
if tHandle in self._items:
|
||||
return self._items[tHandle].level
|
||||
return "H0"
|
||||
|
||||
def allItemTags(self, tHandle):
|
||||
"""Get all tags set for headings of an item.
|
||||
"""
|
||||
@@ -789,12 +814,12 @@ class ItemIndex:
|
||||
"""Iterate over all items and headers in the novel structure for
|
||||
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:
|
||||
continue
|
||||
if tItem.itemLayout == nwItemLayout.NOTE:
|
||||
if tItem.isNoteLayout():
|
||||
continue
|
||||
if skipExcl and not tItem.isExported:
|
||||
if skipExcl and not tItem.isActive:
|
||||
continue
|
||||
|
||||
tHandle = tItem.itemHandle
|
||||
@@ -807,8 +832,6 @@ class ItemIndex:
|
||||
elif tItem.itemRoot == rootHandle:
|
||||
for sTitle in self._items[tHandle].headings():
|
||||
yield tHandle, sTitle, self._items[tHandle][sTitle]
|
||||
else:
|
||||
continue
|
||||
|
||||
return
|
||||
|
||||
@@ -821,7 +844,6 @@ class ItemIndex:
|
||||
"""
|
||||
if tHandle in self._items:
|
||||
tItem = self._items[tHandle]
|
||||
tItem.updateLevel(hDepth)
|
||||
tItem.addHeading(IndexHeading(sTitle, hDepth, hText))
|
||||
return
|
||||
|
||||
@@ -875,7 +897,7 @@ class ItemIndex:
|
||||
if not isHandle(tHandle):
|
||||
raise ValueError("itemIndex keys must be handles")
|
||||
|
||||
nwItem = self.theProject.tree[tHandle]
|
||||
nwItem = self._project.tree[tHandle]
|
||||
if nwItem is not None:
|
||||
tItem = IndexItem(tHandle, nwItem)
|
||||
tItem.unpackData(tData)
|
||||
@@ -897,7 +919,6 @@ class IndexItem:
|
||||
def __init__(self, tHandle, tItem):
|
||||
self._handle = tHandle
|
||||
self._item = tItem
|
||||
self._level = "H0"
|
||||
self._headings = {}
|
||||
self._index = 0
|
||||
|
||||
@@ -917,21 +938,10 @@ class IndexItem:
|
||||
def item(self):
|
||||
return self._item
|
||||
|
||||
@property
|
||||
def level(self):
|
||||
return self._level
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
|
||||
def updateLevel(self, level):
|
||||
"""Set the level only if it has not already been set.
|
||||
"""
|
||||
if self._level == "H0":
|
||||
self._level = level
|
||||
return
|
||||
|
||||
def addHeading(self, tHeading):
|
||||
"""Add a heading to the item. Also remove the placeholder entry
|
||||
if it exists.
|
||||
@@ -1011,7 +1021,7 @@ class IndexItem:
|
||||
if hRefs:
|
||||
refs[sTitle] = hRefs
|
||||
|
||||
data = {"level": self._level}
|
||||
data = {}
|
||||
data["headings"] = heads
|
||||
if refs:
|
||||
data["references"] = refs
|
||||
@@ -1021,7 +1031,6 @@ class IndexItem:
|
||||
def unpackData(self, data):
|
||||
"""Unpack an item entry from the data.
|
||||
"""
|
||||
self._level = data.get("level", "H0")
|
||||
references = data.get("references", {})
|
||||
for sTitle, hData in data.get("headings", {}).items():
|
||||
if not isTitleTag(sTitle):
|
||||
@@ -1030,6 +1039,7 @@ class IndexItem:
|
||||
tHeading.unpackData(hData)
|
||||
tHeading.unpackReferences(references.get(sTitle, {}))
|
||||
self.addHeading(tHeading)
|
||||
|
||||
return
|
||||
|
||||
# END Class IndexItem
|
||||
|
||||
@@ -25,23 +25,27 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
|
||||
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__)
|
||||
|
||||
|
||||
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._handle = None
|
||||
self._parent = None
|
||||
@@ -52,15 +56,16 @@ class NWItem():
|
||||
self._layout = nwItemLayout.NO_LAYOUT
|
||||
self._status = None
|
||||
self._import = None
|
||||
self._active = True
|
||||
self._expanded = False
|
||||
self._exported = True
|
||||
|
||||
# Document Meta Data
|
||||
self._charCount = 0 # Current character count
|
||||
self._wordCount = 0 # Current word count
|
||||
self._paraCount = 0 # Current paragraph count
|
||||
self._cursorPos = 0 # Last cursor position
|
||||
self._initCount = 0 # Initial word count
|
||||
self._heading = "H0" # The main heading
|
||||
self._charCount = 0 # Current character count
|
||||
self._wordCount = 0 # Current word count
|
||||
self._paraCount = 0 # Current paragraph count
|
||||
self._cursorPos = 0 # Last cursor position
|
||||
self._initCount = 0 # Initial word count
|
||||
|
||||
return
|
||||
|
||||
@@ -114,13 +119,17 @@ class NWItem():
|
||||
def itemImport(self):
|
||||
return self._import
|
||||
|
||||
@property
|
||||
def isActive(self):
|
||||
return self._active
|
||||
|
||||
@property
|
||||
def isExpanded(self):
|
||||
return self._expanded
|
||||
|
||||
@property
|
||||
def isExported(self):
|
||||
return self._exported
|
||||
def mainHeading(self):
|
||||
return self._heading
|
||||
|
||||
@property
|
||||
def charCount(self):
|
||||
@@ -143,101 +152,75 @@ class NWItem():
|
||||
return self._cursorPos
|
||||
|
||||
##
|
||||
# XML Pack/Unpack
|
||||
# Pack/Unpack Data
|
||||
##
|
||||
|
||||
def packXML(self, xParent):
|
||||
"""Pack all the data in the class instance into an XML object.
|
||||
def pack(self):
|
||||
"""Pack all the data in the class instance into a dictionary.
|
||||
"""
|
||||
itemAttrib = {}
|
||||
itemAttrib["handle"] = str(self._handle)
|
||||
itemAttrib["parent"] = str(self._parent)
|
||||
itemAttrib["root"] = str(self._root)
|
||||
itemAttrib["order"] = str(self._order)
|
||||
itemAttrib["type"] = str(self._type.name)
|
||||
itemAttrib["class"] = str(self._class.name)
|
||||
item = {}
|
||||
meta = {}
|
||||
name = {}
|
||||
|
||||
item["handle"] = str(self._handle)
|
||||
item["parent"] = str(self._parent)
|
||||
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:
|
||||
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 = {}
|
||||
metaAttrib["expanded"] = str(self._expanded)
|
||||
if self._type == nwItemType.FILE:
|
||||
metaAttrib["charCount"] = str(self._charCount)
|
||||
metaAttrib["wordCount"] = str(self._wordCount)
|
||||
metaAttrib["paraCount"] = str(self._paraCount)
|
||||
metaAttrib["cursorPos"] = str(self._cursorPos)
|
||||
data = {
|
||||
"name": str(self._name),
|
||||
"itemAttr": item,
|
||||
"metaAttr": meta,
|
||||
"nameAttr": name,
|
||||
}
|
||||
|
||||
nameAttrib = {}
|
||||
nameAttrib["status"] = str(self._status)
|
||||
nameAttrib["import"] = str(self._import)
|
||||
if self._type == nwItemType.FILE:
|
||||
nameAttrib["exported"] = str(self._exported)
|
||||
return data
|
||||
|
||||
xPack = etree.SubElement(xParent, "item", attrib=itemAttrib)
|
||||
self._subPack(xPack, "meta", attrib=metaAttrib)
|
||||
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'.
|
||||
def unpack(self, data):
|
||||
"""Set the values from a data dictionary.
|
||||
"""
|
||||
if xItem.tag != "item":
|
||||
logger.error("XML entry is not an NWItem")
|
||||
return False
|
||||
item = data.get("itemAttr", {})
|
||||
meta = data.get("metaAttr", {})
|
||||
name = data.get("nameAttr", {})
|
||||
|
||||
if "handle" in xItem.attrib:
|
||||
self.setHandle(xItem.attrib["handle"])
|
||||
if "handle" in item:
|
||||
self.setHandle(item["handle"])
|
||||
else:
|
||||
logger.error("XML item entry does not have a handle")
|
||||
logger.error("Item does not have a handle")
|
||||
return False
|
||||
|
||||
self.setParent(xItem.attrib.get("parent", None))
|
||||
self.setRoot(xItem.attrib.get("root", None))
|
||||
self.setOrder(xItem.attrib.get("order", 0))
|
||||
self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE))
|
||||
self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS))
|
||||
self.setLayout(xItem.attrib.get("layout", nwItemLayout.NO_LAYOUT))
|
||||
self.setName(data.get("name", ""))
|
||||
self.setParent(item.get("parent", None))
|
||||
self.setRoot(item.get("root", None))
|
||||
self.setOrder(item.get("order", 0))
|
||||
self.setType(item.get("type", nwItemType.NO_TYPE))
|
||||
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 xValue.tag == "meta":
|
||||
self.setExpanded(xValue.attrib.get("expanded", False))
|
||||
self.setCharCount(xValue.attrib.get("charCount", 0))
|
||||
self.setWordCount(xValue.attrib.get("wordCount", 0))
|
||||
self.setParaCount(xValue.attrib.get("paraCount", 0))
|
||||
self.setCursorPos(xValue.attrib.get("cursorPos", 0))
|
||||
elif xValue.tag == "name":
|
||||
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)
|
||||
if self._type == nwItemType.FILE:
|
||||
self.setLayout(item.get("layout", nwItemLayout.NO_LAYOUT))
|
||||
self.setMainHeading(meta.get("heading", "H0"))
|
||||
self.setCharCount(meta.get("charCount", 0))
|
||||
self.setWordCount(meta.get("wordCount", 0))
|
||||
self.setParaCount(meta.get("paraCount", 0))
|
||||
self.setCursorPos(meta.get("cursorPos", 0))
|
||||
self.setActive(name.get("active", True))
|
||||
|
||||
# Make some checks to ensure consistency
|
||||
if self._type == nwItemType.ROOT:
|
||||
@@ -245,31 +228,22 @@ class NWItem():
|
||||
self._parent = None # Root items cannot have a parent
|
||||
|
||||
if self._type != nwItemType.FILE:
|
||||
self._charCount = 0 # Only set for files
|
||||
self._wordCount = 0 # Only set for files
|
||||
self._paraCount = 0 # Only set for files
|
||||
self._cursorPos = 0 # Only set for files
|
||||
# Reset values that should only be set for files
|
||||
self._layout = nwItemLayout.NO_LAYOUT
|
||||
self._heading = "H0"
|
||||
self._active = False
|
||||
self._charCount = 0
|
||||
self._wordCount = 0
|
||||
self._paraCount = 0
|
||||
self._cursorPos = 0
|
||||
|
||||
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
|
||||
##
|
||||
|
||||
def describeMe(self, hLevel=None):
|
||||
def describeMe(self):
|
||||
"""Return a string description of the item.
|
||||
"""
|
||||
descKey = "none"
|
||||
@@ -279,12 +253,14 @@ class NWItem():
|
||||
descKey = "folder"
|
||||
elif self._type == nwItemType.FILE:
|
||||
if self._layout == nwItemLayout.DOCUMENT:
|
||||
if hLevel == "H1":
|
||||
if self._heading == "H1":
|
||||
descKey = "doc_h1"
|
||||
elif hLevel == "H2":
|
||||
elif self._heading == "H2":
|
||||
descKey = "doc_h2"
|
||||
elif hLevel == "H3":
|
||||
elif self._heading == "H3":
|
||||
descKey = "doc_h3"
|
||||
elif self._heading == "H4":
|
||||
descKey = "doc_h4"
|
||||
else:
|
||||
descKey = "document"
|
||||
elif self._layout == nwItemLayout.NOTE:
|
||||
@@ -292,6 +268,22 @@ class NWItem():
|
||||
|
||||
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):
|
||||
"""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)
|
||||
|
||||
def getImportStatus(self):
|
||||
"""Return the relevant importance or status label and icon for
|
||||
the current item based on its class.
|
||||
"""
|
||||
if self.isNovelLike():
|
||||
stName = self.theProject.statusItems.name(self._status)
|
||||
stIcon = self.theProject.statusItems.icon(self._status)
|
||||
else:
|
||||
stName = self.theProject.importItems.name(self._import)
|
||||
stIcon = self.theProject.importItems.icon(self._import)
|
||||
return stName, stIcon
|
||||
def isRootType(self):
|
||||
return self._type == nwItemType.ROOT
|
||||
|
||||
def isFolderType(self):
|
||||
return self._type == nwItemType.FOLDER
|
||||
|
||||
def isFileType(self):
|
||||
return self._type == nwItemType.FILE
|
||||
|
||||
def isNoteLayout(self):
|
||||
return self._layout == nwItemLayout.NOTE
|
||||
|
||||
def isDocumentLayout(self):
|
||||
return self._layout == nwItemLayout.DOCUMENT
|
||||
|
||||
##
|
||||
# Special Setters
|
||||
@@ -419,8 +414,6 @@ class NWItem():
|
||||
self._type = value
|
||||
elif isItemType(value):
|
||||
self._type = nwItemType[value]
|
||||
elif value == "TRASH":
|
||||
self._type = nwItemType.ROOT
|
||||
else:
|
||||
logger.error("Unrecognised item type '%s'", value)
|
||||
self._type = nwItemType.NO_TYPE
|
||||
@@ -447,8 +440,6 @@ class NWItem():
|
||||
self._layout = value
|
||||
elif isItemLayout(value):
|
||||
self._layout = nwItemLayout[value]
|
||||
elif value in ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"):
|
||||
self._layout = nwItemLayout.DOCUMENT
|
||||
else:
|
||||
logger.error("Unrecognised item layout '%s'", value)
|
||||
self._layout = nwItemLayout.NO_LAYOUT
|
||||
@@ -458,60 +449,79 @@ class NWItem():
|
||||
"""Set the item status by looking it up in the valid status
|
||||
items of the current project.
|
||||
"""
|
||||
self._status = self.theProject.statusItems.check(value)
|
||||
self._status = self._project.data.itemStatus.check(value)
|
||||
return
|
||||
|
||||
def setImport(self, value):
|
||||
"""Set the item importance by looking it up in the valid import
|
||||
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
|
||||
|
||||
def setExpanded(self, state):
|
||||
"""Set the expanded status of an item in the project tree.
|
||||
"""
|
||||
if isinstance(state, str):
|
||||
self._expanded = (state == str(True))
|
||||
if isinstance(state, bool):
|
||||
self._expanded = state
|
||||
else:
|
||||
self._expanded = (state is True)
|
||||
return
|
||||
|
||||
def setExported(self, state):
|
||||
"""Set the export flag.
|
||||
"""
|
||||
if isinstance(state, str):
|
||||
self._exported = (state == str(True))
|
||||
else:
|
||||
self._exported = (state is True)
|
||||
self._expanded = False
|
||||
return
|
||||
|
||||
##
|
||||
# Set Document Meta Data
|
||||
##
|
||||
|
||||
def setMainHeading(self, value):
|
||||
"""Set the main heading level.
|
||||
"""
|
||||
if value in nwHeaders.H_LEVEL:
|
||||
self._heading = value
|
||||
return
|
||||
|
||||
def setCharCount(self, count):
|
||||
"""Set the character count, and ensure that it is an integer.
|
||||
"""
|
||||
self._charCount = max(0, checkInt(count, 0))
|
||||
if isinstance(count, int):
|
||||
self._charCount = max(0, count)
|
||||
else:
|
||||
self._charCount = 0
|
||||
return
|
||||
|
||||
def setWordCount(self, count):
|
||||
"""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
|
||||
|
||||
def setParaCount(self, count):
|
||||
"""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
|
||||
|
||||
def setCursorPos(self, position):
|
||||
"""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
|
||||
|
||||
def saveInitialCount(self):
|
||||
|
||||
@@ -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/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from novelwriter.error import logException
|
||||
from novelwriter.common import checkBool, checkFloat, checkInt, checkString
|
||||
@@ -40,30 +40,30 @@ VALID_MAP = {
|
||||
"GuiWritingStats": {
|
||||
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
|
||||
"widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes",
|
||||
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax"
|
||||
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax",
|
||||
},
|
||||
"GuiDocSplit": {"spLevel"},
|
||||
"GuiDocSplit": {"spLevel", "intoFolder", "docHierarchy"},
|
||||
"GuiBuildNovel": {
|
||||
"winWidth", "winHeight", "boxWidth", "docWidth", "hideScene",
|
||||
"hideSection", "addNovel", "addNotes", "ignoreFlag", "justifyText",
|
||||
"excludeBody", "textFont", "textSize", "lineHeight", "noStyling",
|
||||
"incSynopsis", "incComments", "incKeywords", "incBodyText",
|
||||
"replaceTabs", "replaceUCode"
|
||||
"replaceTabs", "replaceUCode", "rootFilter",
|
||||
},
|
||||
"GuiOutline": {"headerOrder", "columnWidth", "columnHidden"},
|
||||
"GuiProjectSettings": {
|
||||
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW"
|
||||
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW",
|
||||
},
|
||||
"GuiProjectDetails": {
|
||||
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
|
||||
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble"
|
||||
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
|
||||
},
|
||||
"GuiWordList": {"winWidth", "winHeight"},
|
||||
"GuiNovelView": {"lastCol"},
|
||||
}
|
||||
|
||||
|
||||
class OptionState():
|
||||
class OptionState:
|
||||
|
||||
def __init__(self, theProject):
|
||||
self.theProject = theProject
|
||||
@@ -77,13 +77,12 @@ class OptionState():
|
||||
def loadSettings(self):
|
||||
"""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
|
||||
|
||||
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
|
||||
theState = {}
|
||||
|
||||
if os.path.isfile(stateFile):
|
||||
if stateFile.exists():
|
||||
logger.debug("Loading GUI options file")
|
||||
try:
|
||||
with open(stateFile, mode="r", encoding="utf-8") as inFile:
|
||||
@@ -106,12 +105,11 @@ class OptionState():
|
||||
def saveSettings(self):
|
||||
"""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
|
||||
|
||||
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
|
||||
logger.debug("Saving GUI options file")
|
||||
|
||||
try:
|
||||
with open(stateFile, mode="w+", encoding="utf-8") as outFile:
|
||||
json.dump(self._theState, outFile, indent=2)
|
||||
@@ -188,8 +186,7 @@ class OptionState():
|
||||
the default value.
|
||||
"""
|
||||
if group in self._theState:
|
||||
if name in self._theState[group]:
|
||||
return checkBool(self._theState[group].get(name, default), default)
|
||||
return checkBool(self._theState[group].get(name, default), default)
|
||||
return default
|
||||
|
||||
def getEnum(self, group, name, lookup, default):
|
||||
|
||||
@@ -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 2018–2022, 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
|
||||
@@ -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 2018–2022, 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
|
||||
@@ -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/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
|
||||
from collections import namedtuple
|
||||
from pathlib import Path
|
||||
|
||||
from novelwriter.error import logException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWSpellEnchant():
|
||||
class NWSpellEnchant:
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -46,36 +48,47 @@ class NWSpellEnchant():
|
||||
return
|
||||
|
||||
##
|
||||
# Getters and Setters
|
||||
# Properties
|
||||
##
|
||||
|
||||
@property
|
||||
def spellLanguage(self):
|
||||
return self._spellLanguage
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
|
||||
def setLanguage(self, theLang, projectDict=None):
|
||||
"""Load a dictionary for the language specified in the config.
|
||||
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:
|
||||
import enchant
|
||||
if self._theBroker is not None:
|
||||
logger.debug("Deleting old pyenchant broker")
|
||||
del self._theBroker
|
||||
|
||||
self._theBroker = enchant.Broker()
|
||||
self._theDict = self._theBroker.request_dict(theLang)
|
||||
self._spellLanguage = theLang
|
||||
logger.debug("Enchant spell checking for language '%s' loaded", theLang)
|
||||
if theLang and enchant.dict_exists(theLang):
|
||||
self._theBroker = enchant.Broker()
|
||||
self._theDict = self._theBroker.request_dict(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:
|
||||
logger.error("Failed to load enchant spell checking for language '%s'", theLang)
|
||||
self._theDict = FakeEnchant()
|
||||
self._spellLanguage = None
|
||||
|
||||
self._readProjectDictionary(projectDict)
|
||||
for pWord in self._projDict:
|
||||
self._theDict.add_to_session(pWord)
|
||||
if self._theDict is None:
|
||||
self._theDict = FakeEnchant()
|
||||
else:
|
||||
self._readProjectDictionary(projectDict)
|
||||
for pWord in self._projDict:
|
||||
self._theDict.add_to_session(pWord)
|
||||
|
||||
return
|
||||
|
||||
@@ -160,10 +173,10 @@ class NWSpellEnchant():
|
||||
self._projDict = set()
|
||||
self._projectDict = projectDict
|
||||
|
||||
if projectDict is None:
|
||||
if not isinstance(projectDict, Path):
|
||||
return False
|
||||
|
||||
if not os.path.isfile(projectDict):
|
||||
if not projectDict.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -189,6 +202,9 @@ class FakeEnchant:
|
||||
"""Fallback for when Enchant is selected, but not installed.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.tag = ""
|
||||
self.provider = namedtuple("provider", "name")
|
||||
self.provider.name = ""
|
||||
return
|
||||
|
||||
def check(self, theWord):
|
||||
|
||||
@@ -28,16 +28,15 @@ import random
|
||||
import logging
|
||||
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 checkInt, minmax, simplified
|
||||
from novelwriter.common import minmax, simplified
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWStatus():
|
||||
class NWStatus:
|
||||
|
||||
STATUS = 1
|
||||
IMPORT = 2
|
||||
@@ -46,13 +45,17 @@ class NWStatus():
|
||||
|
||||
self._type = type
|
||||
self._store = {}
|
||||
self._reverse = {}
|
||||
self._default = None
|
||||
|
||||
self._iconSize = novelwriter.CONFIG.pxInt(32)
|
||||
pixmap = QPixmap(self._iconSize, self._iconSize)
|
||||
pixmap.fill(QColor(100, 100, 100))
|
||||
self._defaultIcon = QIcon(pixmap)
|
||||
self._iPX = novelwriter.CONFIG.pxInt(24)
|
||||
|
||||
pA = novelwriter.CONFIG.pxInt(2)
|
||||
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:
|
||||
self._prefix = "s"
|
||||
@@ -63,31 +66,30 @@ class NWStatus():
|
||||
|
||||
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
|
||||
key is generated.
|
||||
"""
|
||||
if not self._isKey(key):
|
||||
key = self._newKey()
|
||||
if not isinstance(cols, tuple):
|
||||
cols = (100, 100, 100)
|
||||
if len(cols) != 3:
|
||||
cols = (100, 100, 100)
|
||||
|
||||
pixmap = QPixmap(self._iconSize, self._iconSize)
|
||||
pixmap.fill(QColor(*cols))
|
||||
if not isinstance(col, tuple):
|
||||
col = (100, 100, 100)
|
||||
if len(col) != 3:
|
||||
col = (100, 100, 100)
|
||||
|
||||
cR = minmax(col[0], 0, 255)
|
||||
cG = minmax(col[1], 0, 255)
|
||||
cB = minmax(col[2], 0, 255)
|
||||
name = simplified(name)
|
||||
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] = {
|
||||
"name": name,
|
||||
"icon": QIcon(pixmap),
|
||||
"cols": cols,
|
||||
"icon": self._createIcon(cR, cG, cB),
|
||||
"cols": (cR, cG, cB),
|
||||
"count": count,
|
||||
}
|
||||
self._reverse[name] = key
|
||||
|
||||
if self._default is None:
|
||||
self._default = key
|
||||
@@ -103,7 +105,6 @@ class NWStatus():
|
||||
if self._store[key]["count"] > 0:
|
||||
return False
|
||||
|
||||
del self._reverse[self._store[key]["name"]]
|
||||
del self._store[key]
|
||||
|
||||
keys = list(self._store.keys())
|
||||
@@ -120,8 +121,6 @@ class NWStatus():
|
||||
"""
|
||||
if self._isKey(value) and value in self._store:
|
||||
return value
|
||||
elif value in self._reverse:
|
||||
return self._reverse[value]
|
||||
elif self._default is not None:
|
||||
return self._default
|
||||
else:
|
||||
@@ -203,37 +202,30 @@ class NWStatus():
|
||||
self._store[key]["count"] += 1
|
||||
return
|
||||
|
||||
def packXML(self, xParent):
|
||||
"""Pack the status entries into an XML object for saving to the
|
||||
main project file.
|
||||
def pack(self):
|
||||
"""Pack the status entries into a dictionary.
|
||||
"""
|
||||
for key, data in self._store.items():
|
||||
xSub = etree.SubElement(xParent, "entry", attrib={
|
||||
yield (data["name"], {
|
||||
"key": key,
|
||||
"count": str(data["count"]),
|
||||
"red": str(data["cols"][0]),
|
||||
"green": str(data["cols"][1]),
|
||||
"blue": str(data["cols"][2]),
|
||||
})
|
||||
xSub.text = data["name"]
|
||||
return
|
||||
|
||||
return True
|
||||
|
||||
def unpackXML(self, xParent):
|
||||
"""Unpack an XML tree and set the class values.
|
||||
def unpack(self, data):
|
||||
"""Unpack a data dictionary and set the class values.
|
||||
"""
|
||||
self._store = {}
|
||||
self._reverse = {}
|
||||
self._default = None
|
||||
|
||||
for xChild in xParent:
|
||||
key = xChild.attrib.get("key", None)
|
||||
name = xChild.text.strip()
|
||||
count = max(checkInt(xChild.attrib.get("count", 0), 0), 0)
|
||||
red = minmax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255)
|
||||
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)
|
||||
for key, entry in data.items():
|
||||
label = entry.get("label", "")
|
||||
colour = entry.get("colour", (100, 100, 100))
|
||||
count = entry.get("count", 0)
|
||||
self.write(key, label, colour, count)
|
||||
|
||||
return True
|
||||
|
||||
@@ -267,6 +259,19 @@ class NWStatus():
|
||||
return False
|
||||
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
|
||||
##
|
||||
|
||||
@@ -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 2018–2022, 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
|
||||
@@ -38,7 +38,7 @@ class ToHtml(Tokenizer):
|
||||
M_EBOOK = 2 # Tweak output for converting to epub
|
||||
|
||||
def __init__(self, theProject):
|
||||
Tokenizer.__init__(self, theProject)
|
||||
super().__init__(theProject)
|
||||
|
||||
self._genMode = self.M_EXPORT
|
||||
self._cssStyles = True
|
||||
@@ -107,7 +107,7 @@ class ToHtml(Tokenizer):
|
||||
"""Extend the auto-replace to also properly encode some unicode
|
||||
characters into their respective HTML entities.
|
||||
"""
|
||||
Tokenizer.doPreProcessing(self)
|
||||
super().doPreProcessing()
|
||||
self._theText = self._theText.translate(self._trMap)
|
||||
return
|
||||
|
||||
@@ -315,7 +315,7 @@ class ToHtml(Tokenizer):
|
||||
"</body>\n"
|
||||
"</html>\n"
|
||||
).format(
|
||||
projTitle=self.theProject.projName,
|
||||
projTitle=self.theProject.data.name,
|
||||
htmlStyle="\n".join(theStyle),
|
||||
bodyText=bodyText,
|
||||
)
|
||||
|
||||
@@ -36,7 +36,6 @@ from PyQt5.QtCore import QCoreApplication, QRegularExpression
|
||||
from novelwriter.enum import nwItemLayout, nwItemType
|
||||
from novelwriter.common import numberToRoman, checkInt
|
||||
from novelwriter.constants import nwConst, nwRegEx, nwUnicode
|
||||
from novelwriter.core.document import NWDoc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -304,16 +303,10 @@ class Tokenizer(ABC):
|
||||
if self._theItem is None:
|
||||
return False
|
||||
|
||||
self._theText = ""
|
||||
if theText is not None:
|
||||
# If the text is set, just use that
|
||||
self._theText = theText
|
||||
else:
|
||||
# Otherwise, load it from file
|
||||
theDoc = NWDoc(self.theProject, theHandle)
|
||||
theText = theDoc.readDocument()
|
||||
if theText:
|
||||
self._theText = theText
|
||||
if theText is None:
|
||||
theText = self.theProject.storage.getDocument(theHandle).readDocument() or ""
|
||||
|
||||
self._theText = theText
|
||||
|
||||
docSize = len(self._theText)
|
||||
if docSize > nwConst.MAX_DOCSIZE:
|
||||
@@ -333,9 +326,10 @@ class Tokenizer(ABC):
|
||||
"""Run trough the various replace doctionaries.
|
||||
"""
|
||||
# Process the user's auto-replace dictionary
|
||||
if len(self.theProject.autoReplace) > 0:
|
||||
autoReplace = self.theProject.data.autoReplace
|
||||
if len(autoReplace) > 0:
|
||||
repDict = {}
|
||||
for aKey, aVal in self.theProject.autoReplace.items():
|
||||
for aKey, aVal in autoReplace.items():
|
||||
repDict[f"<{aKey}>"] = aVal
|
||||
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)
|
||||
|
||||
@@ -37,7 +37,7 @@ class ToMarkdown(Tokenizer):
|
||||
M_GH = 1 # GitHub Markdown
|
||||
|
||||
def __init__(self, theProject):
|
||||
Tokenizer.__init__(self, theProject)
|
||||
super().__init__(theProject)
|
||||
|
||||
self._genMode = self.M_STD
|
||||
self._fullMD = []
|
||||
|
||||
@@ -89,7 +89,7 @@ M_DEL = ~X_DEL
|
||||
class ToOdt(Tokenizer):
|
||||
|
||||
def __init__(self, theProject, isFlat):
|
||||
Tokenizer.__init__(self, theProject)
|
||||
super().__init__(theProject)
|
||||
|
||||
self._isFlat = isFlat # Flat: .fodt, otherwise .odt
|
||||
|
||||
@@ -261,8 +261,8 @@ class ToOdt(Tokenizer):
|
||||
# ===============
|
||||
|
||||
if self._headerText == "":
|
||||
theTitle = self.theProject.bookTitle
|
||||
theAuth = self.theProject.getAuthors()
|
||||
theTitle = self.theProject.data.title
|
||||
theAuth = self.theProject.getFormattedAuthors()
|
||||
self._headerText = f"{theTitle} / {theAuth} /"
|
||||
|
||||
# Create Roots
|
||||
@@ -994,7 +994,7 @@ class ToOdt(Tokenizer):
|
||||
# Auto-Style Classes
|
||||
# =============================================================================================== #
|
||||
|
||||
class ODTParagraphStyle():
|
||||
class ODTParagraphStyle:
|
||||
"""Wrapper class for the paragraph style setting used by the
|
||||
exporter. Only the used settings are exposed here to keep the class
|
||||
minimal and fast.
|
||||
@@ -1208,7 +1208,7 @@ class ODTParagraphStyle():
|
||||
# END Class ODTParagraphStyle
|
||||
|
||||
|
||||
class ODTTextStyle():
|
||||
class ODTTextStyle:
|
||||
"""Wrapper class for the text style setting used by the exporter.
|
||||
Only the used settings are exposed here to keep the class minimal
|
||||
and fast.
|
||||
@@ -1297,7 +1297,7 @@ X_SPAN_TEXT = 2
|
||||
X_SPAN_SING = 3
|
||||
|
||||
|
||||
class XMLParagraph():
|
||||
class XMLParagraph:
|
||||
"""This is a helper class to manage the text content of a single
|
||||
XML element using mixed content tags.
|
||||
|
||||
|
||||
@@ -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/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
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.common import checkHandle
|
||||
from novelwriter.constants import nwFiles
|
||||
@@ -38,7 +37,7 @@ from novelwriter.core.item import NWItem
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NWTree():
|
||||
class NWTree:
|
||||
|
||||
MAX_DEPTH = 1000 # Cap of tree traversing for loops
|
||||
|
||||
@@ -89,20 +88,20 @@ class NWTree():
|
||||
logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
|
||||
return False
|
||||
|
||||
logger.verbose("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
|
||||
logger.debug("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
|
||||
|
||||
nwItem.setHandle(tHandle)
|
||||
nwItem.setParent(pHandle)
|
||||
|
||||
if nwItem.itemType == nwItemType.ROOT:
|
||||
logger.verbose("Item '%s' is a root item", str(tHandle))
|
||||
if nwItem.isRootType():
|
||||
logger.debug("Item '%s' is a root item", str(tHandle))
|
||||
self._treeRoots[tHandle] = nwItem
|
||||
if nwItem.itemClass == nwItemClass.ARCHIVE:
|
||||
logger.verbose("Item '%s' is the archive folder", str(tHandle))
|
||||
logger.debug("Item '%s' is the archive folder", str(tHandle))
|
||||
self._archRoot = tHandle
|
||||
elif nwItem.itemClass == nwItemClass.TRASH:
|
||||
if self._trashRoot is None:
|
||||
logger.verbose("Item '%s' is the trash folder", str(tHandle))
|
||||
logger.debug("Item '%s' is the trash folder", str(tHandle))
|
||||
self._trashRoot = tHandle
|
||||
else:
|
||||
logger.error("Only one trash folder allowed")
|
||||
@@ -114,30 +113,25 @@ class NWTree():
|
||||
|
||||
return True
|
||||
|
||||
def packXML(self, xParent):
|
||||
def pack(self):
|
||||
"""Pack the content of the tree into the provided XML object. In
|
||||
the order defined by the _treeOrder list.
|
||||
"""
|
||||
xContent = etree.SubElement(xParent, "content", attrib={
|
||||
"count": str(len(self._treeOrder))}
|
||||
)
|
||||
tree = []
|
||||
for tHandle in self._treeOrder:
|
||||
tItem = self.__getitem__(tHandle)
|
||||
tItem.packXML(xContent)
|
||||
return
|
||||
if tItem:
|
||||
tree.append(tItem.pack())
|
||||
return tree
|
||||
|
||||
def unpackXML(self, xContent):
|
||||
"""Iterate through all items of a content XML object and add
|
||||
them to the project tree.
|
||||
def unpack(self, data):
|
||||
"""Iterate through all items of a list and add them to the
|
||||
project tree.
|
||||
"""
|
||||
if xContent.tag != "content":
|
||||
logger.error("XML entry is not a NWTree")
|
||||
return False
|
||||
|
||||
self.clear()
|
||||
for xItem in xContent:
|
||||
for item in data:
|
||||
nwItem = NWItem(self.theProject)
|
||||
if nwItem.unpackXML(xItem):
|
||||
if nwItem.unpack(item):
|
||||
self.append(nwItem.itemHandle, nwItem.itemParent, nwItem)
|
||||
nwItem.saveInitialCount()
|
||||
|
||||
@@ -147,16 +141,22 @@ class NWTree():
|
||||
"""Write the convenience table of contents file in the root of
|
||||
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 = []
|
||||
tocLen = 0
|
||||
for tHandle in self._treeOrder:
|
||||
tItem = self.__getitem__(tHandle)
|
||||
if tItem is None:
|
||||
continue
|
||||
|
||||
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(
|
||||
os.path.join("content", tFile),
|
||||
str(Path("content") / tFile),
|
||||
tItem.itemClass.name,
|
||||
tItem.itemLayout.name,
|
||||
tItem.itemName,
|
||||
@@ -166,7 +166,7 @@ class NWTree():
|
||||
|
||||
try:
|
||||
# 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:
|
||||
outFile.write("\n")
|
||||
outFile.write("Table of Contents\n")
|
||||
@@ -274,11 +274,13 @@ class NWTree():
|
||||
return rootClasses
|
||||
|
||||
def iterRoots(self, itemClass):
|
||||
"""Iterate over all items of a given class.
|
||||
"""Iterate over all root items of a given class in order.
|
||||
"""
|
||||
for tHandle, nwItem in self._treeRoots.items():
|
||||
if nwItem.itemClass == itemClass:
|
||||
yield tHandle, nwItem
|
||||
for tHandle in self._treeOrder:
|
||||
nwItem = self.__getitem__(tHandle)
|
||||
if nwItem is not None and nwItem.isRootType():
|
||||
if itemClass is None or nwItem.itemClass == itemClass:
|
||||
yield tHandle, nwItem
|
||||
return
|
||||
|
||||
def isRoot(self, tHandle):
|
||||
@@ -329,25 +331,20 @@ class NWTree():
|
||||
def setOrder(self, newOrder):
|
||||
"""Reorders the tree based on a list of items.
|
||||
"""
|
||||
tmpOrder = []
|
||||
|
||||
# Add all known elements to a new temp list
|
||||
for tHandle in newOrder:
|
||||
if tHandle in self._projTree:
|
||||
tmpOrder.append(tHandle)
|
||||
else:
|
||||
logger.error("Handle '%s' in new tree order is not in project tree", 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)
|
||||
tmpOrder = [tHandle for tHandle in newOrder if tHandle in self._projTree]
|
||||
if not (len(tmpOrder) == len(newOrder) == len(self._treeOrder)):
|
||||
# Something is wrong, so let's debug it
|
||||
for tHandle in newOrder:
|
||||
if tHandle not in self._projTree:
|
||||
logger.error("Handle '%s' in new tree order is not in old order", tHandle)
|
||||
for tHandle in self._treeOrder:
|
||||
if tHandle not in tmpOrder:
|
||||
logger.warning("Handle '%s' in old tree order is not in new order", tHandle)
|
||||
|
||||
# Save the temp list
|
||||
self._treeOrder = tmpOrder
|
||||
self._setTreeChanged(True)
|
||||
logger.verbose("Project tree order updated")
|
||||
logger.debug("Project tree order updated")
|
||||
|
||||
return
|
||||
|
||||
@@ -357,7 +354,7 @@ class NWTree():
|
||||
tItem = self.__getitem__(tHandle)
|
||||
if tItem is None:
|
||||
return False
|
||||
if tItem.itemType != nwItemType.FILE:
|
||||
if not tItem.isFileType():
|
||||
logger.error("Item '%s' is not a file", tHandle)
|
||||
return False
|
||||
if not isinstance(itemLayout, nwItemLayout):
|
||||
@@ -457,7 +454,7 @@ class NWTree():
|
||||
"""Generate a unique item handle. In the event that the key
|
||||
already exists, generate a new one.
|
||||
"""
|
||||
logger.verbose("Generating new handle")
|
||||
logger.debug("Generating new handle")
|
||||
handle = f"{random.getrandbits(52):013x}"
|
||||
if handle in self._projTree:
|
||||
logger.warning("Duplicate handle encountered! Retrying ...")
|
||||
|
||||
@@ -202,7 +202,7 @@ class QConfigLayout(QGridLayout):
|
||||
class QHelpLabel(QLabel):
|
||||
|
||||
def __init__(self, theText, textCol, fontSize=0.9):
|
||||
QLabel.__init__(self, theText)
|
||||
super().__init__(theText)
|
||||
|
||||
if isinstance(textCol, QColor):
|
||||
qCol = textCol
|
||||
@@ -267,8 +267,8 @@ class QSwitch(QAbstractButton):
|
||||
return self._offset
|
||||
|
||||
@offset.setter
|
||||
def offset(self, theOffset):
|
||||
self._offset = theOffset
|
||||
def offset(self, offset):
|
||||
self._offset = offset
|
||||
self.update()
|
||||
return
|
||||
|
||||
@@ -276,11 +276,11 @@ class QSwitch(QAbstractButton):
|
||||
# Getters and Setters
|
||||
##
|
||||
|
||||
def setChecked(self, isChecked):
|
||||
def setChecked(self, checked):
|
||||
"""Overload setChecked to also alter the offset.
|
||||
"""
|
||||
super().setChecked(isChecked)
|
||||
if isChecked:
|
||||
super().setChecked(checked)
|
||||
if checked:
|
||||
self.offset = self._xW - self._xR
|
||||
else:
|
||||
self.offset = self._xR
|
||||
@@ -290,10 +290,10 @@ class QSwitch(QAbstractButton):
|
||||
# Events
|
||||
##
|
||||
|
||||
def resizeEvent(self, theEvent):
|
||||
def resizeEvent(self, event):
|
||||
"""Overload resize to ensure correct offset.
|
||||
"""
|
||||
super().resizeEvent(theEvent)
|
||||
super().resizeEvent(event)
|
||||
if self.isChecked():
|
||||
self.offset = self._xW - self._xR
|
||||
else:
|
||||
@@ -377,7 +377,7 @@ class QSwitch(QAbstractButton):
|
||||
class PagedDialog(QDialog):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QDialog.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self._tabBar = VerticalTabBar(self)
|
||||
self._tabBar.setExpanding(False)
|
||||
@@ -410,31 +410,37 @@ class PagedDialog(QDialog):
|
||||
return
|
||||
|
||||
def addTab(self, widget, label):
|
||||
"""Forwards the adding of tabs to the QTabWidget.
|
||||
"""Forward the adding of tabs to the QTabWidget.
|
||||
"""
|
||||
self._tabBox.addTab(widget, label)
|
||||
return
|
||||
|
||||
def addControls(self, buttonBar):
|
||||
"""Adds a button bar to the dialog.
|
||||
"""Add a button bar to the dialog.
|
||||
"""
|
||||
self._buttonBox.addWidget(buttonBar)
|
||||
return
|
||||
|
||||
def setCurrentWidget(self, widget):
|
||||
"""Forward the changing of tab to the QTabWidget.
|
||||
"""
|
||||
self._tabBox.setCurrentWidget(widget)
|
||||
return
|
||||
|
||||
# END Class PagedDialog
|
||||
|
||||
|
||||
class VerticalTabBar(QTabBar):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
QTabBar.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent)
|
||||
self._mW = novelwriter.CONFIG.pxInt(150)
|
||||
return
|
||||
|
||||
def tabSizeHint(self, index):
|
||||
"""Returns a transposed size hint for the rotated bar.
|
||||
"""Return a transposed size hint for the rotated bar.
|
||||
"""
|
||||
tSize = QTabBar.tabSizeHint(self, index)
|
||||
tSize = super().tabSizeHint(index)
|
||||
tSize.transpose()
|
||||
tSize.setWidth(min(tSize.width(), self._mW))
|
||||
return tSize
|
||||
@@ -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/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
@@ -44,7 +43,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiAbout(QDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QDialog.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiAbout ...")
|
||||
self.setObjectName("GuiAbout")
|
||||
@@ -234,7 +233,7 @@ class GuiAbout(QDialog):
|
||||
def _fillNotesPage(self):
|
||||
"""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)
|
||||
if docText:
|
||||
self.pageNotes.setHtml(docText)
|
||||
@@ -245,7 +244,7 @@ class GuiAbout(QDialog):
|
||||
def _fillLicensePage(self):
|
||||
"""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)
|
||||
if docText:
|
||||
self.pageLicense.setHtml(docText)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""
|
||||
novelWriter – GUI Doc Merge Tool
|
||||
================================
|
||||
GUI class for merging multiple documents to one document
|
||||
novelWriter – GUI Doc Merge Dialog
|
||||
==================================
|
||||
Custom dialog class for merging documents.
|
||||
|
||||
File History:
|
||||
Created: 2020-01-23 [0.4.3]
|
||||
Created: 2020-01-23 [0.4.3]
|
||||
Rewritten: 2022-10-06 [2.0b1]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2022, Veronica Berglyd Olsen
|
||||
@@ -26,169 +27,146 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QLabel, QListWidget, QAbstractItemView,
|
||||
QListWidgetItem, QDialogButtonBox
|
||||
QAbstractItemView, QDialog, QDialogButtonBox, QGridLayout, QLabel,
|
||||
QListWidget, QListWidgetItem, QVBoxLayout,
|
||||
)
|
||||
|
||||
from novelwriter.core import NWDoc
|
||||
from novelwriter.enum import nwAlert, nwItemType
|
||||
from novelwriter.gui.custom import QHelpLabel
|
||||
from novelwriter.custom import QHelpLabel, QSwitch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiDocMerge(QDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QDialog.__init__(self, mainGui)
|
||||
def __init__(self, mainGui, sHandle, itemList):
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiDocMerge ...")
|
||||
self.setObjectName("GuiDocMerge")
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.theProject = mainGui.theProject
|
||||
self.sourceItem = None
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self._data = {}
|
||||
|
||||
self.setWindowTitle(self.tr("Merge Documents"))
|
||||
|
||||
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge")))
|
||||
self.helpLabel = QHelpLabel(
|
||||
self.tr("Drag and drop items to change the order."), self.mainGui.mainTheme.helpText
|
||||
)
|
||||
self.helpLabel = QHelpLabel(self.tr(
|
||||
"Drag and drop items to change the order, or uncheck to exclude."
|
||||
), self.mainTheme.helpText)
|
||||
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
hSp = self.mainConf.pxInt(12)
|
||||
vSp = self.mainConf.pxInt(8)
|
||||
bSp = self.mainConf.pxInt(12)
|
||||
|
||||
self.listBox = QListWidget()
|
||||
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
|
||||
self.listBox.setIconSize(QSize(iPx, iPx))
|
||||
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
|
||||
self.listBox.setMinimumHeight(self.mainConf.pxInt(180))
|
||||
self.listBox.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
|
||||
|
||||
# Merge Options
|
||||
self.trashLabel = QLabel(self.tr("Move merged items to Trash"))
|
||||
self.trashSwitch = QSwitch(width=2*iPx, height=iPx)
|
||||
|
||||
self.optBox = QGridLayout()
|
||||
self.optBox.addWidget(self.trashLabel, 0, 0)
|
||||
self.optBox.addWidget(self.trashSwitch, 0, 1)
|
||||
self.optBox.setHorizontalSpacing(hSp)
|
||||
self.optBox.setColumnStretch(2, 1)
|
||||
|
||||
# Buttons
|
||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||
self.buttonBox.accepted.connect(self._doMerge)
|
||||
self.buttonBox.rejected.connect(self._doClose)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
|
||||
self.resetButton = self.buttonBox.addButton(QDialogButtonBox.Reset)
|
||||
self.resetButton.clicked.connect(self._resetList)
|
||||
|
||||
# Assemble
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.setSpacing(0)
|
||||
self.outerBox.addWidget(self.headLabel)
|
||||
self.outerBox.addWidget(self.helpLabel)
|
||||
self.outerBox.addSpacing(self.mainConf.pxInt(8))
|
||||
self.outerBox.addSpacing(vSp)
|
||||
self.outerBox.addWidget(self.listBox)
|
||||
self.outerBox.addSpacing(self.mainConf.pxInt(12))
|
||||
self.outerBox.addSpacing(vSp)
|
||||
self.outerBox.addLayout(self.optBox)
|
||||
self.outerBox.addSpacing(bSp)
|
||||
self.outerBox.addWidget(self.buttonBox)
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
self.rejected.connect(self._doClose)
|
||||
|
||||
self._populateList()
|
||||
# Load Content
|
||||
self._loadContent(sHandle, itemList)
|
||||
|
||||
logger.debug("GuiDocMerge initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Buttons
|
||||
##
|
||||
|
||||
def _doMerge(self):
|
||||
"""Perform the merge of the files in the selected folder, and
|
||||
create a new file in the same parent folder. The old files are
|
||||
not removed in the merge process, and must be deleted manually.
|
||||
def getData(self):
|
||||
"""Return the user's choices.
|
||||
"""
|
||||
logger.verbose("GuiDocMerge merge button clicked")
|
||||
|
||||
finalOrder = []
|
||||
finalItems = []
|
||||
for i in range(self.listBox.count()):
|
||||
finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
|
||||
item = self.listBox.item(i)
|
||||
if item is not None and item.checkState() == Qt.Checked:
|
||||
finalItems.append(item.data(Qt.UserRole))
|
||||
|
||||
if len(finalOrder) == 0:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"No source documents found. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
self._data["moveToTrash"] = self.trashSwitch.isChecked()
|
||||
self._data["finalItems"] = finalItems
|
||||
|
||||
theText = ""
|
||||
for tHandle in finalOrder:
|
||||
inDoc = NWDoc(self.theProject, tHandle)
|
||||
docText = inDoc.readDocument()
|
||||
docErr = inDoc.getError()
|
||||
if docText is None and docErr:
|
||||
self.mainGui.makeAlert([
|
||||
self.tr("Failed to open document file."), docErr
|
||||
], nwAlert.ERROR)
|
||||
if docText:
|
||||
theText += docText.rstrip("\n")+"\n\n"
|
||||
return self._data
|
||||
|
||||
if self.sourceItem is None:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"No source folder selected. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
srcItem = self.theProject.tree[self.sourceItem]
|
||||
if srcItem is None:
|
||||
self.mainGui.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent)
|
||||
newItem = self.theProject.tree[nHandle]
|
||||
newItem.setStatus(srcItem.itemStatus)
|
||||
newItem.setImport(srcItem.itemImport)
|
||||
|
||||
outDoc = NWDoc(self.theProject, nHandle)
|
||||
if not outDoc.writeDocument(theText):
|
||||
self.mainGui.makeAlert([
|
||||
self.tr("Could not save document."), outDoc.getError()
|
||||
], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
self.mainGui.projView.revealNewTreeItem(nHandle)
|
||||
self.mainGui.openDocument(nHandle, doScroll=True)
|
||||
|
||||
self._doClose()
|
||||
|
||||
return True
|
||||
|
||||
def _doClose(self):
|
||||
"""Close the dialog window without doing anything.
|
||||
def _resetList(self):
|
||||
"""Reset the content of the list box to its original state.
|
||||
"""
|
||||
self.close()
|
||||
logger.debug("Resetting list box content")
|
||||
sHandle = self._data.get("sHandle", None)
|
||||
itemList = self._data.get("origItems", [])
|
||||
self._loadContent(sHandle, itemList)
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _populateList(self):
|
||||
"""Get the item selected in the tree, check that it is a folder,
|
||||
and try to find all files associated with it. The valid files
|
||||
are then added to the list view in order. The list itself can be
|
||||
reordered by the user.
|
||||
def _loadContent(self, sHandle, itemList):
|
||||
"""Load content from a given list of items.
|
||||
"""
|
||||
tHandle = self.mainGui.projView.getSelectedHandle()
|
||||
self.sourceItem = tHandle
|
||||
if tHandle is None:
|
||||
return False
|
||||
self._data = {}
|
||||
self._data["sHandle"] = sHandle
|
||||
self._data["origItems"] = itemList
|
||||
|
||||
nwItem = self.theProject.tree[tHandle]
|
||||
if nwItem is None:
|
||||
return False
|
||||
|
||||
if nwItem.itemType is not nwItemType.FOLDER:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"Element selected in the project tree must be a folder."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
for sHandle in self.mainGui.projView.getTreeFromHandle(tHandle):
|
||||
newItem = QListWidgetItem()
|
||||
nwItem = self.theProject.tree[sHandle]
|
||||
if nwItem.itemType is not nwItemType.FILE:
|
||||
self.listBox.clear()
|
||||
for tHandle in itemList:
|
||||
nwItem = self.theProject.tree[tHandle]
|
||||
if nwItem is None or not nwItem.isFileType():
|
||||
continue
|
||||
|
||||
itemIcon = self.mainTheme.getItemIcon(
|
||||
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading
|
||||
)
|
||||
|
||||
newItem = QListWidgetItem()
|
||||
newItem.setIcon(itemIcon)
|
||||
newItem.setText(nwItem.itemName)
|
||||
newItem.setData(Qt.UserRole, sHandle)
|
||||
newItem.setData(Qt.UserRole, tHandle)
|
||||
newItem.setCheckState(Qt.Checked)
|
||||
|
||||
self.listBox.addItem(newItem)
|
||||
|
||||
return True
|
||||
return
|
||||
|
||||
# END Class GuiDocMerge
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""
|
||||
novelWriter – GUI Doc Split Tool
|
||||
================================
|
||||
GUI class for splitting a single document into multiple documents
|
||||
novelWriter – GUI Doc Split Dialog
|
||||
==================================
|
||||
Custom dialog class for splitting documents.
|
||||
|
||||
File History:
|
||||
Created: 2020-02-01 [0.4.3]
|
||||
Created: 2020-02-01 [0.4.3]
|
||||
Rewritten: 2022-10-12 [2.0b1]
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2022, Veronica Berglyd Olsen
|
||||
@@ -29,32 +30,34 @@ import novelwriter
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QComboBox, QListWidget, QAbstractItemView,
|
||||
QListWidgetItem, QDialogButtonBox, QLabel
|
||||
QListWidgetItem, QDialogButtonBox, QLabel, QGridLayout
|
||||
)
|
||||
|
||||
from novelwriter.core import NWDoc
|
||||
from novelwriter.enum import nwAlert, nwItemType
|
||||
from novelwriter.gui.custom import QHelpLabel
|
||||
from novelwriter.custom import QHelpLabel, QSwitch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiDocSplit(QDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QDialog.__init__(self, mainGui)
|
||||
LINE_ROLE = Qt.UserRole
|
||||
LEVEL_ROLE = Qt.UserRole + 1
|
||||
LABEL_ROLE = Qt.UserRole + 2
|
||||
|
||||
def __init__(self, mainGui, sHandle):
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiDocSplit ...")
|
||||
self.setObjectName("GuiDocSplit")
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.theProject = mainGui.theProject
|
||||
|
||||
self.sourceItem = None
|
||||
self.sourceText = []
|
||||
self._data = {}
|
||||
self._text = []
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.setWindowTitle(self.tr("Split Document"))
|
||||
|
||||
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Document Headers")))
|
||||
@@ -63,6 +66,18 @@ class GuiDocSplit(QDialog):
|
||||
self.mainGui.mainTheme.helpText
|
||||
)
|
||||
|
||||
# Values
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
hSp = self.mainConf.pxInt(12)
|
||||
vSp = self.mainConf.pxInt(8)
|
||||
bSp = self.mainConf.pxInt(12)
|
||||
|
||||
pOptions = self.theProject.options
|
||||
spLevel = pOptions.getInt("GuiDocSplit", "spLevel", 3)
|
||||
intoFolder = pOptions.getBool("GuiDocSplit", "intoFolder", True)
|
||||
docHierarchy = pOptions.getBool("GuiDocSplit", "docHierarchy", True)
|
||||
|
||||
# Header Selection
|
||||
self.listBox = QListWidget()
|
||||
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
|
||||
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
|
||||
@@ -73,206 +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 3 (Scene)"), 3)
|
||||
self.splitLevel.addItem(self.tr("Split up to Header Level 4 (Section)"), 4)
|
||||
spIndex = self.splitLevel.findData(
|
||||
self.theProject.options.getInt("GuiDocSplit", "spLevel", 3)
|
||||
)
|
||||
spIndex = self.splitLevel.findData(spLevel)
|
||||
if spIndex != -1:
|
||||
self.splitLevel.setCurrentIndex(spIndex)
|
||||
self.splitLevel.currentIndexChanged.connect(self._populateList)
|
||||
self.splitLevel.currentIndexChanged.connect(self._reloadList)
|
||||
|
||||
# Split Options
|
||||
self.folderLabel = QLabel(self.tr("Split into a new folder"))
|
||||
self.folderSwitch = QSwitch(width=2*iPx, height=iPx)
|
||||
self.folderSwitch.setChecked(intoFolder)
|
||||
|
||||
self.hierarchyLabel = QLabel(self.tr("Create document hierarchy"))
|
||||
self.hierarchySwitch = QSwitch(width=2*iPx, height=iPx)
|
||||
self.hierarchySwitch.setChecked(docHierarchy)
|
||||
|
||||
self.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.accepted.connect(self._doSplit)
|
||||
self.buttonBox.rejected.connect(self._doClose)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
|
||||
# Assemble
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.setSpacing(0)
|
||||
self.outerBox.addWidget(self.headLabel)
|
||||
self.outerBox.addWidget(self.helpLabel)
|
||||
self.outerBox.addSpacing(self.mainConf.pxInt(8))
|
||||
self.outerBox.addSpacing(vSp)
|
||||
self.outerBox.addWidget(self.listBox)
|
||||
self.outerBox.addWidget(self.splitLevel)
|
||||
self.outerBox.addSpacing(self.mainConf.pxInt(12))
|
||||
self.outerBox.addSpacing(vSp)
|
||||
self.outerBox.addLayout(self.optBox)
|
||||
self.outerBox.addSpacing(bSp)
|
||||
self.outerBox.addWidget(self.buttonBox)
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
self.rejected.connect(self._doClose)
|
||||
|
||||
self._populateList()
|
||||
# Load Content
|
||||
self._loadContent(sHandle)
|
||||
|
||||
logger.debug("GuiDocSplit initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Buttons
|
||||
##
|
||||
|
||||
def _doSplit(self):
|
||||
"""Perform the split of the file, create a new folder in the
|
||||
same parent folder, and multiple files depending on split level
|
||||
settings. The old file is not removed in the split process, and
|
||||
must be deleted manually.
|
||||
def getData(self):
|
||||
"""Return the user's choices. Also save the users options for
|
||||
the next time the dialog is used.
|
||||
"""
|
||||
logger.verbose("GuiDocSplit split button clicked")
|
||||
|
||||
if self.sourceItem is None:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"No source document selected. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
srcItem = self.theProject.tree[self.sourceItem]
|
||||
if srcItem is None:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"Could not parse source document."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
inDoc = NWDoc(self.theProject, self.sourceItem)
|
||||
theText = inDoc.readDocument()
|
||||
|
||||
docErr = inDoc.getError()
|
||||
if theText is None and docErr:
|
||||
self.mainGui.makeAlert([
|
||||
self.tr("Failed to open document file."), docErr
|
||||
], nwAlert.ERROR)
|
||||
|
||||
if theText is None:
|
||||
theText = ""
|
||||
|
||||
nLines = len(self.sourceText)
|
||||
logger.debug("Splitting document %s with %d lines", self.sourceItem, nLines)
|
||||
|
||||
finalOrder = []
|
||||
headerList = []
|
||||
for i in range(self.listBox.count()):
|
||||
listItem = self.listBox.item(i)
|
||||
wTitle = listItem.text()
|
||||
lineNo = listItem.data(Qt.UserRole)
|
||||
finalOrder.append([wTitle, lineNo, nLines])
|
||||
if i > 0:
|
||||
finalOrder[i-1][2] = lineNo
|
||||
item = self.listBox.item(i)
|
||||
if item is not None:
|
||||
headerList.append((
|
||||
item.data(self.LINE_ROLE),
|
||||
item.data(self.LEVEL_ROLE),
|
||||
item.data(self.LABEL_ROLE),
|
||||
))
|
||||
|
||||
nFiles = len(finalOrder)
|
||||
if nFiles == 0:
|
||||
self.mainGui.makeAlert(self.tr(
|
||||
"No headers found. Nothing to do."
|
||||
), nwAlert.ERROR)
|
||||
return False
|
||||
spLevel = self.splitLevel.currentData()
|
||||
intoFolder = self.folderSwitch.isChecked()
|
||||
docHierarchy = self.hierarchySwitch.isChecked()
|
||||
moveToTrash = self.trashSwitch.isChecked()
|
||||
|
||||
msgYes = self.mainGui.askQuestion(
|
||||
self.tr("Split Document"),
|
||||
"{0}<br><br>{1}".format(
|
||||
self.tr(
|
||||
"The document will be split into {0} file(s) in a new folder. "
|
||||
"The original document will remain intact."
|
||||
).format(nFiles),
|
||||
self.tr(
|
||||
"Continue with the splitting process?"
|
||||
)
|
||||
)
|
||||
)
|
||||
if not msgYes:
|
||||
return False
|
||||
self._data["spLevel"] = spLevel
|
||||
self._data["headerList"] = headerList
|
||||
self._data["intoFolder"] = intoFolder
|
||||
self._data["docHierarchy"] = docHierarchy
|
||||
self._data["moveToTrash"] = moveToTrash
|
||||
|
||||
# Create the folder
|
||||
fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent)
|
||||
self.mainGui.projView.revealNewTreeItem(fHandle)
|
||||
logger.verbose("Creating folder '%s'", fHandle)
|
||||
pOptions = self.theProject.options
|
||||
pOptions.setValue("GuiDocSplit", "spLevel", spLevel)
|
||||
pOptions.setValue("GuiDocSplit", "intoFolder", intoFolder)
|
||||
pOptions.setValue("GuiDocSplit", "docHierarchy", docHierarchy)
|
||||
|
||||
# Loop through, and create the files
|
||||
for wTitle, iStart, iEnd in finalOrder:
|
||||
return self._data, self._text
|
||||
|
||||
wTitle = wTitle.lstrip("#").strip()
|
||||
nHandle = self.theProject.newFile(wTitle, fHandle)
|
||||
newItem = self.theProject.tree[nHandle]
|
||||
newItem.setStatus(srcItem.itemStatus)
|
||||
newItem.setImport(srcItem.itemImport)
|
||||
logger.verbose(
|
||||
"Creating new document '%s' with text from line %d to %d",
|
||||
nHandle, iStart+1, iEnd
|
||||
)
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
|
||||
theText = "\n".join(self.sourceText[iStart:iEnd])
|
||||
theText = theText.rstrip("\n") + "\n\n"
|
||||
|
||||
outDoc = NWDoc(self.theProject, nHandle)
|
||||
if not outDoc.writeDocument(theText):
|
||||
self.mainGui.makeAlert([
|
||||
self.tr("Could not save document."), outDoc.getError()
|
||||
], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
self.mainGui.projView.revealNewTreeItem(nHandle)
|
||||
|
||||
self._doClose()
|
||||
|
||||
return True
|
||||
|
||||
def _doClose(self):
|
||||
"""Close the dialog window without doing anything.
|
||||
def _reloadList(self):
|
||||
"""Reload the content of the list box.
|
||||
"""
|
||||
self.theProject.options.saveSettings()
|
||||
self.close()
|
||||
sHandle = self._data.get("sHandle", None)
|
||||
self._loadContent(sHandle)
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _populateList(self):
|
||||
"""Get the item selected in the tree, check that it is a folder,
|
||||
and try to find all files associated with it. The valid files
|
||||
are then added to the list view in order. The list itself can be
|
||||
reordered by the user.
|
||||
def _loadContent(self, sHandle):
|
||||
"""Load content from a given source item.
|
||||
"""
|
||||
self._data = {}
|
||||
self._data["sHandle"] = sHandle
|
||||
|
||||
self.listBox.clear()
|
||||
if self.sourceItem is None:
|
||||
self.sourceItem = self.mainGui.projView.getSelectedHandle()
|
||||
|
||||
if self.sourceItem is None:
|
||||
return False
|
||||
|
||||
nwItem = self.theProject.tree[self.sourceItem]
|
||||
if nwItem is None:
|
||||
return False
|
||||
|
||||
if 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
|
||||
nwItem = self.theProject.tree[sHandle]
|
||||
if nwItem is None or not nwItem.isFileType():
|
||||
return
|
||||
|
||||
spLevel = self.splitLevel.currentData()
|
||||
self.theProject.options.setValue("GuiDocSplit", "spLevel", spLevel)
|
||||
logger.debug(
|
||||
"Scanning document '%s' for headings level <= %d",
|
||||
self.sourceItem, spLevel
|
||||
)
|
||||
if not self._text:
|
||||
inDoc = self.theProject.storage.getDocument(sHandle)
|
||||
self._text = (inDoc.readDocument() or "").splitlines()
|
||||
|
||||
self.sourceText = theText.splitlines()
|
||||
for lineNo, aLine in enumerate(self.sourceText):
|
||||
for lineNo, aLine in enumerate(self._text):
|
||||
|
||||
onLine = -1
|
||||
hLevel = 0
|
||||
hLabel = aLine.strip()
|
||||
if aLine.startswith("# ") and spLevel >= 1:
|
||||
onLine = lineNo
|
||||
hLevel = 1
|
||||
hLabel = aLine[2:].strip()
|
||||
elif aLine.startswith("## ") and spLevel >= 2:
|
||||
onLine = lineNo
|
||||
hLevel = 2
|
||||
hLabel = aLine[3:].strip()
|
||||
elif aLine.startswith("### ") and spLevel >= 3:
|
||||
onLine = lineNo
|
||||
hLevel = 3
|
||||
hLabel = aLine[4:].strip()
|
||||
elif aLine.startswith("#### ") and spLevel >= 4:
|
||||
onLine = lineNo
|
||||
hLevel = 4
|
||||
hLabel = aLine[5:].strip()
|
||||
elif aLine.startswith("#! ") and spLevel >= 1:
|
||||
onLine = lineNo
|
||||
hLevel = 1
|
||||
hLabel = aLine[3:].strip()
|
||||
elif aLine.startswith("##! ") and spLevel >= 2:
|
||||
onLine = lineNo
|
||||
hLevel = 2
|
||||
hLabel = aLine[4:].strip()
|
||||
|
||||
if onLine >= 0:
|
||||
if onLine >= 0 and hLevel > 0:
|
||||
newItem = QListWidgetItem()
|
||||
newItem.setText(aLine.strip())
|
||||
newItem.setData(Qt.UserRole, onLine)
|
||||
newItem.setData(self.LINE_ROLE, onLine)
|
||||
newItem.setData(self.LEVEL_ROLE, hLevel)
|
||||
newItem.setData(self.LABEL_ROLE, hLabel)
|
||||
self.listBox.addItem(newItem)
|
||||
|
||||
return True
|
||||
return
|
||||
|
||||
# END Class GuiDocSplit
|
||||
|
||||
@@ -36,7 +36,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiEditLabel(QDialog):
|
||||
|
||||
def __init__(self, parent, text=""):
|
||||
QDialog.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.setObjectName("GuiEditLabel")
|
||||
self.setWindowTitle(self.tr("Item Label"))
|
||||
|
||||
@@ -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/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
@@ -34,8 +33,7 @@ from PyQt5.QtWidgets import (
|
||||
QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox
|
||||
)
|
||||
|
||||
from novelwriter.enum import nwAlert
|
||||
from novelwriter.gui.custom import QSwitch, QConfigLayout, PagedDialog
|
||||
from novelwriter.custom import QSwitch, QConfigLayout, PagedDialog
|
||||
from novelwriter.dialogs.quotes import GuiQuoteSelect
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -44,7 +42,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiPreferences(PagedDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
PagedDialog.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiPreferences ...")
|
||||
self.setObjectName("GuiPreferences")
|
||||
@@ -55,13 +53,13 @@ class GuiPreferences(PagedDialog):
|
||||
|
||||
self.setWindowTitle(self.tr("Preferences"))
|
||||
|
||||
self.tabGeneral = GuiPreferencesGeneral(self.mainGui)
|
||||
self.tabProjects = GuiPreferencesProjects(self.mainGui)
|
||||
self.tabDocs = GuiPreferencesDocuments(self.mainGui)
|
||||
self.tabEditor = GuiPreferencesEditor(self.mainGui)
|
||||
self.tabSyntax = GuiPreferencesSyntax(self.mainGui)
|
||||
self.tabAuto = GuiPreferencesAutomation(self.mainGui)
|
||||
self.tabQuote = GuiPreferencesQuotes(self.mainGui)
|
||||
self.tabGeneral = GuiPreferencesGeneral(self)
|
||||
self.tabProjects = GuiPreferencesProjects(self)
|
||||
self.tabDocs = GuiPreferencesDocuments(self)
|
||||
self.tabEditor = GuiPreferencesEditor(self)
|
||||
self.tabSyntax = GuiPreferencesSyntax(self)
|
||||
self.tabAuto = GuiPreferencesAutomation(self)
|
||||
self.tabQuote = GuiPreferencesQuotes(self)
|
||||
|
||||
self.addTab(self.tabGeneral, self.tr("General"))
|
||||
self.addTab(self.tabProjects, self.tr("Projects"))
|
||||
@@ -76,12 +74,38 @@ class GuiPreferences(PagedDialog):
|
||||
self.buttonBox.rejected.connect(self._doClose)
|
||||
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")
|
||||
|
||||
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
|
||||
##
|
||||
@@ -92,8 +116,7 @@ class GuiPreferences(PagedDialog):
|
||||
"""
|
||||
logger.debug("Saving new preferences")
|
||||
|
||||
needsRestart, refreshTree = self.tabGeneral.saveValues()
|
||||
|
||||
self.tabGeneral.saveValues()
|
||||
self.tabProjects.saveValues()
|
||||
self.tabDocs.saveValues()
|
||||
self.tabEditor.saveValues()
|
||||
@@ -101,15 +124,8 @@ class GuiPreferences(PagedDialog):
|
||||
self.tabAuto.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.mainConf.saveConfig()
|
||||
self.accept()
|
||||
|
||||
return
|
||||
@@ -128,9 +144,7 @@ class GuiPreferences(PagedDialog):
|
||||
def _saveWindowSize(self):
|
||||
"""Save the dialog window size.
|
||||
"""
|
||||
winWidth = self.mainConf.rpxInt(self.width())
|
||||
winHeight = self.mainConf.rpxInt(self.height())
|
||||
self.mainConf.setPreferencesSize(winWidth, winHeight)
|
||||
self.mainConf.setPreferencesWinSize(self.width(), self.height())
|
||||
return
|
||||
|
||||
# END Class GuiPreferences
|
||||
@@ -138,12 +152,13 @@ class GuiPreferences(PagedDialog):
|
||||
|
||||
class GuiPreferencesGeneral(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, prefsGui):
|
||||
super().__init__(parent=prefsGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.prefsGui = prefsGui
|
||||
self.mainGui = prefsGui.mainGui
|
||||
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -156,19 +171,19 @@ class GuiPreferencesGeneral(QWidget):
|
||||
minWidth = self.mainConf.pxInt(200)
|
||||
|
||||
# Select Locale
|
||||
self.guiLang = QComboBox()
|
||||
self.guiLang.setMinimumWidth(minWidth)
|
||||
self.guiLocale = QComboBox()
|
||||
self.guiLocale.setMinimumWidth(minWidth)
|
||||
theLangs = self.mainConf.listLanguages(self.mainConf.LANG_NW)
|
||||
for lang, langName in theLangs:
|
||||
self.guiLang.addItem(langName, lang)
|
||||
langIdx = self.guiLang.findData(self.mainConf.guiLang)
|
||||
self.guiLocale.addItem(langName, lang)
|
||||
langIdx = self.guiLocale.findData(self.mainConf.guiLocale)
|
||||
if langIdx != -1:
|
||||
self.guiLang.setCurrentIndex(langIdx)
|
||||
self.guiLocale.setCurrentIndex(langIdx)
|
||||
|
||||
self.mainForm.addRow(
|
||||
self.tr("Main GUI language"),
|
||||
self.guiLang,
|
||||
self.tr("Requires restart.")
|
||||
self.guiLocale,
|
||||
self.tr("Requires restart to take effect.")
|
||||
)
|
||||
|
||||
# Select Theme
|
||||
@@ -184,23 +199,7 @@ class GuiPreferencesGeneral(QWidget):
|
||||
self.mainForm.addRow(
|
||||
self.tr("Main GUI theme"),
|
||||
self.guiTheme,
|
||||
self.tr("Requires restart.")
|
||||
)
|
||||
|
||||
# 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.")
|
||||
self.tr("General colour theme and icons.")
|
||||
)
|
||||
|
||||
# Editor Theme
|
||||
@@ -230,7 +229,7 @@ class GuiPreferencesGeneral(QWidget):
|
||||
self.mainForm.addRow(
|
||||
self.tr("Font family"),
|
||||
self.guiFont,
|
||||
self.tr("Requires restart."),
|
||||
self.tr("Requires restart to take effect."),
|
||||
theButton=self.fontButton
|
||||
)
|
||||
|
||||
@@ -243,7 +242,7 @@ class GuiPreferencesGeneral(QWidget):
|
||||
self.mainForm.addRow(
|
||||
self.tr("Font size"),
|
||||
self.guiFontSize,
|
||||
self.tr("Requires restart."),
|
||||
self.tr("Requires restart to take effect."),
|
||||
theUnit=self.tr("pt")
|
||||
)
|
||||
|
||||
@@ -288,29 +287,23 @@ class GuiPreferencesGeneral(QWidget):
|
||||
def saveValues(self):
|
||||
"""Save the values set for this tab.
|
||||
"""
|
||||
guiLang = self.guiLang.currentData()
|
||||
guiLocale = self.guiLocale.currentData()
|
||||
guiTheme = self.guiTheme.currentData()
|
||||
guiIcons = self.guiIcons.currentData()
|
||||
guiSyntax = self.guiSyntax.currentData()
|
||||
guiFont = self.guiFont.text()
|
||||
guiFontSize = self.guiFontSize.value()
|
||||
emphLabels = self.emphLabels.isChecked()
|
||||
|
||||
# Check if restart is needed
|
||||
needsRestart = False
|
||||
needsRestart |= self.mainConf.guiLang != guiLang
|
||||
needsRestart |= self.mainConf.guiTheme != guiTheme
|
||||
needsRestart |= self.mainConf.guiIcons != guiIcons
|
||||
needsRestart |= self.mainConf.guiFont != guiFont
|
||||
needsRestart |= self.mainConf.guiFontSize != guiFontSize
|
||||
# Update Flags
|
||||
self.prefsGui._updateTheme |= self.mainConf.guiTheme != guiTheme
|
||||
self.prefsGui._updateSyntax |= self.mainConf.guiSyntax != guiSyntax
|
||||
self.prefsGui._needsRestart |= self.mainConf.guiLocale != guiLocale
|
||||
self.prefsGui._needsRestart |= self.mainConf.guiFont != guiFont
|
||||
self.prefsGui._needsRestart |= self.mainConf.guiFontSize != guiFontSize
|
||||
self.prefsGui._refreshTree |= self.mainConf.emphLabels != emphLabels
|
||||
|
||||
# Check if refreshing project tree is needed
|
||||
refreshTree = False
|
||||
refreshTree |= self.mainConf.emphLabels != emphLabels
|
||||
|
||||
self.mainConf.guiLang = guiLang
|
||||
self.mainConf.guiLocale = guiLocale
|
||||
self.mainConf.guiTheme = guiTheme
|
||||
self.mainConf.guiIcons = guiIcons
|
||||
self.mainConf.guiSyntax = guiSyntax
|
||||
self.mainConf.guiFont = guiFont
|
||||
self.mainConf.guiFontSize = guiFontSize
|
||||
@@ -319,9 +312,7 @@ class GuiPreferencesGeneral(QWidget):
|
||||
self.mainConf.hideVScroll = self.hideVScroll.isChecked()
|
||||
self.mainConf.hideHScroll = self.hideHScroll.isChecked()
|
||||
|
||||
self.mainConf.confChanged = True
|
||||
|
||||
return needsRestart, refreshTree
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
@@ -344,12 +335,12 @@ class GuiPreferencesGeneral(QWidget):
|
||||
|
||||
class GuiPreferencesProjects(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, prefsGui):
|
||||
super().__init__(parent=prefsGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.mainGui = prefsGui.mainGui
|
||||
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -391,7 +382,7 @@ class GuiPreferencesProjects(QWidget):
|
||||
self.mainForm.addGroupLabel(self.tr("Project Backup"))
|
||||
|
||||
# Backup Path
|
||||
self.backupPath = self.mainConf.backupPath
|
||||
self.backupPath = self.mainConf.backupPath()
|
||||
self.backupGetPath = QPushButton(self.tr("Browse"))
|
||||
self.backupGetPath.clicked.connect(self._backupFolder)
|
||||
self.backupPathRow = self.mainForm.addRow(
|
||||
@@ -458,7 +449,7 @@ class GuiPreferencesProjects(QWidget):
|
||||
self.mainConf.autoSaveProj = self.autoSaveProj.value()
|
||||
|
||||
# Project Backup
|
||||
self.mainConf.backupPath = self.backupPath
|
||||
self.mainConf.setBackupPath(self.backupPath)
|
||||
self.mainConf.backupOnClose = self.backupOnClose.isChecked()
|
||||
self.mainConf.askBeforeBackup = self.askBeforeBackup.isChecked()
|
||||
|
||||
@@ -466,8 +457,6 @@ class GuiPreferencesProjects(QWidget):
|
||||
self.mainConf.stopWhenIdle = self.stopWhenIdle.isChecked()
|
||||
self.mainConf.userIdleTime = round(self.userIdleTime.value() * 60)
|
||||
|
||||
self.mainConf.confChanged = True
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -477,12 +466,9 @@ class GuiPreferencesProjects(QWidget):
|
||||
def _backupFolder(self):
|
||||
"""Open a dialog to select the backup folder.
|
||||
"""
|
||||
currDir = self.backupPath
|
||||
if not os.path.isdir(currDir):
|
||||
currDir = ""
|
||||
|
||||
currDir = self.backupPath or ""
|
||||
newDir = QFileDialog.getExistingDirectory(
|
||||
self, self.tr("Backup Directory"), currDir, options=QFileDialog.ShowDirsOnly
|
||||
self, self.tr("Backup Directory"), str(currDir), options=QFileDialog.ShowDirsOnly
|
||||
)
|
||||
if newDir:
|
||||
self.backupPath = newDir
|
||||
@@ -505,12 +491,12 @@ class GuiPreferencesProjects(QWidget):
|
||||
|
||||
class GuiPreferencesDocuments(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, prefsGui):
|
||||
super().__init__(parent=prefsGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.mainGui = prefsGui.mainGui
|
||||
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -640,8 +626,6 @@ class GuiPreferencesDocuments(QWidget):
|
||||
self.mainConf.textMargin = self.textMargin.value()
|
||||
self.mainConf.tabWidth = self.tabWidth.value()
|
||||
|
||||
self.mainConf.confChanged = True
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -666,12 +650,12 @@ class GuiPreferencesDocuments(QWidget):
|
||||
|
||||
class GuiPreferencesEditor(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, prefsGui):
|
||||
super().__init__(parent=prefsGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.mainGui = prefsGui.mainGui
|
||||
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -831,8 +815,6 @@ class GuiPreferencesEditor(QWidget):
|
||||
self.mainConf.autoScroll = self.autoScroll.isChecked()
|
||||
self.mainConf.autoScrollPos = self.autoScrollPos.value()
|
||||
|
||||
self.mainConf.confChanged = True
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiPreferencesEditor
|
||||
@@ -840,12 +822,12 @@ class GuiPreferencesEditor(QWidget):
|
||||
|
||||
class GuiPreferencesSyntax(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, prefsGui):
|
||||
super().__init__(parent=prefsGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.mainGui = prefsGui.mainGui
|
||||
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -922,8 +904,6 @@ class GuiPreferencesSyntax(QWidget):
|
||||
# Text Errors
|
||||
self.mainConf.showMultiSpaces = self.showMultiSpaces.isChecked()
|
||||
|
||||
self.mainConf.confChanged = True
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -943,12 +923,12 @@ class GuiPreferencesSyntax(QWidget):
|
||||
|
||||
class GuiPreferencesAutomation(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, prefsGui):
|
||||
super().__init__(parent=prefsGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.mainGui = prefsGui.mainGui
|
||||
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -1076,8 +1056,6 @@ class GuiPreferencesAutomation(QWidget):
|
||||
self.mainConf.fmtPadAfter = self.fmtPadAfter.text().strip()
|
||||
self.mainConf.fmtPadThin = self.fmtPadThin.isChecked()
|
||||
|
||||
self.mainConf.confChanged = True
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -1100,12 +1078,12 @@ class GuiPreferencesAutomation(QWidget):
|
||||
|
||||
class GuiPreferencesQuotes(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, prefsGui):
|
||||
super().__init__(parent=prefsGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.mainGui = prefsGui.mainGui
|
||||
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -1126,7 +1104,7 @@ class GuiPreferencesQuotes(QWidget):
|
||||
self.quoteSym["SO"].setReadOnly(True)
|
||||
self.quoteSym["SO"].setFixedWidth(qWidth)
|
||||
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.setMaximumWidth(bWidth)
|
||||
self.btnSingleStyleO.clicked.connect(lambda: self._getQuote("SO"))
|
||||
@@ -1142,7 +1120,7 @@ class GuiPreferencesQuotes(QWidget):
|
||||
self.quoteSym["SC"].setReadOnly(True)
|
||||
self.quoteSym["SC"].setFixedWidth(qWidth)
|
||||
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.setMaximumWidth(bWidth)
|
||||
self.btnSingleStyleC.clicked.connect(lambda: self._getQuote("SC"))
|
||||
@@ -1159,7 +1137,7 @@ class GuiPreferencesQuotes(QWidget):
|
||||
self.quoteSym["DO"].setReadOnly(True)
|
||||
self.quoteSym["DO"].setFixedWidth(qWidth)
|
||||
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.setMaximumWidth(bWidth)
|
||||
self.btnDoubleStyleO.clicked.connect(lambda: self._getQuote("DO"))
|
||||
@@ -1175,7 +1153,7 @@ class GuiPreferencesQuotes(QWidget):
|
||||
self.quoteSym["DC"].setReadOnly(True)
|
||||
self.quoteSym["DC"].setFixedWidth(qWidth)
|
||||
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.setMaximumWidth(bWidth)
|
||||
self.btnDoubleStyleC.clicked.connect(lambda: self._getQuote("DC"))
|
||||
@@ -1192,13 +1170,10 @@ class GuiPreferencesQuotes(QWidget):
|
||||
"""Save the values set for this tab.
|
||||
"""
|
||||
# Quotation Style
|
||||
self.mainConf.fmtSingleQuotes[0] = self.quoteSym["SO"].text()
|
||||
self.mainConf.fmtSingleQuotes[1] = self.quoteSym["SC"].text()
|
||||
self.mainConf.fmtDoubleQuotes[0] = self.quoteSym["DO"].text()
|
||||
self.mainConf.fmtDoubleQuotes[1] = self.quoteSym["DC"].text()
|
||||
|
||||
self.mainConf.confChanged = True
|
||||
|
||||
self.mainConf.fmtSQuoteOpen = self.quoteSym["SO"].text()
|
||||
self.mainConf.fmtSQuoteClose = self.quoteSym["SC"].text()
|
||||
self.mainConf.fmtDQuoteOpen = self.quoteSym["DO"].text()
|
||||
self.mainConf.fmtDQuoteClose = self.quoteSym["DC"].text()
|
||||
return
|
||||
|
||||
##
|
||||
|
||||
@@ -27,16 +27,18 @@ import math
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtCore import Qt, QSize, pyqtSlot
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QDialogButtonBox, QVBoxLayout, QTreeWidget, QTreeWidgetItem,
|
||||
QLabel, QSpinBox, QGridLayout, QHBoxLayout, QLineEdit, QAbstractItemView
|
||||
QAbstractItemView, QComboBox, QDialogButtonBox, QGridLayout, QHBoxLayout,
|
||||
QLabel, QLineEdit, QSpinBox, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
||||
QWidget
|
||||
)
|
||||
|
||||
from novelwriter.enum import nwItemClass
|
||||
from novelwriter.common import numberToRoman
|
||||
from novelwriter.constants import nwUnicode
|
||||
from novelwriter.gui.custom import PagedDialog, QSwitch
|
||||
from novelwriter.custom import PagedDialog, QSwitch
|
||||
from novelwriter.constants import nwLabels, nwUnicode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,7 +46,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiProjectDetails(PagedDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
PagedDialog.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiProjectDetails ...")
|
||||
self.setObjectName("GuiProjectDetails")
|
||||
@@ -140,7 +142,7 @@ class GuiProjectDetails(PagedDialog):
|
||||
class GuiProjectDetailsMain(QWidget):
|
||||
|
||||
def __init__(self, mainGui, theProject):
|
||||
QWidget.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.theProject = theProject
|
||||
@@ -155,7 +157,7 @@ class GuiProjectDetailsMain(QWidget):
|
||||
# Header
|
||||
# ======
|
||||
|
||||
self.bookTitle = QLabel(self.theProject.bookTitle)
|
||||
self.bookTitle = QLabel(self.theProject.data.title)
|
||||
bookFont = self.bookTitle.font()
|
||||
bookFont.setPointSizeF(2.2*fPt)
|
||||
bookFont.setWeight(QFont.Bold)
|
||||
@@ -164,7 +166,7 @@ class GuiProjectDetailsMain(QWidget):
|
||||
self.bookTitle.setWordWrap(True)
|
||||
|
||||
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.setPointSizeF(0.8*fPt)
|
||||
@@ -173,7 +175,9 @@ class GuiProjectDetailsMain(QWidget):
|
||||
self.projName.setAlignment(Qt.AlignHCenter)
|
||||
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.setPointSizeF(1.2*fPt)
|
||||
self.bookAuthors.setFont(authFont)
|
||||
@@ -253,10 +257,10 @@ class GuiProjectDetailsMain(QWidget):
|
||||
self.wordCountVal.setText(f"{nwCount:n}")
|
||||
self.chapCountVal.setText(f"{hCounts[2]: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.projPathVal.setText(self.theProject.projPath)
|
||||
self.projPathVal.setText(str(self.theProject.storage.storagePath))
|
||||
|
||||
return
|
||||
|
||||
@@ -272,7 +276,7 @@ class GuiProjectDetailsContents(QWidget):
|
||||
C_PROG = 4
|
||||
|
||||
def __init__(self, mainGui, theProject):
|
||||
QWidget.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.theProject = theProject
|
||||
@@ -281,12 +285,26 @@ class GuiProjectDetailsContents(QWidget):
|
||||
|
||||
# Internal
|
||||
self._theToC = []
|
||||
self._currentRoot = None
|
||||
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
hPx = self.mainConf.pxInt(12)
|
||||
vPx = self.mainConf.pxInt(4)
|
||||
pOptions = self.theProject.options
|
||||
|
||||
# Header
|
||||
# ======
|
||||
|
||||
self.tocLabel = QLabel("<b>%s</b>" % self.tr("Table of Contents"))
|
||||
|
||||
self.novelValue = QComboBox(self)
|
||||
self.novelValue.setMinimumWidth(self.mainConf.pxInt(200))
|
||||
self.novelValue.currentIndexChanged.connect(self._novelValueChanged)
|
||||
|
||||
self.headBox = QHBoxLayout()
|
||||
self.headBox.addWidget(self.tocLabel)
|
||||
self.headBox.addWidget(self.novelValue)
|
||||
|
||||
# Contents Tree
|
||||
# =============
|
||||
|
||||
@@ -389,7 +407,7 @@ class GuiProjectDetailsContents(QWidget):
|
||||
# ========
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.outerBox.addWidget(QLabel("<b>%s</b>" % self.tr("Table of Contents")))
|
||||
self.outerBox.addLayout(self.headBox)
|
||||
self.outerBox.addWidget(self.tocTree)
|
||||
self.outerBox.addLayout(self.optionsBox)
|
||||
|
||||
@@ -412,19 +430,35 @@ class GuiProjectDetailsContents(QWidget):
|
||||
def updateValues(self):
|
||||
"""Populate the tree.
|
||||
"""
|
||||
self._prepareData()
|
||||
self._currentRoot = None
|
||||
self._populateNovelList()
|
||||
|
||||
rootHandle = self.novelValue.currentData()
|
||||
self._prepareData(rootHandle)
|
||||
self._populateTree()
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _prepareData(self):
|
||||
"""Extract the data for the tree.
|
||||
def _populateNovelList(self):
|
||||
"""Fill the novel combo box with a list of all novel folders.
|
||||
"""
|
||||
self._theToC = []
|
||||
self._theToC = self.theProject.index.getTableOfContents(2)
|
||||
self.novelValue.clear()
|
||||
|
||||
tIcon = self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])
|
||||
for tHandle, nwItem in self.theProject.tree.iterRoots(nwItemClass.NOVEL):
|
||||
self.novelValue.addItem(tIcon, nwItem.itemName, tHandle)
|
||||
|
||||
return
|
||||
|
||||
def _prepareData(self, rootHandle):
|
||||
"""Extract the information from the project index.
|
||||
"""
|
||||
logger.debug("Populating ToC from handle '%s'", rootHandle)
|
||||
self._theToC = self.theProject.index.getTableOfContents(rootHandle, 2)
|
||||
self._theToC.append(("", 0, self.tr("END"), 0))
|
||||
return
|
||||
|
||||
@@ -432,6 +466,18 @@ class GuiProjectDetailsContents(QWidget):
|
||||
# Slots
|
||||
##
|
||||
|
||||
@pyqtSlot()
|
||||
def _novelValueChanged(self):
|
||||
"""Refresh the tree with another root item.
|
||||
"""
|
||||
rootHandle = self.novelValue.currentData()
|
||||
if rootHandle != self._currentRoot:
|
||||
self._prepareData(rootHandle)
|
||||
self._populateTree()
|
||||
self._currentRoot = rootHandle
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _populateTree(self):
|
||||
"""Set the content of the chapter/page tree.
|
||||
"""
|
||||
@@ -466,10 +512,11 @@ class GuiProjectDetailsContents(QWidget):
|
||||
progPage = f"{cPage:n}"
|
||||
progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%"
|
||||
|
||||
hDec = self.mainTheme.getHeaderDecoration(tLevel)
|
||||
if tTitle.strip() == "":
|
||||
tTitle = self.tr("Untitled")
|
||||
|
||||
newItem.setIcon(self.C_TITLE, self.mainTheme.getIcon("doc_h%d" % tLevel))
|
||||
newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
|
||||
newItem.setText(self.C_TITLE, tTitle)
|
||||
newItem.setText(self.C_WORDS, f"{wCount:n}")
|
||||
newItem.setText(self.C_PAGES, f"{pCount:n}")
|
||||
|
||||
@@ -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/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.QtGui import QKeySequence
|
||||
@@ -54,7 +54,7 @@ class GuiProjectLoad(QDialog):
|
||||
C_TIME = 2
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QDialog.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiProjectLoad ...")
|
||||
self.setObjectName("GuiProjectLoad")
|
||||
@@ -77,7 +77,6 @@ class GuiProjectLoad(QDialog):
|
||||
self.setWindowTitle(self.tr("Open Project"))
|
||||
self.setMinimumWidth(self.mainConf.pxInt(650))
|
||||
self.setMinimumHeight(self.mainConf.pxInt(400))
|
||||
self.setModal(True)
|
||||
|
||||
self.nwIcon = QLabel()
|
||||
self.nwIcon.setPixmap(self.mainGui.mainTheme.getPixmap("novelwriter", (nPx, nPx)))
|
||||
@@ -158,7 +157,6 @@ class GuiProjectLoad(QDialog):
|
||||
def _doOpenRecent(self):
|
||||
"""Close the dialog window with a recent project selected.
|
||||
"""
|
||||
logger.verbose("GuiProjectLoad open button clicked")
|
||||
self._saveSettings()
|
||||
|
||||
self.openPath = None
|
||||
@@ -183,7 +181,6 @@ class GuiProjectLoad(QDialog):
|
||||
def _doBrowse(self):
|
||||
"""Browse for a folder path.
|
||||
"""
|
||||
logger.verbose("GuiProjectLoad browse button clicked")
|
||||
extFilter = [
|
||||
self.tr("novelWriter Project File ({0})").format(nwFiles.PROJ_FILE),
|
||||
self.tr("All files ({0})").format("*"),
|
||||
@@ -192,8 +189,8 @@ class GuiProjectLoad(QDialog):
|
||||
self, self.tr("Open Project"), "", filter=";;".join(extFilter)
|
||||
)
|
||||
if projFile:
|
||||
thePath = os.path.abspath(os.path.dirname(projFile))
|
||||
self.selPath.setText(thePath)
|
||||
thePath = Path(projFile).absolute()
|
||||
self.selPath.setText(str(thePath))
|
||||
self.openPath = thePath
|
||||
self.openState = self.OPEN_STATE
|
||||
self.accept()
|
||||
@@ -203,7 +200,6 @@ class GuiProjectLoad(QDialog):
|
||||
def _doCancel(self):
|
||||
"""Close the dialog window without doing anything.
|
||||
"""
|
||||
logger.verbose("GuiProjectLoad close button clicked")
|
||||
self.openPath = None
|
||||
self.openState = self.NONE_STATE
|
||||
self.close()
|
||||
@@ -212,7 +208,6 @@ class GuiProjectLoad(QDialog):
|
||||
def _doNewProject(self):
|
||||
"""Create a new project.
|
||||
"""
|
||||
logger.verbose("GuiProjectLoad new project button clicked")
|
||||
self._saveSettings()
|
||||
self.openPath = None
|
||||
self.openState = self.NEW_STATE
|
||||
@@ -233,7 +228,7 @@ class GuiProjectLoad(QDialog):
|
||||
).format(projName)
|
||||
)
|
||||
if msgYes:
|
||||
self.mainConf.removeFromRecentCache(
|
||||
self.mainConf.recentProjects.remove(
|
||||
selList[0].data(self.C_NAME, Qt.UserRole)
|
||||
)
|
||||
self._populateList()
|
||||
@@ -262,29 +257,23 @@ class GuiProjectLoad(QDialog):
|
||||
colWidths[self.C_NAME] = self.listBox.columnWidth(self.C_NAME)
|
||||
colWidths[self.C_COUNT] = self.listBox.columnWidth(self.C_COUNT)
|
||||
colWidths[self.C_TIME] = self.listBox.columnWidth(self.C_TIME)
|
||||
self.mainConf.setProjColWidths(colWidths)
|
||||
self.mainConf.setProjLoadColWidths(colWidths)
|
||||
return
|
||||
|
||||
def _populateList(self):
|
||||
"""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()
|
||||
sortList = sorted(dataList, key=lambda x: x[1], reverse=True)
|
||||
for theTitle, theTime, theWords, projPath in sortList:
|
||||
dataList = self.mainConf.recentProjects.listEntries()
|
||||
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.setIcon(self.C_NAME, self.mainGui.mainTheme.getIcon("proj_nwx"))
|
||||
newItem.setText(self.C_NAME, theTitle)
|
||||
newItem.setData(self.C_NAME, Qt.UserRole, projPath)
|
||||
newItem.setText(self.C_COUNT, formatInt(theWords))
|
||||
newItem.setText(self.C_TIME, datetime.fromtimestamp(theTime).strftime("%x %X"))
|
||||
newItem.setIcon(self.C_NAME, nwxIcon)
|
||||
newItem.setText(self.C_NAME, title)
|
||||
newItem.setData(self.C_NAME, Qt.UserRole, path)
|
||||
newItem.setText(self.C_COUNT, formatInt(words))
|
||||
newItem.setText(self.C_TIME, datetime.fromtimestamp(time).strftime("%x %X"))
|
||||
newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter)
|
||||
newItem.setTextAlignment(self.C_COUNT, 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:
|
||||
self.listBox.topLevelItem(0).setSelected(True)
|
||||
|
||||
projColWidth = self.mainConf.getProjColWidths()
|
||||
projColWidth = self.mainConf.projLoadColWidths
|
||||
if len(projColWidth) == 3:
|
||||
self.listBox.setColumnWidth(self.C_NAME, projColWidth[self.C_NAME])
|
||||
self.listBox.setColumnWidth(self.C_COUNT, projColWidth[self.C_COUNT])
|
||||
|
||||
@@ -36,15 +36,20 @@ from PyQt5.QtWidgets import (
|
||||
|
||||
from novelwriter.enum import nwAlert
|
||||
from novelwriter.common import simplified
|
||||
from novelwriter.gui.custom import QSwitch, PagedDialog, QConfigLayout
|
||||
from novelwriter.custom import QSwitch, PagedDialog, QConfigLayout
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiProjectSettings(PagedDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
PagedDialog.__init__(self, mainGui)
|
||||
TAB_MAIN = 0
|
||||
TAB_STATUS = 1
|
||||
TAB_IMPORT = 2
|
||||
TAB_REPLACE = 3
|
||||
|
||||
def __init__(self, mainGui, focusTab=TAB_MAIN):
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiProjectSettings ...")
|
||||
self.setObjectName("GuiProjectSettings")
|
||||
@@ -67,10 +72,10 @@ class GuiProjectSettings(PagedDialog):
|
||||
self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH))
|
||||
)
|
||||
|
||||
self.tabMain = GuiProjectEditMain(self.mainGui, self.theProject)
|
||||
self.tabStatus = GuiProjectEditStatus(self.mainGui, self.theProject, True)
|
||||
self.tabImport = GuiProjectEditStatus(self.mainGui, self.theProject, False)
|
||||
self.tabReplace = GuiProjectEditReplace(self.mainGui, self.theProject)
|
||||
self.tabMain = GuiProjectEditMain(self)
|
||||
self.tabStatus = GuiProjectEditStatus(self, True)
|
||||
self.tabImport = GuiProjectEditStatus(self, False)
|
||||
self.tabReplace = GuiProjectEditReplace(self)
|
||||
|
||||
self.addTab(self.tabMain, self.tr("Settings"))
|
||||
self.addTab(self.tabStatus, self.tr("Status"))
|
||||
@@ -83,12 +88,19 @@ class GuiProjectSettings(PagedDialog):
|
||||
self.addControls(self.buttonBox)
|
||||
|
||||
# Flags
|
||||
self.spellChanged = False
|
||||
self._spellChanged = False
|
||||
|
||||
# Focus Tab
|
||||
self._focusTab(focusTab)
|
||||
|
||||
logger.debug("GuiProjectSettings initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
@property
|
||||
def spellChanged(self):
|
||||
return self._spellChanged
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
@@ -96,21 +108,19 @@ class GuiProjectSettings(PagedDialog):
|
||||
def _doSave(self):
|
||||
"""Save settings and close dialog.
|
||||
"""
|
||||
logger.verbose("GuiProjectSettings save button clicked")
|
||||
|
||||
projName = self.tabMain.editName.text()
|
||||
bookTitle = self.tabMain.editTitle.text()
|
||||
bookAuthors = self.tabMain.editAuthors.toPlainText()
|
||||
spellLang = self.tabMain.spellLang.currentData()
|
||||
doBackup = not self.tabMain.doBackup.isChecked()
|
||||
|
||||
self.theProject.setProjectName(projName)
|
||||
self.theProject.setBookTitle(bookTitle)
|
||||
self.theProject.setBookAuthors(bookAuthors)
|
||||
self.theProject.setProjBackup(doBackup)
|
||||
self.theProject.data.setName(projName)
|
||||
self.theProject.data.setTitle(bookTitle)
|
||||
self.theProject.data.setAuthors(bookAuthors)
|
||||
self.theProject.data.setDoBackup(doBackup)
|
||||
|
||||
# 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:
|
||||
newList, delList = self.tabStatus.getNewList()
|
||||
@@ -125,7 +135,7 @@ class GuiProjectSettings(PagedDialog):
|
||||
|
||||
if self.tabReplace.arChanged:
|
||||
newList = self.tabReplace.getNewList()
|
||||
self.theProject.setAutoReplace(newList)
|
||||
self.theProject.data.setAutoReplace(newList)
|
||||
|
||||
self._saveGuiSettings()
|
||||
self.accept()
|
||||
@@ -143,6 +153,19 @@ class GuiProjectSettings(PagedDialog):
|
||||
# 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):
|
||||
"""Save GUI settings.
|
||||
"""
|
||||
@@ -166,12 +189,12 @@ class GuiProjectSettings(PagedDialog):
|
||||
|
||||
class GuiProjectEditMain(QWidget):
|
||||
|
||||
def __init__(self, mainGui, theProject):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, projGui):
|
||||
super().__init__(parent=projGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.theProject = theProject
|
||||
self.mainGui = projGui.mainGui
|
||||
self.theProject = projGui.theProject
|
||||
|
||||
# The Form
|
||||
self.mainForm = QConfigLayout()
|
||||
@@ -186,7 +209,7 @@ class GuiProjectEditMain(QWidget):
|
||||
self.editName = QLineEdit()
|
||||
self.editName.setMaxLength(200)
|
||||
self.editName.setMaximumWidth(xW)
|
||||
self.editName.setText(self.theProject.projName)
|
||||
self.editName.setText(self.theProject.data.name)
|
||||
self.mainForm.addRow(
|
||||
self.tr("Project name"),
|
||||
self.editName,
|
||||
@@ -196,7 +219,7 @@ class GuiProjectEditMain(QWidget):
|
||||
self.editTitle = QLineEdit()
|
||||
self.editTitle.setMaxLength(200)
|
||||
self.editTitle.setMaximumWidth(xW)
|
||||
self.editTitle.setText(self.theProject.bookTitle)
|
||||
self.editTitle.setText(self.theProject.data.title)
|
||||
self.mainForm.addRow(
|
||||
self.tr("Novel title"),
|
||||
self.editTitle,
|
||||
@@ -206,7 +229,7 @@ class GuiProjectEditMain(QWidget):
|
||||
self.editAuthors = QPlainTextEdit()
|
||||
self.editAuthors.setMaximumHeight(xH)
|
||||
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.tr("Author(s)"),
|
||||
self.editAuthors,
|
||||
@@ -229,13 +252,13 @@ class GuiProjectEditMain(QWidget):
|
||||
)
|
||||
|
||||
spellIdx = 0
|
||||
if self.theProject.projSpell is not None:
|
||||
spellIdx = self.spellLang.findData(self.theProject.projSpell)
|
||||
if self.theProject.data.spellLang is not None:
|
||||
spellIdx = self.spellLang.findData(self.theProject.data.spellLang)
|
||||
if spellIdx != -1:
|
||||
self.spellLang.setCurrentIndex(spellIdx)
|
||||
|
||||
self.doBackup = QSwitch(self)
|
||||
self.doBackup.setChecked(not self.theProject.doBackup)
|
||||
self.doBackup.setChecked(not self.theProject.data.doBackup)
|
||||
self.mainForm.addRow(
|
||||
self.tr("No backup on close"),
|
||||
self.doBackup,
|
||||
@@ -256,20 +279,20 @@ class GuiProjectEditStatus(QWidget):
|
||||
COL_ROLE = Qt.UserRole + 1
|
||||
NUM_ROLE = Qt.UserRole + 2
|
||||
|
||||
def __init__(self, mainGui, theProject, isStatus):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, projGui, isStatus):
|
||||
super().__init__(parent=projGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.theProject = theProject
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.mainGui = projGui.mainGui
|
||||
self.theProject = projGui.theProject
|
||||
self.mainTheme = projGui.mainGui.mainTheme
|
||||
|
||||
if isStatus:
|
||||
self.theStatus = self.theProject.statusItems
|
||||
self.theStatus = self.theProject.data.itemStatus
|
||||
pageLabel = self.tr("Novel File Status Levels")
|
||||
colSetting = "statusColW"
|
||||
else:
|
||||
self.theStatus = self.theProject.importItems
|
||||
self.theStatus = self.theProject.data.itemImport
|
||||
pageLabel = self.tr("Note File Importance Levels")
|
||||
colSetting = "importColW"
|
||||
|
||||
@@ -367,11 +390,12 @@ class GuiProjectEditStatus(QWidget):
|
||||
newList = []
|
||||
for n in range(self.listBox.topLevelItemCount()):
|
||||
item = self.listBox.topLevelItem(n)
|
||||
newList.append({
|
||||
"key": item.data(self.COL_LABEL, self.KEY_ROLE),
|
||||
"name": item.text(self.COL_LABEL),
|
||||
"cols": item.data(self.COL_LABEL, self.COL_ROLE),
|
||||
})
|
||||
if item is not None:
|
||||
newList.append({
|
||||
"key": item.data(self.COL_LABEL, self.KEY_ROLE),
|
||||
"name": item.text(self.COL_LABEL),
|
||||
"cols": item.data(self.COL_LABEL, self.COL_ROLE),
|
||||
})
|
||||
return newList, self.colDeleted
|
||||
|
||||
return [], []
|
||||
@@ -470,7 +494,8 @@ class GuiProjectEditStatus(QWidget):
|
||||
self.listBox.insertTopLevelItem(nIndex, cItem)
|
||||
self.listBox.clearSelection()
|
||||
|
||||
cItem.setSelected(True)
|
||||
if cItem is not None:
|
||||
cItem.setSelected(True)
|
||||
self.colChanged = True
|
||||
|
||||
return
|
||||
@@ -527,13 +552,13 @@ class GuiProjectEditReplace(QWidget):
|
||||
COL_KEY = 0
|
||||
COL_REPL = 1
|
||||
|
||||
def __init__(self, mainGui, theProject):
|
||||
QWidget.__init__(self, mainGui)
|
||||
def __init__(self, projGui):
|
||||
super().__init__(parent=projGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainTheme = mainGui.mainTheme
|
||||
self.theProject = theProject
|
||||
self.mainGui = projGui.mainGui
|
||||
self.mainTheme = projGui.mainGui.mainTheme
|
||||
self.theProject = projGui.theProject
|
||||
self.arChanged = False
|
||||
|
||||
wCol0 = self.mainConf.pxInt(
|
||||
@@ -553,7 +578,7 @@ class GuiProjectEditReplace(QWidget):
|
||||
self.listBox.setColumnWidth(self.COL_KEY, wCol0)
|
||||
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])
|
||||
self.listBox.addTopLevelItem(newItem)
|
||||
|
||||
@@ -619,10 +644,11 @@ class GuiProjectEditReplace(QWidget):
|
||||
newList = {}
|
||||
for n in range(self.listBox.topLevelItemCount()):
|
||||
tItem = self.listBox.topLevelItem(n)
|
||||
aKey = self._stripNotAllowed(tItem.text(0))
|
||||
aVal = tItem.text(1)
|
||||
if len(aKey) > 0:
|
||||
newList[aKey] = aVal
|
||||
if tItem is not None:
|
||||
aKey = self._stripNotAllowed(tItem.text(0))
|
||||
aVal = tItem.text(1)
|
||||
if len(aKey) > 0:
|
||||
newList[aKey] = aVal
|
||||
|
||||
return newList
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class GuiQuoteSelect(QDialog):
|
||||
selectedQuote = ""
|
||||
|
||||
def __init__(self, parent=None, currentQuote='"'):
|
||||
QDialog.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiUpdates(QDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QDialog.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiUpdates ...")
|
||||
self.setObjectName("GuiUpdates")
|
||||
@@ -135,7 +135,7 @@ class GuiUpdates(QDialog):
|
||||
logException()
|
||||
|
||||
relVersion = rawData.get("tag_name", "Unknown")
|
||||
relDate = rawData.get("created_at", None)
|
||||
relDate = rawData.get("created_at", "")
|
||||
|
||||
try:
|
||||
relDate = datetime.strptime(relDate[:10], "%Y-%m-%d").strftime("%x")
|
||||
|
||||
@@ -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/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget,
|
||||
@@ -43,7 +44,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiWordList(QDialog):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QDialog.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiWordList ...")
|
||||
self.setObjectName("GuiWordList")
|
||||
@@ -150,13 +151,19 @@ class GuiWordList(QDialog):
|
||||
"""
|
||||
self._saveGuiSettings()
|
||||
|
||||
dctFile = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT)
|
||||
tmpFile = dctFile + "~"
|
||||
dctFile = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT)
|
||||
if not isinstance(dctFile, Path):
|
||||
return False
|
||||
|
||||
tmpFile = dctFile.with_suffix(".tmp")
|
||||
try:
|
||||
with open(tmpFile, mode="w", encoding="utf-8") as outFile:
|
||||
for i in range(self.listBox.count()):
|
||||
outFile.write(self.listBox.item(i).text() + "\n")
|
||||
item = self.listBox.item(i)
|
||||
if item is not None:
|
||||
outFile.write(item.text() + "\n")
|
||||
|
||||
tmpFile.replace(dctFile)
|
||||
|
||||
except Exception:
|
||||
logger.error("Could not save new word list")
|
||||
@@ -164,9 +171,6 @@ class GuiWordList(QDialog):
|
||||
self.reject()
|
||||
return False
|
||||
|
||||
if os.path.isfile(dctFile):
|
||||
os.unlink(dctFile)
|
||||
os.rename(tmpFile, dctFile)
|
||||
self.accept()
|
||||
|
||||
return True
|
||||
@@ -185,10 +189,12 @@ class GuiWordList(QDialog):
|
||||
def _loadWordList(self):
|
||||
"""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)
|
||||
if not os.path.isfile(wordList):
|
||||
self.listBox.clear()
|
||||
if not wordList.exists():
|
||||
logger.debug("No project dictionary file found")
|
||||
return False
|
||||
|
||||
|
||||
@@ -112,9 +112,10 @@ class nwDocInsert(Enum):
|
||||
QUOTE_RS = 2
|
||||
QUOTE_LD = 3
|
||||
QUOTE_RD = 4
|
||||
NEW_PAGE = 5
|
||||
VSPACE_S = 6
|
||||
VSPACE_M = 7
|
||||
SYNOPSIS = 5
|
||||
NEW_PAGE = 6
|
||||
VSPACE_S = 7
|
||||
VSPACE_M = 8
|
||||
|
||||
# END Enum nwDocInsert
|
||||
|
||||
|
||||
@@ -44,7 +44,8 @@ def logException():
|
||||
"""Log the content of an exception message.
|
||||
"""
|
||||
exType, exValue, _ = sys.exc_info()
|
||||
logger.error("%s: %s", exType.__name__, str(exValue))
|
||||
if exType is not None:
|
||||
logger.error("%s: %s", exType.__name__, str(exValue))
|
||||
|
||||
|
||||
def formatException(exc):
|
||||
@@ -61,7 +62,7 @@ def formatException(exc):
|
||||
class NWErrorMessage(QDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
QDialog.__init__(self, parent=parent)
|
||||
super().__init__(parent=parent)
|
||||
self.setObjectName("NWErrorMessage")
|
||||
|
||||
# Widgets
|
||||
@@ -131,7 +132,7 @@ class NWErrorMessage(QDialog):
|
||||
|
||||
try:
|
||||
import lxml
|
||||
lxmlVersion = lxml.__version__
|
||||
lxmlVersion = lxml.__version__ # type: ignore
|
||||
except Exception:
|
||||
lxmlVersion = "Unknown"
|
||||
|
||||
@@ -198,8 +199,8 @@ def exceptionHandler(exType, exValue, exTrace):
|
||||
|
||||
try:
|
||||
# Try a controlled shutdown
|
||||
nwGUI.closeProject(isYes=True)
|
||||
nwGUI.closeMain()
|
||||
nwGUI.closeProject(isYes=True) # type: ignore
|
||||
nwGUI.closeMain() # type: ignore
|
||||
logger.info("Emergency shutdown successful")
|
||||
|
||||
except Exception as exc:
|
||||
|
||||
@@ -47,7 +47,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
BLOCK_TITLE = 4
|
||||
|
||||
def __init__(self, theDoc, mainGui, spEnchant):
|
||||
QSyntaxHighlighter.__init__(self, theDoc)
|
||||
super().__init__(theDoc)
|
||||
|
||||
logger.debug("Initialising GuiDocHighlighter ...")
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
@@ -151,11 +151,13 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
# Quoted Strings
|
||||
if self.mainConf.highlightQuotes:
|
||||
fmtDbl = self.mainConf.fmtDoubleQuotes
|
||||
fmtSng = self.mainConf.fmtSingleQuotes
|
||||
fmtDblO = self.mainConf.fmtDQuoteOpen
|
||||
fmtDblC = self.mainConf.fmtDQuoteClose
|
||||
fmtSngO = self.mainConf.fmtSQuoteOpen
|
||||
fmtSngC = self.mainConf.fmtSQuoteClose
|
||||
|
||||
# Straight Quotes
|
||||
if fmtDbl != ["\"", "\""]:
|
||||
if not (fmtDblO == fmtDblC == "\""):
|
||||
self.hRules.append((
|
||||
"(\\B\")(.*?)(\"\\B)", {
|
||||
0: self.hStyles["dialogue1"],
|
||||
@@ -165,7 +167,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
# Double Quotes
|
||||
dblEnd = "|$" if self.mainConf.allowOpenDQuote else ""
|
||||
self.hRules.append((
|
||||
f"(\\B{fmtDbl[0]})(.*?)({fmtDbl[1]}\\B{dblEnd})", {
|
||||
f"(\\B{fmtDblO})(.*?)({fmtDblC}\\B{dblEnd})", {
|
||||
0: self.hStyles["dialogue2"],
|
||||
}
|
||||
))
|
||||
@@ -173,7 +175,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
# Single Quotes
|
||||
sngEnd = "|$" if self.mainConf.allowOpenSQuote else ""
|
||||
self.hRules.append((
|
||||
f"(\\B{fmtSng[0]})(.*?)({fmtSng[1]}\\B{sngEnd})", {
|
||||
f"(\\B{fmtSngO})(.*?)({fmtSngC}\\B{sngEnd})", {
|
||||
0: self.hStyles["dialogue3"],
|
||||
}
|
||||
))
|
||||
|
||||
@@ -54,7 +54,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QTextBrowser.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiDocViewer ...")
|
||||
|
||||
@@ -102,6 +102,13 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.docHeader.setTitleFromHandle(self._docHandle)
|
||||
return True
|
||||
|
||||
def updateTheme(self):
|
||||
"""Update theme elements.
|
||||
"""
|
||||
self.docHeader.updateTheme()
|
||||
self.docFooter.updateTheme()
|
||||
return
|
||||
|
||||
def initViewer(self):
|
||||
"""Set editor settings from main config.
|
||||
"""
|
||||
@@ -150,10 +157,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||
|
||||
# Refresh the tab stops
|
||||
if self.mainConf.verQtValue >= 51000:
|
||||
self.setTabStopDistance(self.mainConf.getTabWidth())
|
||||
else:
|
||||
self.setTabStopWidth(self.mainConf.getTabWidth())
|
||||
self.setTabStopDistance(self.mainConf.getTabWidth())
|
||||
|
||||
# If we have a document open, we should reload it in case the font changed
|
||||
if self._docHandle is not None:
|
||||
@@ -193,10 +197,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
return False
|
||||
|
||||
# Refresh the tab stops
|
||||
if self.mainConf.verQtValue >= 51000:
|
||||
self.setTabStopDistance(self.mainConf.getTabWidth())
|
||||
else:
|
||||
self.setTabStopWidth(self.mainConf.getTabWidth())
|
||||
self.setTabStopDistance(self.mainConf.getTabWidth())
|
||||
|
||||
# Must be before setHtml
|
||||
if updateHistory:
|
||||
@@ -216,7 +217,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
self.verticalScrollBar().setValue(sPos)
|
||||
|
||||
self._docHandle = tHandle
|
||||
self.theProject.setLastViewed(tHandle)
|
||||
self.theProject._data.setLastHandle(tHandle, "viewer")
|
||||
self.docHeader.setTitleFromHandle(self._docHandle)
|
||||
self.updateDocMargins()
|
||||
|
||||
@@ -247,7 +248,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
"""Wrapper function for various document actions on the current
|
||||
document.
|
||||
"""
|
||||
logger.verbose("Requesting action: '%s'", theAction.name)
|
||||
logger.debug("Requesting action: '%s'", theAction.name)
|
||||
if self._docHandle is None:
|
||||
logger.error("No document open")
|
||||
return False
|
||||
@@ -270,7 +271,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
if not isinstance(tAnchor, str):
|
||||
return False
|
||||
if tAnchor.startswith("#"):
|
||||
logger.verbose("Moving to anchor '%s'", tAnchor)
|
||||
logger.debug("Moving to anchor '%s'", tAnchor)
|
||||
self.setSource(QUrl(tAnchor))
|
||||
return True
|
||||
|
||||
@@ -356,7 +357,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
theBlock = self.document().findBlockByLineNumber(theLine)
|
||||
if theBlock:
|
||||
self.setCursorPosition(theBlock.position())
|
||||
logger.verbose("Cursor moved to line %d", theLine)
|
||||
logger.debug("Cursor moved to line %d", theLine)
|
||||
return True
|
||||
|
||||
def setScrollPosition(self, thePos):
|
||||
@@ -402,7 +403,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
"""Process a clicked link internally in the document.
|
||||
"""
|
||||
theLink = theURL.url()
|
||||
logger.verbose("Clicked link: '%s'", theLink)
|
||||
logger.debug("Clicked link: '%s'", theLink)
|
||||
if len(theLink) > 0:
|
||||
theBits = theLink.split("=")
|
||||
if len(theBits) == 2:
|
||||
@@ -461,7 +462,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
has its margins adjusted according to user preferences.
|
||||
"""
|
||||
self.updateDocMargins()
|
||||
QTextBrowser.resizeEvent(self, theEvent)
|
||||
super().resizeEvent(theEvent)
|
||||
return
|
||||
|
||||
def mouseReleaseEvent(self, theEvent):
|
||||
@@ -472,7 +473,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
elif theEvent.button() == Qt.ForwardButton:
|
||||
self.navForward()
|
||||
else:
|
||||
QTextBrowser.mouseReleaseEvent(self, theEvent)
|
||||
super().mouseReleaseEvent(theEvent)
|
||||
return
|
||||
|
||||
##
|
||||
@@ -568,7 +569,7 @@ class GuiDocViewer(QTextBrowser):
|
||||
# END Class GuiDocViewer
|
||||
|
||||
|
||||
class GuiDocViewHistory():
|
||||
class GuiDocViewHistory:
|
||||
|
||||
def __init__(self, docViewer):
|
||||
|
||||
@@ -584,7 +585,7 @@ class GuiDocViewHistory():
|
||||
def clear(self):
|
||||
"""Clear the view history.
|
||||
"""
|
||||
logger.verbose("View history cleared")
|
||||
logger.debug("View history cleared")
|
||||
self._navHistory = []
|
||||
self._posHistory = []
|
||||
self._currPos = -1
|
||||
@@ -598,7 +599,7 @@ class GuiDocViewHistory():
|
||||
"""
|
||||
if self._currPos >= 0 and self._currPos < len(self._navHistory):
|
||||
if tHandle == self._navHistory[self._currPos]:
|
||||
logger.verbose("Not updating view hsitory")
|
||||
logger.debug("Not updating view hsitory")
|
||||
return False
|
||||
|
||||
self._truncateHistory(self._currPos)
|
||||
@@ -613,7 +614,7 @@ class GuiDocViewHistory():
|
||||
|
||||
self._dumpHistory()
|
||||
|
||||
logger.verbose("Added '%s' to view history", tHandle)
|
||||
logger.debug("Added '%s' to view history", tHandle)
|
||||
|
||||
return True
|
||||
|
||||
@@ -622,7 +623,7 @@ class GuiDocViewHistory():
|
||||
"""
|
||||
newPos = self._currPos + 1
|
||||
if newPos < len(self._navHistory):
|
||||
logger.verbose("Move forward in view history")
|
||||
logger.debug("Move forward in view history")
|
||||
self._prevPos = self._currPos
|
||||
self._updateScrollBar()
|
||||
|
||||
@@ -640,7 +641,7 @@ class GuiDocViewHistory():
|
||||
"""
|
||||
newPos = self._currPos - 1
|
||||
if newPos >= 0:
|
||||
logger.verbose("Move backward in view history")
|
||||
logger.debug("Move backward in view history")
|
||||
self._prevPos = self._currPos
|
||||
self._updateScrollBar()
|
||||
|
||||
@@ -686,11 +687,11 @@ class GuiDocViewHistory():
|
||||
|
||||
def _dumpHistory(self):
|
||||
"""Debug function to dump history to the logger. Since it is a
|
||||
for loop, it is skipped entirely if log level isn't VERBOSE.
|
||||
for loop, it is skipped entirely if log level isn't DEBUG.
|
||||
"""
|
||||
if logger.getEffectiveLevel() < logging.DEBUG:
|
||||
if logger.getEffectiveLevel() == logging.DEBUG:
|
||||
for i, (h, p) in enumerate(zip(self._navHistory, self._posHistory)):
|
||||
logger.verbose(
|
||||
logger.debug(
|
||||
"History %02d: %s %13s [x:%d]" % (
|
||||
i + 1, ">" if i == self._currPos else " ", h, p
|
||||
)
|
||||
@@ -708,7 +709,7 @@ class GuiDocViewHistory():
|
||||
class GuiDocViewHeader(QWidget):
|
||||
|
||||
def __init__(self, docViewer):
|
||||
QWidget.__init__(self, docViewer)
|
||||
super().__init__(parent=docViewer)
|
||||
|
||||
logger.debug("Initialising GuiDocViewHeader ...")
|
||||
|
||||
@@ -741,51 +742,38 @@ class GuiDocViewHeader(QWidget):
|
||||
lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize)
|
||||
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
|
||||
self.backButton = QToolButton(self)
|
||||
self.backButton.setIcon(self.mainTheme.getIcon("backward"))
|
||||
self.backButton.setContentsMargins(0, 0, 0, 0)
|
||||
self.backButton.setIconSize(QSize(fPx, fPx))
|
||||
self.backButton.setFixedSize(fPx, fPx)
|
||||
self.backButton.setStyleSheet(buttonStyle)
|
||||
self.backButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.backButton.setVisible(False)
|
||||
self.backButton.setToolTip(self.tr("Go backward"))
|
||||
self.backButton.clicked.connect(self.docViewer.navBackward)
|
||||
|
||||
self.forwardButton = QToolButton(self)
|
||||
self.forwardButton.setIcon(self.mainTheme.getIcon("forward"))
|
||||
self.forwardButton.setContentsMargins(0, 0, 0, 0)
|
||||
self.forwardButton.setIconSize(QSize(fPx, fPx))
|
||||
self.forwardButton.setFixedSize(fPx, fPx)
|
||||
self.forwardButton.setStyleSheet(buttonStyle)
|
||||
self.forwardButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.forwardButton.setVisible(False)
|
||||
self.forwardButton.setToolTip(self.tr("Go forward"))
|
||||
self.forwardButton.clicked.connect(self.docViewer.navForward)
|
||||
|
||||
self.refreshButton = QToolButton(self)
|
||||
self.refreshButton.setIcon(self.mainTheme.getIcon("refresh"))
|
||||
self.refreshButton.setContentsMargins(0, 0, 0, 0)
|
||||
self.refreshButton.setIconSize(QSize(fPx, fPx))
|
||||
self.refreshButton.setFixedSize(fPx, fPx)
|
||||
self.refreshButton.setStyleSheet(buttonStyle)
|
||||
self.refreshButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.refreshButton.setVisible(False)
|
||||
self.refreshButton.setToolTip(self.tr("Reload the document"))
|
||||
self.refreshButton.clicked.connect(self._refreshDocument)
|
||||
|
||||
self.closeButton = QToolButton(self)
|
||||
self.closeButton.setIcon(self.mainTheme.getIcon("close"))
|
||||
self.closeButton.setContentsMargins(0, 0, 0, 0)
|
||||
self.closeButton.setIconSize(QSize(fPx, fPx))
|
||||
self.closeButton.setFixedSize(fPx, fPx)
|
||||
self.closeButton.setStyleSheet(buttonStyle)
|
||||
self.closeButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.closeButton.setVisible(False)
|
||||
self.closeButton.setToolTip(self.tr("Close the document"))
|
||||
@@ -809,7 +797,7 @@ class GuiDocViewHeader(QWidget):
|
||||
self.setMinimumHeight(fPx + 2*cM)
|
||||
|
||||
# Fix the Colours
|
||||
self.matchColours()
|
||||
self.updateTheme()
|
||||
|
||||
logger.debug("GuiDocViewHeader initialisation complete")
|
||||
|
||||
@@ -819,6 +807,28 @@ class GuiDocViewHeader(QWidget):
|
||||
# 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):
|
||||
"""Update the colours of the widget to match those of the syntax
|
||||
theme rather than the main GUI.
|
||||
@@ -879,12 +889,14 @@ class GuiDocViewHeader(QWidget):
|
||||
# Slots
|
||||
##
|
||||
|
||||
@pyqtSlot()
|
||||
def _closeDocument(self):
|
||||
"""Trigger the close editor/viewer on the main window.
|
||||
"""
|
||||
self.mainGui.closeDocViewer()
|
||||
return
|
||||
|
||||
@pyqtSlot()
|
||||
def _refreshDocument(self):
|
||||
"""Reload the content of the document.
|
||||
"""
|
||||
@@ -915,7 +927,7 @@ class GuiDocViewHeader(QWidget):
|
||||
class GuiDocViewFooter(QWidget):
|
||||
|
||||
def __init__(self, docViewer):
|
||||
QWidget.__init__(self, docViewer)
|
||||
super().__init__(parent=docViewer)
|
||||
|
||||
logger.debug("Initialising GuiDocViewFooter ...")
|
||||
|
||||
@@ -932,33 +944,13 @@ class GuiDocViewFooter(QWidget):
|
||||
bSp = self.mainConf.pxInt(2)
|
||||
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
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
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
|
||||
self.showHide = QToolButton(self)
|
||||
self.showHide.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.showHide.setStyleSheet(buttonStyle)
|
||||
self.showHide.setIcon(self.mainTheme.getIcon("reference"))
|
||||
self.showHide.setIconSize(QSize(fPx, fPx))
|
||||
self.showHide.setFixedSize(QSize(fPx, fPx))
|
||||
self.showHide.clicked.connect(self._doShowHide)
|
||||
@@ -968,8 +960,6 @@ class GuiDocViewFooter(QWidget):
|
||||
self.stickyRefs = QToolButton(self)
|
||||
self.stickyRefs.setCheckable(True)
|
||||
self.stickyRefs.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.stickyRefs.setStyleSheet(buttonStyle)
|
||||
self.stickyRefs.setIcon(stickyIcon)
|
||||
self.stickyRefs.setIconSize(QSize(fPx, fPx))
|
||||
self.stickyRefs.setFixedSize(QSize(fPx, fPx))
|
||||
self.stickyRefs.toggled.connect(self._doToggleSticky)
|
||||
@@ -982,8 +972,6 @@ class GuiDocViewFooter(QWidget):
|
||||
self.showComments.setCheckable(True)
|
||||
self.showComments.setChecked(self.mainConf.viewComments)
|
||||
self.showComments.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.showComments.setStyleSheet(buttonStyle)
|
||||
self.showComments.setIcon(bulletIcon)
|
||||
self.showComments.setIconSize(QSize(fPx, fPx))
|
||||
self.showComments.setFixedSize(QSize(fPx, fPx))
|
||||
self.showComments.toggled.connect(self._doToggleComments)
|
||||
@@ -994,8 +982,6 @@ class GuiDocViewFooter(QWidget):
|
||||
self.showSynopsis.setCheckable(True)
|
||||
self.showSynopsis.setChecked(self.mainConf.viewSynopsis)
|
||||
self.showSynopsis.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||
self.showSynopsis.setStyleSheet(buttonStyle)
|
||||
self.showSynopsis.setIcon(bulletIcon)
|
||||
self.showSynopsis.setIconSize(QSize(fPx, fPx))
|
||||
self.showSynopsis.setFixedSize(QSize(fPx, fPx))
|
||||
self.showSynopsis.toggled.connect(self._doToggleSynopsis)
|
||||
@@ -1069,7 +1055,7 @@ class GuiDocViewFooter(QWidget):
|
||||
self.setMinimumHeight(fPx + 2*cM)
|
||||
|
||||
# Fix the Colours
|
||||
self.matchColours()
|
||||
self.updateTheme()
|
||||
|
||||
logger.debug("GuiDocViewFooter initialisation complete")
|
||||
|
||||
@@ -1079,6 +1065,46 @@ class GuiDocViewFooter(QWidget):
|
||||
# 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):
|
||||
"""Update the colours of the widget to match those of the syntax
|
||||
theme rather than the main GUI.
|
||||
@@ -1100,6 +1126,7 @@ class GuiDocViewFooter(QWidget):
|
||||
# Slots
|
||||
##
|
||||
|
||||
@pyqtSlot()
|
||||
def _doShowHide(self):
|
||||
"""Toggle the expand/collapse of the panel.
|
||||
"""
|
||||
@@ -1107,26 +1134,29 @@ class GuiDocViewFooter(QWidget):
|
||||
self.viewMeta.setVisible(not isVisible)
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _doToggleSticky(self, theState):
|
||||
"""Toggle the sticky flag for the reference panel.
|
||||
"""
|
||||
logger.verbose("Reference sticky is %s", str(theState))
|
||||
logger.debug("Reference sticky is %s", str(theState))
|
||||
self.docViewer.stickyRef = theState
|
||||
if not theState and self.docViewer.docHandle() is not None:
|
||||
self.viewMeta.refreshReferences(self.docViewer.docHandle())
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _doToggleComments(self, theState):
|
||||
"""Toggle the view comment button and reload the document.
|
||||
"""
|
||||
self.mainConf.setViewComments(theState)
|
||||
self.mainConf.viewComments = theState
|
||||
self.docViewer.reloadText()
|
||||
return
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def _doToggleSynopsis(self, theState):
|
||||
"""Toggle the view synopsis button and reload the document.
|
||||
"""
|
||||
self.mainConf.setViewSynopsis(theState)
|
||||
self.mainConf.viewSynopsis = theState
|
||||
self.docViewer.reloadText()
|
||||
return
|
||||
|
||||
@@ -1141,7 +1171,7 @@ class GuiDocViewFooter(QWidget):
|
||||
class GuiDocViewDetails(QScrollArea):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QScrollArea.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiDocViewDetails ...")
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
@@ -1205,7 +1235,7 @@ class GuiDocViewDetails(QScrollArea):
|
||||
"""Capture the link-click and forward it to the document viewer
|
||||
class for handling.
|
||||
"""
|
||||
logger.verbose("Clicked link: '%s'", theLink)
|
||||
logger.debug("Clicked link: '%s'", theLink)
|
||||
if len(theLink) == 21:
|
||||
tHandle = theLink[:13]
|
||||
tAnchor = theLink[13:]
|
||||
|
||||
@@ -30,7 +30,6 @@ from PyQt5.QtCore import Qt, pyqtSlot
|
||||
from PyQt5.QtGui import QFont, QPixmap
|
||||
from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel
|
||||
|
||||
from novelwriter.enum import nwItemType
|
||||
from novelwriter.constants import trConst, nwLabels
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -39,7 +38,7 @@ logger = logging.getLogger(__name__)
|
||||
class GuiItemDetails(QWidget):
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiItemDetails ...")
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
@@ -54,12 +53,8 @@ class GuiItemDetails(QWidget):
|
||||
hSp = self.mainConf.pxInt(6)
|
||||
vSp = self.mainConf.pxInt(1)
|
||||
mPx = self.mainConf.pxInt(6)
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
fPt = self.mainTheme.fontPointSize
|
||||
|
||||
self._expCheck = self.mainTheme.getPixmap("check", (iPx, iPx))
|
||||
self._expCross = self.mainTheme.getPixmap("cross", (iPx, iPx))
|
||||
|
||||
fntLabel = QFont()
|
||||
fntLabel.setBold(True)
|
||||
fntLabel.setPointSizeF(0.9*fPt)
|
||||
@@ -180,6 +175,8 @@ class GuiItemDetails(QWidget):
|
||||
|
||||
self.setLayout(self.mainBox)
|
||||
|
||||
self.updateTheme()
|
||||
|
||||
# Make sure the columns for flags and counts don't resize too often
|
||||
flagWidth = self.mainTheme.getTextWidth("Mm", fntValue)
|
||||
countWidth = self.mainTheme.getTextWidth("99,999", fntValue)
|
||||
@@ -220,6 +217,12 @@ class GuiItemDetails(QWidget):
|
||||
"""
|
||||
self.updateViewBox(self._itemHandle)
|
||||
|
||||
def updateTheme(self):
|
||||
"""Update theme elements.
|
||||
"""
|
||||
self.updateViewBox(self._itemHandle)
|
||||
return
|
||||
|
||||
##
|
||||
# Public Slots
|
||||
##
|
||||
@@ -247,13 +250,13 @@ class GuiItemDetails(QWidget):
|
||||
if len(theLabel) > 100:
|
||||
theLabel = theLabel[:96].rstrip()+" ..."
|
||||
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
if nwItem.isExported:
|
||||
self.labelIcon.setPixmap(self._expCheck)
|
||||
if nwItem.isFileType():
|
||||
if nwItem.isActive:
|
||||
self.labelIcon.setPixmap(self.mainTheme.getPixmap("checked", (iPx, iPx)))
|
||||
else:
|
||||
self.labelIcon.setPixmap(self._expCross)
|
||||
self.labelIcon.setPixmap(self.mainTheme.getPixmap("unchecked", (iPx, iPx)))
|
||||
else:
|
||||
self.labelIcon.setPixmap(QPixmap(1, 1))
|
||||
self.labelIcon.setPixmap(self.mainTheme.getPixmap("noncheckable", (iPx, iPx)))
|
||||
|
||||
self.labelData.setText(theLabel)
|
||||
|
||||
@@ -274,17 +277,16 @@ class GuiItemDetails(QWidget):
|
||||
# Layout
|
||||
# ======
|
||||
|
||||
hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
|
||||
usageIcon = self.mainTheme.getItemIcon(
|
||||
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
|
||||
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading
|
||||
)
|
||||
self.usageIcon.setPixmap(usageIcon.pixmap(iPx, iPx))
|
||||
self.usageData.setText(nwItem.describeMe(hLevel))
|
||||
self.usageData.setText(nwItem.describeMe())
|
||||
|
||||
# Counts
|
||||
# ======
|
||||
|
||||
if nwItem.itemType == nwItemType.FILE:
|
||||
if nwItem.isFileType():
|
||||
self.cCountData.setText(f"{nwItem.charCount:n}")
|
||||
self.wCountData.setText(f"{nwItem.wordCount:n}")
|
||||
self.pCountData.setText(f"{nwItem.paraCount:n}")
|
||||
|
||||
@@ -26,6 +26,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import logging
|
||||
import novelwriter
|
||||
|
||||
from pathlib import Path
|
||||
from urllib.parse import urljoin
|
||||
from urllib.request import pathname2url
|
||||
|
||||
@@ -40,9 +41,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiMainMenu(QMenuBar):
|
||||
"""The GUI main menu. All menu actions are defined here with the
|
||||
main menu as the owner. Each widget that need them elsewhere need to
|
||||
add them from this class.
|
||||
"""
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QMenuBar.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
logger.debug("Initialising GuiMainMenu ...")
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
@@ -79,12 +84,6 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aSpellCheck.setChecked(theMode)
|
||||
return
|
||||
|
||||
def setFocusMode(self, theMode):
|
||||
"""Forward focus mode check state to its action.
|
||||
"""
|
||||
self.aFocusMode.setChecked(theMode)
|
||||
return
|
||||
|
||||
##
|
||||
# Slots
|
||||
##
|
||||
@@ -106,10 +105,11 @@ class GuiMainMenu(QMenuBar):
|
||||
def _openUserManualFile(self):
|
||||
"""Open the documentation in PDF format.
|
||||
"""
|
||||
if self.mainConf.pdfDocs is None:
|
||||
return False
|
||||
QDesktopServices.openUrl(QUrl(urljoin("file:", pathname2url(self.mainConf.pdfDocs))))
|
||||
return True
|
||||
if isinstance(self.mainConf.pdfDocs, Path):
|
||||
QDesktopServices.openUrl(
|
||||
QUrl(urljoin("file:", pathname2url(str(self.mainConf.pdfDocs))))
|
||||
)
|
||||
return
|
||||
|
||||
##
|
||||
# Menu Builders
|
||||
@@ -164,14 +164,14 @@ class GuiMainMenu(QMenuBar):
|
||||
|
||||
# Project > Edit
|
||||
self.aEditItem = QAction(self.tr("Rename Item"), self)
|
||||
self.aEditItem.setShortcuts(["F2"])
|
||||
self.aEditItem.setShortcut("F2")
|
||||
self.aEditItem.triggered.connect(lambda: self.mainGui.editItemLabel(None))
|
||||
self.projMenu.addAction(self.aEditItem)
|
||||
|
||||
# Project > Delete
|
||||
self.aDeleteItem = QAction(self.tr("Delete Item"), self)
|
||||
self.aDeleteItem.setShortcut("Ctrl+Shift+Del")
|
||||
self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.deleteItem(None))
|
||||
self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.requestDeleteItem(None))
|
||||
self.projMenu.addAction(self.aDeleteItem)
|
||||
|
||||
# Project > Empty Trash
|
||||
@@ -244,16 +244,6 @@ class GuiMainMenu(QMenuBar):
|
||||
self.aImportFile.triggered.connect(lambda: self.mainGui.importDocument())
|
||||
self.docuMenu.addAction(self.aImportFile)
|
||||
|
||||
# Document > Merge Documents
|
||||
self.aMergeDocs = QAction(self.tr("Merge Folder to Document"), self)
|
||||
self.aMergeDocs.triggered.connect(lambda: self.mainGui.mergeDocuments())
|
||||
self.docuMenu.addAction(self.aMergeDocs)
|
||||
|
||||
# Document > Split Document
|
||||
self.aSplitDoc = QAction(self.tr("Split Document to Folder"), self)
|
||||
self.aSplitDoc.triggered.connect(lambda: self.mainGui.splitDocument())
|
||||
self.docuMenu.addAction(self.aSplitDoc)
|
||||
|
||||
return
|
||||
|
||||
def _buildEditMenu(self):
|
||||
@@ -375,8 +365,6 @@ class GuiMainMenu(QMenuBar):
|
||||
# View > Focus Mode
|
||||
self.aFocusMode = QAction(self.tr("Focus Mode"), self)
|
||||
self.aFocusMode.setShortcut("F8")
|
||||
self.aFocusMode.setCheckable(True)
|
||||
self.aFocusMode.setChecked(self.mainGui.isFocusMode)
|
||||
self.aFocusMode.triggered.connect(lambda: self.mainGui.toggleFocusMode())
|
||||
self.viewMenu.addAction(self.aFocusMode)
|
||||
|
||||
@@ -568,6 +556,15 @@ class GuiMainMenu(QMenuBar):
|
||||
)
|
||||
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
|
||||
self.mInsBreaks = self.insertMenu.addMenu(self.tr("Page Break and Space"))
|
||||
|
||||
@@ -799,7 +796,7 @@ class GuiMainMenu(QMenuBar):
|
||||
# Tools > Check Spelling
|
||||
self.aSpellCheck = QAction(self.tr("Check Spelling"), self)
|
||||
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.setShortcut("Ctrl+F7")
|
||||
self.toolsMenu.addAction(self.aSpellCheck)
|
||||
@@ -829,7 +826,7 @@ class GuiMainMenu(QMenuBar):
|
||||
|
||||
# Tools > Backup
|
||||
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)
|
||||
|
||||
# Tools > Export Project
|
||||
@@ -886,7 +883,7 @@ class GuiMainMenu(QMenuBar):
|
||||
self.helpMenu.addAction(self.aHelpDocs)
|
||||
|
||||
# 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.setShortcut("Shift+F1")
|
||||
self.aPdfDocs.triggered.connect(self._openUserManualFile)
|
||||
|
||||
@@ -35,11 +35,11 @@ from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal
|
||||
from PyQt5.QtWidgets import (
|
||||
QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel,
|
||||
QMenu, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
||||
QWidget
|
||||
QMenu, QSizePolicy, QToolButton, QToolTip, QTreeWidget, QTreeWidgetItem,
|
||||
QVBoxLayout, QWidget
|
||||
)
|
||||
|
||||
from novelwriter.enum import nwDocMode, nwItemClass
|
||||
from novelwriter.enum import nwDocMode, nwItemClass, nwOutline
|
||||
from novelwriter.common import checkInt
|
||||
from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst
|
||||
|
||||
@@ -63,7 +63,7 @@ class GuiNovelView(QWidget):
|
||||
openDocumentRequest = pyqtSignal(str, Enum, int, str)
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
self.mainGui = mainGui
|
||||
self.theProject = mainGui.theProject
|
||||
@@ -71,6 +71,7 @@ class GuiNovelView(QWidget):
|
||||
# Build GUI
|
||||
self.novelTree = GuiNovelTree(self)
|
||||
self.novelBar = GuiNovelToolBar(self)
|
||||
self.novelBar.setEnabled(False)
|
||||
|
||||
# Assemble
|
||||
self.outerBox = QVBoxLayout()
|
||||
@@ -84,6 +85,7 @@ class GuiNovelView(QWidget):
|
||||
# Function Mappings
|
||||
self.updateWordCounts = self.novelTree.updateWordCounts
|
||||
self.getSelectedHandle = self.novelTree.getSelectedHandle
|
||||
self.setActiveHandle = self.novelTree.setActiveHandle
|
||||
|
||||
return
|
||||
|
||||
@@ -91,14 +93,24 @@ class GuiNovelView(QWidget):
|
||||
# Methods
|
||||
##
|
||||
|
||||
def updateTheme(self):
|
||||
"""Update theme elements.
|
||||
"""
|
||||
self.novelBar.updateTheme()
|
||||
self.novelTree.updateTheme()
|
||||
self.refreshTree()
|
||||
return
|
||||
|
||||
def initSettings(self):
|
||||
"""Initialise GUI elements that depend on specific settings.
|
||||
"""
|
||||
self.novelTree.initSettings()
|
||||
return
|
||||
|
||||
def refreshTree(self):
|
||||
"""Refresh the current tree.
|
||||
"""
|
||||
self.novelTree.refreshTree(rootHandle=self.theProject.lastNovel)
|
||||
self.novelTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("novelTree"))
|
||||
return
|
||||
|
||||
def clearProject(self):
|
||||
@@ -106,12 +118,13 @@ class GuiNovelView(QWidget):
|
||||
"""
|
||||
self.novelTree.clearContent()
|
||||
self.novelBar.clearContent()
|
||||
self.novelBar.setEnabled(False)
|
||||
return
|
||||
|
||||
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:
|
||||
lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL)
|
||||
|
||||
@@ -125,6 +138,7 @@ class GuiNovelView(QWidget):
|
||||
self.novelBar.buildNovelRootMenu()
|
||||
self.novelBar.setLastColType(lastCol, doRefresh=False)
|
||||
self.novelBar.setCurrentRoot(lastNovel)
|
||||
self.novelBar.setEnabled(True)
|
||||
|
||||
return
|
||||
|
||||
@@ -135,8 +149,8 @@ class GuiNovelView(QWidget):
|
||||
self.theProject.options.setValue("GuiNovelView", "lastCol", lastColType)
|
||||
return
|
||||
|
||||
def setFocus(self):
|
||||
"""Forward the set focus call to the tree widget.
|
||||
def setTreeFocus(self):
|
||||
"""Set the focus to the tree widget.
|
||||
"""
|
||||
self.novelTree.setFocus()
|
||||
return
|
||||
@@ -163,7 +177,7 @@ class GuiNovelView(QWidget):
|
||||
class GuiNovelToolBar(QWidget):
|
||||
|
||||
def __init__(self, novelView):
|
||||
QTreeWidget.__init__(self, novelView)
|
||||
super().__init__(parent=novelView)
|
||||
|
||||
logger.debug("Initialising GuiNovelToolBar ...")
|
||||
|
||||
@@ -173,21 +187,11 @@ class GuiNovelToolBar(QWidget):
|
||||
self.mainTheme = novelView.mainGui.mainTheme
|
||||
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
mPx = self.mainConf.pxInt(3)
|
||||
mPx = self.mainConf.pxInt(2)
|
||||
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
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
|
||||
self.viewLabel = QLabel("<b>%s</b>" % self.tr("Novel Outline"))
|
||||
self.viewLabel.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -196,9 +200,7 @@ class GuiNovelToolBar(QWidget):
|
||||
# Refresh Button
|
||||
self.tbRefresh = QToolButton(self)
|
||||
self.tbRefresh.setToolTip(self.tr("Refresh"))
|
||||
self.tbRefresh.setIcon(self.mainTheme.getIcon("refresh"))
|
||||
self.tbRefresh.setIconSize(QSize(iPx, iPx))
|
||||
self.tbRefresh.setStyleSheet(buttonStyle)
|
||||
self.tbRefresh.clicked.connect(self._refreshNovelTree)
|
||||
|
||||
# Novel Root Menu
|
||||
@@ -208,9 +210,7 @@ class GuiNovelToolBar(QWidget):
|
||||
|
||||
self.tbRoot = QToolButton(self)
|
||||
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.setStyleSheet(buttonStyle)
|
||||
self.tbRoot.setMenu(self.mRoot)
|
||||
self.tbRoot.setPopupMode(QToolButton.InstantPopup)
|
||||
|
||||
@@ -227,9 +227,7 @@ class GuiNovelToolBar(QWidget):
|
||||
|
||||
self.tbMore = QToolButton(self)
|
||||
self.tbMore.setToolTip(self.tr("More Options"))
|
||||
self.tbMore.setIcon(self.mainTheme.getIcon("menu"))
|
||||
self.tbMore.setIconSize(QSize(iPx, iPx))
|
||||
self.tbMore.setStyleSheet(buttonStyle)
|
||||
self.tbMore.setMenu(self.mMore)
|
||||
self.tbMore.setPopupMode(QToolButton.InstantPopup)
|
||||
|
||||
@@ -244,6 +242,8 @@ class GuiNovelToolBar(QWidget):
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
self.updateTheme()
|
||||
|
||||
logger.debug("GuiNovelToolBar initialisation complete")
|
||||
|
||||
return
|
||||
@@ -252,6 +252,31 @@ class GuiNovelToolBar(QWidget):
|
||||
# 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):
|
||||
"""Run clearing project tasks.
|
||||
"""
|
||||
@@ -297,7 +322,7 @@ class GuiNovelToolBar(QWidget):
|
||||
def _refreshNovelTree(self):
|
||||
"""Rebuild the current tree.
|
||||
"""
|
||||
rootHandle = self.theProject.lastNovel
|
||||
rootHandle = self.theProject.data.getLastHandle("novelTree")
|
||||
self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True)
|
||||
return
|
||||
|
||||
@@ -322,10 +347,15 @@ class GuiNovelTree(QTreeWidget):
|
||||
|
||||
C_TITLE = 0
|
||||
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):
|
||||
QTreeWidget.__init__(self, novelView)
|
||||
super().__init__(parent=novelView)
|
||||
|
||||
logger.debug("Initialising GuiNovelTree ...")
|
||||
|
||||
@@ -339,6 +369,7 @@ class GuiNovelTree(QTreeWidget):
|
||||
self._treeMap = {}
|
||||
self._lastBuild = 0
|
||||
self._lastCol = NovelTreeColumn.POV
|
||||
self._actHandle = None
|
||||
|
||||
# Cached Strings
|
||||
self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
|
||||
@@ -353,9 +384,11 @@ class GuiNovelTree(QTreeWidget):
|
||||
|
||||
self.setIconSize(QSize(iPx, iPx))
|
||||
self.setFrameStyle(QFrame.NoFrame)
|
||||
self.setUniformRowHeights(True)
|
||||
self.setAllColumnsShowFocus(True)
|
||||
self.setHeaderHidden(True)
|
||||
self.setIndentation(0)
|
||||
self.setColumnCount(3)
|
||||
self.setColumnCount(4)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.setExpandsOnDoubleClick(False)
|
||||
@@ -367,7 +400,8 @@ class GuiNovelTree(QTreeWidget):
|
||||
treeHeader.setMinimumSectionSize(iPx + cMg)
|
||||
treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.Stretch)
|
||||
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
|
||||
fH1 = self.font()
|
||||
@@ -378,20 +412,15 @@ class GuiNovelTree(QTreeWidget):
|
||||
fH2.setBold(True)
|
||||
|
||||
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
|
||||
self._pIndent = [
|
||||
self.mainTheme.loadDecoration("deco_doc_h0", pxH=iPx),
|
||||
self.mainTheme.loadDecoration("deco_doc_h1", pxH=iPx),
|
||||
self.mainTheme.loadDecoration("deco_doc_h2", pxH=iPx),
|
||||
self.mainTheme.loadDecoration("deco_doc_h3", pxH=iPx),
|
||||
self.mainTheme.loadDecoration("deco_doc_h4", pxH=iPx),
|
||||
]
|
||||
|
||||
# Connect signals
|
||||
self.clicked.connect(self._treeItemClicked)
|
||||
self.itemDoubleClicked.connect(self._treeDoubleClick)
|
||||
self.itemSelectionChanged.connect(self._treeSelectionChange)
|
||||
|
||||
# Set custom settings
|
||||
self.initSettings()
|
||||
self.updateTheme()
|
||||
|
||||
logger.debug("GuiNovelTree initialisation complete")
|
||||
|
||||
@@ -413,6 +442,13 @@ class GuiNovelTree(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
def updateTheme(self):
|
||||
"""Update theme elements.
|
||||
"""
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
self._pMore = self.mainTheme.loadDecoration("deco_doc_more", pxH=iPx)
|
||||
return
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
@@ -436,23 +472,23 @@ class GuiNovelTree(QTreeWidget):
|
||||
def refreshTree(self, rootHandle=None, overRide=False):
|
||||
"""Called whenever the Novel tab is activated.
|
||||
"""
|
||||
logger.verbose("Requesting refresh of the novel tree")
|
||||
logger.debug("Requesting refresh of the novel tree")
|
||||
if rootHandle is None:
|
||||
rootHandle = self.theProject.tree.findRoot(nwItemClass.NOVEL)
|
||||
|
||||
treeChanged = self.mainGui.projView.changedSince(self._lastBuild)
|
||||
indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
|
||||
if not (treeChanged or indexChanged or overRide):
|
||||
logger.verbose("No changes have been made to the novel index")
|
||||
logger.debug("No changes have been made to the novel index")
|
||||
return
|
||||
|
||||
selItem = self.selectedItems()
|
||||
titleKey = None
|
||||
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.theProject.setLastNovelViewed(rootHandle)
|
||||
self.theProject.data.setLastHandle(rootHandle, "novelTree")
|
||||
|
||||
if titleKey is not None and titleKey in self._treeMap:
|
||||
self._treeMap[titleKey].setSelected(True)
|
||||
@@ -476,8 +512,9 @@ class GuiNovelTree(QTreeWidget):
|
||||
tHandle = None
|
||||
tLine = 0
|
||||
if selItem:
|
||||
tHandle = selItem[0].data(self.C_TITLE, Qt.UserRole)[0]
|
||||
tLine = checkInt(selItem[0].data(self.C_TITLE, Qt.UserRole)[1], 1) - 1
|
||||
tHandle = selItem[0].data(self.C_TITLE, self.D_HANDLE)
|
||||
sTitle = selItem[0].data(self.C_TITLE, self.D_TITLE)
|
||||
tLine = checkInt(sTitle[1:], 1) - 1
|
||||
|
||||
return tHandle, tLine
|
||||
|
||||
@@ -487,9 +524,34 @@ class GuiNovelTree(QTreeWidget):
|
||||
if self._lastCol != colType:
|
||||
logger.debug("Changing last column to %s", colType.name)
|
||||
self._lastCol = colType
|
||||
self.setColumnHidden(self.C_LAST, colType == NovelTreeColumn.HIDDEN)
|
||||
self.setColumnHidden(self.C_EXTRA, colType == NovelTreeColumn.HIDDEN)
|
||||
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
|
||||
|
||||
##
|
||||
@@ -501,7 +563,7 @@ class GuiNovelTree(QTreeWidget):
|
||||
mouse in a blank area of the tree view, and to load a document
|
||||
for viewing if the user middle-clicked.
|
||||
"""
|
||||
QTreeWidget.mousePressEvent(self, theEvent)
|
||||
super().mousePressEvent(theEvent)
|
||||
|
||||
if theEvent.button() == Qt.LeftButton:
|
||||
selItem = self.indexAt(theEvent.pos())
|
||||
@@ -521,10 +583,28 @@ class GuiNovelTree(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
def focusOutEvent(self, theEvent):
|
||||
"""Clear the selection when the tree no longer has focus.
|
||||
"""
|
||||
super().focusOutEvent(theEvent)
|
||||
self.clearSelection()
|
||||
return
|
||||
|
||||
##
|
||||
# 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()
|
||||
def _treeSelectionChange(self):
|
||||
"""Extract the handle and line number of the currently selected
|
||||
@@ -554,7 +634,7 @@ class GuiNovelTree(QTreeWidget):
|
||||
"""
|
||||
self.clearContent()
|
||||
tStart = time()
|
||||
logger.verbose("Building novel tree for root item '%s'", rootHandle)
|
||||
logger.debug("Building novel tree for root item '%s'", rootHandle)
|
||||
|
||||
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
|
||||
for tKey, tHandle, sTitle, novIdx in novStruct:
|
||||
@@ -563,25 +643,31 @@ class GuiNovelTree(QTreeWidget):
|
||||
if iLevel == 0:
|
||||
continue
|
||||
|
||||
newItem = QTreeWidgetItem()
|
||||
theData = (tHandle, sTitle[1:].lstrip("0"), tKey)
|
||||
hDec = self.mainTheme.getHeaderDecoration(iLevel)
|
||||
|
||||
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.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.setText(self.C_WORDS, f"{novIdx.wordCount:n}")
|
||||
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
|
||||
newItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore)
|
||||
|
||||
# Custom column
|
||||
lastText, toolTip = self._getLastColumnText(tHandle, sTitle)
|
||||
newItem.setText(self.C_LAST, lastText)
|
||||
newItem.setText(self.C_EXTRA, lastText)
|
||||
if lastText:
|
||||
newItem.setToolTip(self.C_LAST, toolTip)
|
||||
newItem.setToolTip(self.C_EXTRA, toolTip)
|
||||
|
||||
self._treeMap[tKey] = 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()
|
||||
|
||||
return
|
||||
@@ -607,4 +693,49 @@ class GuiNovelTree(QTreeWidget):
|
||||
|
||||
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
|
||||
|
||||
@@ -37,16 +37,16 @@ from PyQt5.QtCore import (
|
||||
Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP
|
||||
)
|
||||
from PyQt5.QtWidgets import (
|
||||
QAbstractItemView, QAction, QGridLayout, QGroupBox, QHBoxLayout, QLabel,
|
||||
QMenu, QScrollArea, QSplitter, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
||||
QWidget, QFrame, QToolBar, QSizePolicy, QComboBox, QToolButton
|
||||
QAbstractItemView, QAction, QComboBox, QFrame, QGridLayout, QGroupBox,
|
||||
QHBoxLayout, QLabel, QMenu, QScrollArea, QSizePolicy, QSplitter, QToolBar,
|
||||
QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
|
||||
)
|
||||
|
||||
from novelwriter.enum import (
|
||||
nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline
|
||||
)
|
||||
from novelwriter.common import checkInt
|
||||
from novelwriter.constants import trConst, nwKeyWords, nwLabels
|
||||
from novelwriter.constants import nwHeaders, trConst, nwKeyWords, nwLabels
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -57,20 +57,22 @@ class GuiOutlineView(QWidget):
|
||||
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
||||
|
||||
def __init__(self, mainGui):
|
||||
QWidget.__init__(self, mainGui)
|
||||
super().__init__(parent=mainGui)
|
||||
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.mainConf = novelwriter.CONFIG
|
||||
self.mainGui = mainGui
|
||||
self.theProject = mainGui.theProject
|
||||
|
||||
# Build GUI
|
||||
self.outlineBar = GuiOutlineToolBar(self)
|
||||
self.outlineTree = GuiOutlineTree(self)
|
||||
self.outlineData = GuiOutlineDetails(self)
|
||||
self.outlineBar = GuiOutlineToolBar(self)
|
||||
self.outlineBar.setEnabled(False)
|
||||
|
||||
self.splitOutline = QSplitter(Qt.Vertical)
|
||||
self.splitOutline.addWidget(self.outlineTree)
|
||||
self.splitOutline.addWidget(self.outlineData)
|
||||
self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
|
||||
self.splitOutline.setSizes(self.mainConf.outlinePanePos)
|
||||
|
||||
# Assemble
|
||||
self.outerBox = QVBoxLayout()
|
||||
@@ -96,33 +98,67 @@ class GuiOutlineView(QWidget):
|
||||
# Methods
|
||||
##
|
||||
|
||||
def splitSizes(self):
|
||||
return self.splitOutline.sizes()
|
||||
def updateTheme(self):
|
||||
"""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.outlineBar.setEnabled(False)
|
||||
return
|
||||
|
||||
def initOutline(self):
|
||||
self.outlineTree.initOutline()
|
||||
self.outlineData.initDetails()
|
||||
def openProjectTasks(self):
|
||||
"""Run open project tasks.
|
||||
"""
|
||||
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
|
||||
|
||||
def closeOutline(self):
|
||||
self.outlineTree.closeOutline()
|
||||
def closeProjectTasks(self):
|
||||
self.outlineTree.closeProjectTasks()
|
||||
self.outlineData.updateClasses()
|
||||
return
|
||||
|
||||
def refreshView(self, overRide=False, novelChanged=False):
|
||||
self.outlineTree.refreshTree(overRide=overRide, novelChanged=novelChanged)
|
||||
return
|
||||
|
||||
def treeHasFocus(self):
|
||||
return self.outlineTree.hasFocus()
|
||||
def splitSizes(self):
|
||||
return self.splitOutline.sizes()
|
||||
|
||||
def setTreeFocus(self):
|
||||
"""Set the focus to the tree widget.
|
||||
"""
|
||||
return self.outlineTree.setFocus()
|
||||
|
||||
def treeHasFocus(self):
|
||||
"""Check if the outline tree has focus.
|
||||
"""
|
||||
return self.outlineTree.hasFocus()
|
||||
|
||||
##
|
||||
# Public Slots
|
||||
##
|
||||
@@ -172,7 +208,7 @@ class GuiOutlineToolBar(QToolBar):
|
||||
viewColumnToggled = pyqtSignal(bool, Enum)
|
||||
|
||||
def __init__(self, theOutline):
|
||||
QTreeWidget.__init__(self, theOutline)
|
||||
super().__init__(parent=theOutline)
|
||||
|
||||
logger.debug("Initialising GuiOutlineToolBar ...")
|
||||
|
||||
@@ -187,7 +223,6 @@ class GuiOutlineToolBar(QToolBar):
|
||||
self.setMovable(False)
|
||||
self.setIconSize(QSize(iPx, iPx))
|
||||
self.setContentsMargins(0, 0, 0, 0)
|
||||
self.setStyleSheet("QToolBar {border: 0px;}")
|
||||
|
||||
stretch = QWidget(self)
|
||||
stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
@@ -202,7 +237,6 @@ class GuiOutlineToolBar(QToolBar):
|
||||
|
||||
# Actions
|
||||
self.aRefresh = QAction(self.tr("Refresh"), self)
|
||||
self.aRefresh.setIcon(self.mainTheme.getIcon("refresh"))
|
||||
self.aRefresh.triggered.connect(self._refreshRequested)
|
||||
|
||||
# Column Menu
|
||||
@@ -212,7 +246,6 @@ class GuiOutlineToolBar(QToolBar):
|
||||
)
|
||||
|
||||
self.tbColumns = QToolButton(self)
|
||||
self.tbColumns.setIcon(self.mainTheme.getIcon("menu"))
|
||||
self.tbColumns.setMenu(self.mColumns)
|
||||
self.tbColumns.setPopupMode(QToolButton.InstantPopup)
|
||||
|
||||
@@ -224,6 +257,8 @@ class GuiOutlineToolBar(QToolBar):
|
||||
self.addWidget(self.tbColumns)
|
||||
self.addWidget(stretch)
|
||||
|
||||
self.updateTheme()
|
||||
|
||||
logger.debug("GuiOutlineToolBar initialisation complete")
|
||||
|
||||
return
|
||||
@@ -232,6 +267,16 @@ class GuiOutlineToolBar(QToolBar):
|
||||
# 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):
|
||||
"""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"), "")
|
||||
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):
|
||||
"""Forward the change of column hidden states to the menu.
|
||||
"""
|
||||
@@ -313,11 +369,14 @@ class GuiOutlineTree(QTreeWidget):
|
||||
nwOutline.SYNOP: False,
|
||||
}
|
||||
|
||||
D_HANDLE = Qt.UserRole
|
||||
D_TITLE = Qt.UserRole + 1
|
||||
|
||||
hiddenStateChanged = pyqtSignal()
|
||||
activeItemChanged = pyqtSignal(str, str)
|
||||
|
||||
def __init__(self, theOutline):
|
||||
QTreeWidget.__init__(self, theOutline)
|
||||
super().__init__(parent=theOutline)
|
||||
|
||||
logger.debug("Initialising GuiOutlineTree ...")
|
||||
|
||||
@@ -326,6 +385,7 @@ class GuiOutlineTree(QTreeWidget):
|
||||
self.theProject = theOutline.mainGui.theProject
|
||||
self.mainTheme = theOutline.mainGui.mainTheme
|
||||
|
||||
self.setUniformRowHeights(True)
|
||||
self.setFrameStyle(QFrame.NoFrame)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
@@ -336,11 +396,28 @@ class GuiOutlineTree(QTreeWidget):
|
||||
|
||||
iPx = self.mainTheme.baseIconSize
|
||||
self.setIconSize(QSize(iPx, iPx))
|
||||
self.setIndentation(iPx)
|
||||
self.setIndentation(0)
|
||||
|
||||
self.treeHead = self.header()
|
||||
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
|
||||
self._treeOrder = []
|
||||
self._colWidth = {}
|
||||
@@ -350,8 +427,8 @@ class GuiOutlineTree(QTreeWidget):
|
||||
self._firstView = True
|
||||
self._lastBuild = 0
|
||||
|
||||
self.initOutline()
|
||||
self.clearOutline()
|
||||
self.initSettings()
|
||||
self.clearContent()
|
||||
|
||||
self.hiddenStateChanged.emit()
|
||||
|
||||
@@ -371,7 +448,7 @@ class GuiOutlineTree(QTreeWidget):
|
||||
# Methods
|
||||
##
|
||||
|
||||
def initOutline(self):
|
||||
def initSettings(self):
|
||||
"""Set or update outline settings.
|
||||
"""
|
||||
# Scroll bars
|
||||
@@ -387,7 +464,7 @@ class GuiOutlineTree(QTreeWidget):
|
||||
|
||||
return
|
||||
|
||||
def clearOutline(self):
|
||||
def clearContent(self):
|
||||
"""Clear the tree and header and set the default values for the
|
||||
columns arrays.
|
||||
"""
|
||||
@@ -425,18 +502,20 @@ class GuiOutlineTree(QTreeWidget):
|
||||
# If the novel index or novel tree has changed since the tree
|
||||
# was last built, we rebuild the tree from the updated index.
|
||||
indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
|
||||
doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline
|
||||
if doBuild or overRide:
|
||||
logger.debug("Rebuilding Project Outline")
|
||||
self._populateTree(rootHandle)
|
||||
if not (novelChanged or indexChanged or overRide):
|
||||
logger.debug("No changes have been made to the novel index")
|
||||
return
|
||||
|
||||
self._populateTree(rootHandle)
|
||||
self.theProject.data.setLastHandle(rootHandle or None, "outline")
|
||||
|
||||
return
|
||||
|
||||
def closeOutline(self):
|
||||
def closeProjectTasks(self):
|
||||
"""Called before a project is closed.
|
||||
"""
|
||||
self._saveHeaderState()
|
||||
self.clearOutline()
|
||||
self.clearContent()
|
||||
self._firstView = True
|
||||
return
|
||||
|
||||
@@ -448,7 +527,7 @@ class GuiOutlineTree(QTreeWidget):
|
||||
tHandle = None
|
||||
tLine = 0
|
||||
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
|
||||
|
||||
return tHandle, tLine
|
||||
@@ -474,8 +553,8 @@ class GuiOutlineTree(QTreeWidget):
|
||||
"""
|
||||
selItems = self.selectedItems()
|
||||
if selItems:
|
||||
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
|
||||
sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole)
|
||||
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
|
||||
sTitle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
|
||||
self.activeItemChanged.emit(tHandle, sTitle)
|
||||
|
||||
return
|
||||
@@ -494,11 +573,9 @@ class GuiOutlineTree(QTreeWidget):
|
||||
"""Receive the changes to column visibility forwarded by the
|
||||
column selection menu.
|
||||
"""
|
||||
logger.verbose("User toggled Outline column '%s'", theItem.name)
|
||||
if theItem in self._colIdx:
|
||||
self.setColumnHidden(self._colIdx[theItem], not isChecked)
|
||||
self._saveHeaderState()
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -613,110 +690,60 @@ class GuiOutlineTree(QTreeWidget):
|
||||
self.setColumnWidth(self._colIdx[hItem], self._colWidth[hItem])
|
||||
self.setColumnHidden(self._colIdx[hItem], self._colHidden[hItem])
|
||||
|
||||
# Make sure title column is always visible,
|
||||
# and handle column always hidden
|
||||
# Make sure title column is always visible
|
||||
self.setColumnHidden(self._colIdx[nwOutline.TITLE], False)
|
||||
|
||||
headItem = self.headerItem()
|
||||
headItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
|
||||
headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
|
||||
headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
|
||||
|
||||
currTitle = None
|
||||
currChapter = None
|
||||
currScene = None
|
||||
if headItem is not None:
|
||||
headItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
|
||||
headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
|
||||
headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
|
||||
|
||||
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
|
||||
for _, tHandle, sTitle, novIdx in novStruct:
|
||||
|
||||
tItem = self._createTreeItem(tHandle, sTitle, novIdx)
|
||||
iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0)
|
||||
if iLevel == 0:
|
||||
continue
|
||||
|
||||
tLevel = novIdx.level
|
||||
if tLevel == "H1":
|
||||
self.addTopLevelItem(tItem)
|
||||
currTitle = tItem
|
||||
currChapter = None
|
||||
currScene = None
|
||||
trItem = QTreeWidgetItem()
|
||||
nwItem = self.theProject.tree[tHandle]
|
||||
hDec = self.mainTheme.getHeaderDecoration(iLevel)
|
||||
|
||||
elif tLevel == "H2":
|
||||
if currTitle is None:
|
||||
self.addTopLevelItem(tItem)
|
||||
else:
|
||||
currTitle.addChild(tItem)
|
||||
currChapter = tItem
|
||||
currScene = None
|
||||
trItem.setData(self._colIdx[nwOutline.TITLE], Qt.DecorationRole, hDec)
|
||||
trItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title)
|
||||
trItem.setData(self._colIdx[nwOutline.TITLE], self.D_HANDLE, tHandle)
|
||||
trItem.setData(self._colIdx[nwOutline.TITLE], self.D_TITLE, sTitle)
|
||||
trItem.setFont(self._colIdx[nwOutline.TITLE], self._hFonts[iLevel])
|
||||
trItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level)
|
||||
trItem.setIcon(self._colIdx[nwOutline.LABEL], self._dIcon[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":
|
||||
if currChapter is None:
|
||||
if currTitle is None:
|
||||
self.addTopLevelItem(tItem)
|
||||
else:
|
||||
currTitle.addChild(tItem)
|
||||
else:
|
||||
currChapter.addChild(tItem)
|
||||
currScene = tItem
|
||||
refs = self.theProject.index.getReferences(tHandle, sTitle)
|
||||
trItem.setText(self._colIdx[nwOutline.POV], ", ".join(refs[nwKeyWords.POV_KEY]))
|
||||
trItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(refs[nwKeyWords.FOCUS_KEY]))
|
||||
trItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(refs[nwKeyWords.CHAR_KEY]))
|
||||
trItem.setText(self._colIdx[nwOutline.PLOT], ", ".join(refs[nwKeyWords.PLOT_KEY]))
|
||||
trItem.setText(self._colIdx[nwOutline.TIME], ", ".join(refs[nwKeyWords.TIME_KEY]))
|
||||
trItem.setText(self._colIdx[nwOutline.WORLD], ", ".join(refs[nwKeyWords.WORLD_KEY]))
|
||||
trItem.setText(self._colIdx[nwOutline.OBJECT], ", ".join(refs[nwKeyWords.OBJECT_KEY]))
|
||||
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":
|
||||
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.addTopLevelItem(trItem)
|
||||
|
||||
self._lastBuild = time()
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -725,7 +752,7 @@ class GuiOutlineHeaderMenu(QMenu):
|
||||
columnToggled = pyqtSignal(bool, Enum)
|
||||
|
||||
def __init__(self, theOutline):
|
||||
QMenu.__init__(self, theOutline)
|
||||
super().__init__(parent=theOutline)
|
||||
|
||||
self.acceptToggle = True
|
||||
|
||||
@@ -776,7 +803,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
itemTagClicked = pyqtSignal(str)
|
||||
|
||||
def __init__(self, theOutline):
|
||||
QScrollArea.__init__(self, theOutline)
|
||||
super().__init__(parent=theOutline)
|
||||
|
||||
logger.debug("Initialising GuiOutlineDetails ...")
|
||||
|
||||
@@ -963,13 +990,13 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.setWidgetResizable(True)
|
||||
self.setFrameStyle(QFrame.NoFrame)
|
||||
|
||||
self.initDetails()
|
||||
self.initSettings()
|
||||
|
||||
logger.debug("GuiOutlineDetails initialisation complete")
|
||||
|
||||
return
|
||||
|
||||
def initDetails(self):
|
||||
def initSettings(self):
|
||||
"""Set or update outline settings.
|
||||
"""
|
||||
# Scroll bars
|
||||
@@ -1032,7 +1059,7 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
|
||||
self.titleValue.setText(novIdx.title)
|
||||
|
||||
itemStatus, _ = nwItem.getImportStatus()
|
||||
itemStatus, _ = nwItem.getImportStatus(incIcon=False)
|
||||
|
||||
self.fileValue.setText(nwItem.itemName)
|
||||
self.itemValue.setText(itemStatus)
|
||||
|
||||