Merge branch 'main' into i18n-de_DE-created
@@ -14,7 +14,7 @@ jobs:
|
|||||||
testLinux:
|
testLinux:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.7", "3.8", "3.9", "3.10"]
|
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Python Setup
|
- name: Python Setup
|
||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
- name: Install Packages (apt)
|
- name: Install Packages (apt)
|
||||||
run: |
|
run: |
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install libenchant-dev qttools5-dev-tools
|
sudo apt install libenchant-dev qttools5-dev-tools aspell-en
|
||||||
- name: Checkout Source
|
- name: Checkout Source
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Install Dependencies (pip)
|
- name: Install Dependencies (pip)
|
||||||
|
|||||||
@@ -1,7 +1,237 @@
|
|||||||
# novelWriter Changelog
|
# novelWriter Changelog
|
||||||
|
|
||||||
|
## Version 2.0 RC 2 [2022-11-13]
|
||||||
|
|
||||||
|
### Release Notes
|
||||||
|
|
||||||
|
This is a release candidate of the next release version, and is intended for testing purposes.
|
||||||
|
Please be careful when using this version on live writing projects, and make sure you take frequent
|
||||||
|
backups.
|
||||||
|
|
||||||
|
Please check the changelog for an overview of changes. The full release notes will be added to the
|
||||||
|
final release.
|
||||||
|
|
||||||
|
### Detailed Changelog
|
||||||
|
|
||||||
|
Note: This release introduces a new Project XML format with version number 1.5. When the project is
|
||||||
|
opened, a request to update the file format will show up.
|
||||||
|
|
||||||
|
**Bugfixes**
|
||||||
|
|
||||||
|
* The custom folders for user defined themes and syntax were not created properly when the app was
|
||||||
|
first launched on a new computer. The folders were successfully created on a second launch. The
|
||||||
|
error was handled, but reported. This was caused by the folder creation process being in the
|
||||||
|
wrong order. Issue #1180. PR #1184.
|
||||||
|
* Fixed context menu entries for split and merge having inconsistent labels. Issue #1199. PR #1197.
|
||||||
|
|
||||||
|
**User Interface**
|
||||||
|
|
||||||
|
* The exported status for document items have been renamed to active/inactive. Their icons have
|
||||||
|
also been updated. Issues #1196 and #1198. PRs #1200 and #1216.
|
||||||
|
* The status/importance context menu now shows which label is the current. Issue #1202. PR #1207.
|
||||||
|
* The status/importance context menu now has a "Manage Labels" action that opens the Project
|
||||||
|
Settings dialog at the correct place. Issue #1203. PR #1207.
|
||||||
|
* Both GUI theme and syntax theme can now be updated without restarting the app. Issue #1171.
|
||||||
|
PR #1212.
|
||||||
|
* The GUI theme now determines which icon theme is to be loaded. It is no longer a separate
|
||||||
|
setting. The icon theme can also be reloaded without restart. Issue #1172. PR #1212.
|
||||||
|
* The block formating features in the Format menu now also works on empty lines. Issue #1178.
|
||||||
|
PR #1214.
|
||||||
|
* There is now a Format menu entry and shortcut code for synopsis comments. Issue #1177. PR #1214.
|
||||||
|
* The split document dialog now has the option to move teh source document to trash. Issue #1179.
|
||||||
|
PR #1217.
|
||||||
|
|
||||||
|
**Other Changes**
|
||||||
|
|
||||||
|
* Archived documents are now partially indexed. This mainly means that the item will have the
|
||||||
|
correct document icon in the project tree corresponding to its main heading. Issue #1176.
|
||||||
|
PR #1183.
|
||||||
|
* The option to add notes files in the Project Wizard now automatically switches off if there are
|
||||||
|
no notes categories enabled. Issue #1192. PR #1201.
|
||||||
|
* When a project is opened for the first time, the first document in the project is also opened.
|
||||||
|
Issue #1219. PR #1223.
|
||||||
|
* When there is no project open, the toolbars on the Project Tree, Novel View and Outline View are
|
||||||
|
disabled. They are enabled only when a project is loaded. Issue #1220. PR #1230.
|
||||||
|
|
||||||
|
**Installation and Packaging**
|
||||||
|
|
||||||
|
* The AppImage release now has version information in the package name. Issue #1182. PR #1218.
|
||||||
|
|
||||||
|
**Code Improvements**
|
||||||
|
|
||||||
|
* The main heading of a document is now stored in the item class instead of the index. PR #1183.
|
||||||
|
* The common module checker functions no longer allow None values. The only one needing it was the
|
||||||
|
string checker. A new string checker that allows None has been added for those cases. This makes
|
||||||
|
type discovery in the code editor easier. Issue #1185. PR #1188.
|
||||||
|
* Verbose logging has been removed. The lowest severity level is now DEBUG. Issue #1186. PR #1191.
|
||||||
|
* Added a number of None checks in the code where especially Qt calls could potentially return
|
||||||
|
None, even if they were unlikely to do so. PR #1197.
|
||||||
|
* Renamed the status bar attribute in the main GUI class as it conflicts with a Qt method.
|
||||||
|
Issue #1190. PR #1197.
|
||||||
|
* The data access methods for the custom config file parser have been improved to better report
|
||||||
|
correct type information. PR #1197.
|
||||||
|
* Saving and loading of XML data is now handled by a separate set of reader and writer classes. The
|
||||||
|
reader class is capable of reading all file formats that have been used thus far. The various
|
||||||
|
data classes have been improved, and a new XML file formart version 1.5 added. Issue #1189.
|
||||||
|
PRs #1221 and #1232.
|
||||||
|
* The index is now automatically rebuilt when the project file format is updated. Issue #1235.
|
||||||
|
PR #1236.
|
||||||
|
* The project folder on disk is now wrapped in a storage class that the project accesses files
|
||||||
|
through. It also handles lock files and archiving used for backup. The change is in preparation
|
||||||
|
for adding a potential single file format. Issue #1222. PR #1225.
|
||||||
|
* The Project Wizard now creates the project on disk, and then opens it. This replaces the old
|
||||||
|
method where the new project was built directly into the current session. This caused a few
|
||||||
|
inconsistencies from time to time, and was a duplicate way of getting a project into the session.
|
||||||
|
Issue #1152. PR #1225.
|
||||||
|
* The Config class has been refactored extensively and now also uses pathlib for all paths. Tests
|
||||||
|
are also switched to using pathlib. Issue #1224. PRs #1228 and #1229.
|
||||||
|
* The updating of tree order method of the project tree class has been updated for better
|
||||||
|
performance. PR #1236.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## Version 2.0 RC 1 [2022-10-17]
|
||||||
|
|
||||||
|
### Release Notes
|
||||||
|
|
||||||
|
This is a release candidate of the next release version, and is intended for testing purposes.
|
||||||
|
Please be careful when using this version on live writing projects, and make sure you take frequent
|
||||||
|
backups.
|
||||||
|
|
||||||
|
Please check the changelog for an overview of changes. The full release notes will be added to the
|
||||||
|
final release.
|
||||||
|
|
||||||
|
**Note:** As of the 2.0 release, novelWriter requires Qt 5.10 or higher and Python 3.7 or higher.
|
||||||
|
|
||||||
|
### Detailed Changelog
|
||||||
|
|
||||||
|
**Note:** This will no longer be release 1.7, but 2.0 instead due to the major changes to the User
|
||||||
|
Interface. PR #1146.
|
||||||
|
|
||||||
|
**Features**
|
||||||
|
|
||||||
|
* The add new documents feature has been made a little smarter and now tries to choose whether the
|
||||||
|
added document should be a sibling or a child to the selected document. Issue #1107. PR #1110.
|
||||||
|
* A folder in the Project Tree can now be converted to a Novel Document or Project Note from the
|
||||||
|
item's context menu. This makes sense now that documents can have child items. Issue #1071.
|
||||||
|
PR #1128.
|
||||||
|
* All child elements of an item in the Project Tree can now be collectively expanded or collapsed
|
||||||
|
from the context menu. The same action can be triggered on the entire tree from the menu button
|
||||||
|
at the top. Issue #1122. PR #1129.
|
||||||
|
* If using the auto-insert feature for spaces next to punctuation designed for for instance French,
|
||||||
|
any existing spaces are first stripped before the correct space character is added. Some users
|
||||||
|
find it hard to unlearn the reflex to type the space. Issue #1061. PR #1131.
|
||||||
|
* The Project Tree now has Quick Links for navigating directly between root folders. This is
|
||||||
|
convenient for very large projects. Issue #1137. PR #1165.
|
||||||
|
* It is now possible to filter out entire root folders on the Build Tool. Issue #1138. PR #1168.
|
||||||
|
|
||||||
|
**Bugfixes**
|
||||||
|
|
||||||
|
* Fix a typo in the Lorem Ipsum tool and set a max width for the views bar. PR #1065.
|
||||||
|
* The language set in the Build Tool is now properly exported to Open Document files. Previously,
|
||||||
|
it would be set to English regardless of the user's selection. Issue #1073. PR #1077.
|
||||||
|
* Since the text editor cursor extends to the right of its position in the window, it would
|
||||||
|
disappear under the right-hand margin if it reached the edge. A minimum margin of the width of
|
||||||
|
the cursor has been added to the editor, and the same value subtracted from the margin of the
|
||||||
|
viewbox. Thus, the cursor no longer disappears. Issue #1112. PR #1113.
|
||||||
|
* The `Shift+Enter` key combination no longer inserts a Unicode line separator in the text editor.
|
||||||
|
Issue #1150. PR #1151.
|
||||||
|
* Fixed an issue where idle time would not be properly reset when a new project was created.
|
||||||
|
Issue #1149. PR #1153.
|
||||||
|
* Removed the context menu that would appear on the Views Bar, from which the bar could be hidden.
|
||||||
|
This is some feature imposed by the Qt library, and it is not wanted. Issue #1147. PR #1153.
|
||||||
|
|
||||||
|
**Known Issues**
|
||||||
|
|
||||||
|
* An attempt has been made to fix the missing mime icon for novelWriter projects on Linux. The
|
||||||
|
newer versions of Nautilus don't seem to display the correct icon due to some changes in the way
|
||||||
|
icons are extracted from the theme. The icon displays fine in other places. PR #1068.
|
||||||
|
|
||||||
|
**Internationalisation**
|
||||||
|
|
||||||
|
* Norwegian and US English translations have been updated. PR #1170.
|
||||||
|
|
||||||
|
**User Interface**
|
||||||
|
|
||||||
|
* The New Project Wizard has been updated based on user feedback. The "Working Title" setting is
|
||||||
|
now called "Project Name". The number of root folders one can create on the options page has been
|
||||||
|
reduced to the standard ones. An option to add sample notes has been added, and the option to
|
||||||
|
generate chapter folders has been removed. The "Minimal Project" option also no longer creates
|
||||||
|
folders. An Archive root folder is automatically added to all new projects, and a Trash folder is
|
||||||
|
added to custom projects. PR #1067.
|
||||||
|
* The border around a lot of Widgets on the main GUI have been removed. PR #1069.
|
||||||
|
* The Views Bar has been updated since the beta re;ease with better icons, tooltips, and an
|
||||||
|
expanding menu on the Settings button. The Build Tool can now also be accessed from the Views
|
||||||
|
Bar. PR #1069.
|
||||||
|
* The editor theme setting in Preferences has been moved back to the General tab where users seem
|
||||||
|
to expect to find it. PR #1069.
|
||||||
|
* A toolbar has been added to the Outline View where the user can select which Novel folder to
|
||||||
|
view. A refresh button for a forced reload of the selected Novel folder has been added, and the
|
||||||
|
menu to select which columns to show has been added to a new button. This makes it easier to find
|
||||||
|
for users instead of having to right-click the table header. Issue #1105. PRs #1063, #1094, and
|
||||||
|
#1111.
|
||||||
|
* The Rebuild Outline and Auto-Update Outline options have been removed from the Main Menu.
|
||||||
|
PR #1063.
|
||||||
|
* The Project Tree has been redesigned. The header is now hidden, and the columns resize
|
||||||
|
automatically. A toolbar has been added with buttons for moving items up and down in the tree, a
|
||||||
|
button for adding new files, folders and root folders, and a menu button for further options
|
||||||
|
affecting the whole tree. These features have mostly bee removed from the Main Menu. PR #1079.
|
||||||
|
* The context menu of the Project Tree has been rewritten completely. It is now possible to set
|
||||||
|
importance and status directly from this menu. Part of issue #973. PRs #1079 and #1105.
|
||||||
|
* The Edit Item dialog has been replaced with a simple Edit Label dialog. Most of the features
|
||||||
|
handled by the old dialog have been moved to the Project Tree context menu. PR #1082.
|
||||||
|
* The Novel View has been redesigned to match the new Outline View and Project Tree. The header is
|
||||||
|
now hidden, and the columns auto-size. The third column can be selected from a set of options, or
|
||||||
|
be hidden entirely. A button with a menu can be used to select which Novel folder to show.
|
||||||
|
Issue #1041. PR #1084.
|
||||||
|
* An arrow icon has been added behind each item in the Novel View. Clicking it, will pop up a
|
||||||
|
tooltip showing the collected meta data for the heading the item represents. PR #1088.
|
||||||
|
* The Project Details dialog now supports multiple novel folders. Issue #1078. PRs #1130 and #1167.
|
||||||
|
* The Split and Merge tools have been rewritten to work with the new feature of allowing documents
|
||||||
|
to have child documents. The tools have also had more options added to them. The feature is now
|
||||||
|
accessible through the Project Tree item's context menu, and is no longer available from the Main
|
||||||
|
Menu. Issues #1072 and #1032. PRs #1148 and #1154.
|
||||||
|
|
||||||
|
**Installation and Packaging**
|
||||||
|
|
||||||
|
* AppImage distribution has been added by @Ryex. Issue #1091. PR #1092.
|
||||||
|
|
||||||
|
**Code Improvements**
|
||||||
|
|
||||||
|
* A new test project generator function has been added to replace the dependency on the New Project
|
||||||
|
tools in the app itself. This ensures that changing the in-app feature doesn't affect the entire
|
||||||
|
test suite. Over time, this generator function should also replace the minimal sample project
|
||||||
|
saved in the test suite. PR #1067.
|
||||||
|
* The index class has been rewritten. The index data is now stored in a hierarchy of objects rather
|
||||||
|
than a set of nested dictionaries. The `itemIndex` holds all the data collected from the project,
|
||||||
|
and the `tagsIndex` is a reverse lookup index for linking tags back to where they are used. The
|
||||||
|
reading/writing of the index to disk between sessions is now handled by pack/unpack functions in
|
||||||
|
each object. The index will be regenerated when the user first opens a project as it has been
|
||||||
|
completely restructured. PR #1074.
|
||||||
|
* The index instance is now an instance of the project class, not the main GUI. PR #1074.
|
||||||
|
* The Outline View has been restructured into a single parent widget in the same manner as the
|
||||||
|
Novel View was in the previous pre-release. Related to #1041. PR #1063.
|
||||||
|
* A lot of main GUI objects have been given new names in the code to better represent what they do.
|
||||||
|
PR #1081.
|
||||||
|
* The converter for the old 1.0 project structure has been simplified. Mostly to reduce the amount
|
||||||
|
of code and number of translation labels needed for it. It now produces a single error if
|
||||||
|
something went wrong. PR #1083.
|
||||||
|
* A number of unused icons have been removed from the code base. PR #1164.
|
||||||
|
* All checks for Qt versions below 5.10 have been removed. PR #1174.
|
||||||
|
|
||||||
|
**Not Implemented**
|
||||||
|
|
||||||
|
* An idea to make the indexer run in the global thread pool was not implemented. Issue #1076.
|
||||||
|
* Feature #997 is now obsolete due to the changes made to the Edit Item dialog in #1082.
|
||||||
|
* Feature #1106 is not implemented. It proposes to hide the option to select Novel folder if there
|
||||||
|
is only one. We will see if this is really needed based on user feedback.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
## Version 1.7 Beta 1 [2022-05-17]
|
## Version 1.7 Beta 1 [2022-05-17]
|
||||||
|
|
||||||
|
**Note:** The 1.7 release cycle was renamed 2.0 on 2022-10-06. See #1144.
|
||||||
|
|
||||||
### Release Notes
|
### Release Notes
|
||||||
|
|
||||||
This is a beta release of the next release version, and is intended for testing purposes. Please be
|
This is a beta release of the next release version, and is intended for testing purposes. Please be
|
||||||
@@ -89,6 +319,105 @@ final release.
|
|||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
|
## Version 1.6.6 [2022-10-25]
|
||||||
|
|
||||||
|
### Release Notes
|
||||||
|
|
||||||
|
This is a bugfix release that fixes a minor issues with following tags in the editor. It is now
|
||||||
|
possible to also follow tags that contain spaces.
|
||||||
|
|
||||||
|
### Detailed Changelog
|
||||||
|
|
||||||
|
**Bugfixes**
|
||||||
|
|
||||||
|
* Fix a bug where only the word under the cursor would be looked up when the user tried to follow a
|
||||||
|
tag in the editor. The lookup function now uses the same parser for the `@`-line as the syntax
|
||||||
|
highlighter does, so they should behave consistently. Issue #1195. PR #1209.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## Version 1.6.5 [2022-10-13]
|
||||||
|
|
||||||
|
### Release Notes
|
||||||
|
|
||||||
|
This is a bugfix release that fixes a few minor issues. The idle time for new projects would be
|
||||||
|
artificially inflated as the clock was not reset when the project was first created. This only
|
||||||
|
affects the first entry in the writing statistics. A scaling issue for the Preferences dialog has
|
||||||
|
also been fixed. It only affected screens with UI scaling enabled. Lastly, typing `Shift+Enter` in
|
||||||
|
the text editor now creates a regular line break instead of a special line separator. The line
|
||||||
|
separator serves no purpose in plain text, and was producing inconsistencies in how text is
|
||||||
|
processed and displayed.
|
||||||
|
|
||||||
|
### Detailed Changelog
|
||||||
|
|
||||||
|
**Bugfixes**
|
||||||
|
|
||||||
|
* Fixed a bug where the idle time was not properly zeroed when a new project was generated after
|
||||||
|
the wizard was closed. The idle time would be calculated from the time the previous project
|
||||||
|
closed, thus inflating the value. Issue #1149. PR #1159.
|
||||||
|
* Fixes an issue where the window size of the Preferences dialog would have the GUI scaling factor
|
||||||
|
applied twice when the dialog was closed, resulting in the dialog growing in size each time it is
|
||||||
|
opened. Issue #989. PR #1159.
|
||||||
|
|
||||||
|
**Other Changes**
|
||||||
|
|
||||||
|
* The text editor no longer creates a Unicode line separator (U+2028) when the user presses
|
||||||
|
`Shift+Enter`. The line separator serves no purpose in a plain text editor, and the code in
|
||||||
|
general treats them as regular line break. This caused the line separator to display differently
|
||||||
|
before and after saving. The line separator character is now automatically replaced by a
|
||||||
|
paragraph separator. Issue #1150. PR #1159.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## Version 1.6.4 [2022-09-29]
|
||||||
|
|
||||||
|
### Release Notes
|
||||||
|
|
||||||
|
This is a bugfix release that fixes a critical bug in the insert non-breaking spaces feature. It
|
||||||
|
basically no longer worked in the 1.6.3 release. This release also fixes a minor issue where the
|
||||||
|
text cursor sometimes disappears when reaching the right-hand edge of the text editor window.
|
||||||
|
|
||||||
|
### Detailed Changelog
|
||||||
|
|
||||||
|
**Bugfixes**
|
||||||
|
|
||||||
|
* Fixed a bug in the auto-replace feature of the editor that caused a crash when using the insert
|
||||||
|
non-breaking spaces feature was used. Issue #1118. PR #1120.
|
||||||
|
* Back ported a bugfix from 1.7 RC1 that resolves an issue with the text cursor sometimes
|
||||||
|
disappearing at the right-hand edge of the text editor. Issues #1112 and #1119. PR #1120.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
## Version 1.6.3 [2022-08-18]
|
||||||
|
|
||||||
|
### Release Notes
|
||||||
|
|
||||||
|
This is a bugfix release that fixes a rare problem causing novelWriter to crash if the spell
|
||||||
|
checker language setting was configured to an empty value.
|
||||||
|
|
||||||
|
A few other minor issues have also been fixed: The project language setting is now properly
|
||||||
|
exported to ODT documents. Spaces are no longer inserted automatically in front of colons in
|
||||||
|
certain meta data settings when the feature is enabled (it is primarily used for French). Lastly,
|
||||||
|
the slider splitting the editor and viewer panels can no longer be dragged until the viewer
|
||||||
|
disappears. It was not necessarily obvious how the viewer panel could be restored in such cases.
|
||||||
|
|
||||||
|
### Detailed Changelog
|
||||||
|
|
||||||
|
**Bugfixes**
|
||||||
|
|
||||||
|
* Fixed an issue where the project language setting was not exported when building Open Document
|
||||||
|
files. Issue #1073. PR #1087.
|
||||||
|
* Fixed an issue where the splitter in the main window could be dragged until it hid the document
|
||||||
|
viewer panel. This is no longer possible. Issue #1085. PR #1087.
|
||||||
|
* Fixed an issue where an empty spell check language setting would crash novelWriter. Issue #1096.
|
||||||
|
PR #1098.
|
||||||
|
* Added a checker that blocks the automatic insertion of spaces in front of special characters in
|
||||||
|
the cases where the character is a colon in either a meta tag, or as part of the synopsis
|
||||||
|
keyword. This feature is used for certain languages like French and Spanish. Issue #1090.
|
||||||
|
PR #1099.
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
## Version 1.6.2 [2022-03-20]
|
## Version 1.6.2 [2022-03-20]
|
||||||
|
|
||||||
### Release Notes
|
### Release Notes
|
||||||
@@ -3468,8 +3797,8 @@ helpful feedback and issue reports for the new features added in this, and previ
|
|||||||
|
|
||||||
**User Interface**
|
**User Interface**
|
||||||
|
|
||||||
* Added a preferences dialog for the program settings. No longer necessary to edit the config file.
|
* Added a preferences dialog for the program settings. It is no longer necessary to edit the config
|
||||||
PR #30.
|
file. PR #30.
|
||||||
* The document viewer remembers scroll bar position when pressing `Ctrl+R` on a document already
|
* The document viewer remembers scroll bar position when pressing `Ctrl+R` on a document already
|
||||||
being viewed. PR #28.
|
being viewed. PR #28.
|
||||||
* Removed version number from windows title. PR #28.
|
* Removed version number from windows title. PR #28.
|
||||||
@@ -3510,8 +3839,8 @@ helpful feedback and issue reports for the new features added in this, and previ
|
|||||||
**Status Bar**
|
**Status Bar**
|
||||||
|
|
||||||
* Redesign of the status bar adding project and session stats as well as a session timer. PR #21.
|
* Redesign of the status bar adding project and session stats as well as a session timer. PR #21.
|
||||||
* Project word count is written to the project file, which is needed for the session word count. PR
|
* Project word count is written to the project file, which is needed for the session word count.
|
||||||
#21.
|
PR #21.
|
||||||
* Closing a project now clears the status bar. PR #21.
|
* Closing a project now clears the status bar. PR #21.
|
||||||
|
|
||||||
**Editor**
|
**Editor**
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ You can also follow novelWriter on Mastodon at [fosstodon.org/@novelwriter](http
|
|||||||
|
|
||||||
## Implementation
|
## Implementation
|
||||||
|
|
||||||
The application is written with Python 3 (3.7+) using Qt5 and PyQt5 (5.3+). It is developed on
|
The application is written with Python 3 (3.7+) using Qt5 and PyQt5 (5.10+). It is developed on
|
||||||
Linux, but should in principle work fine on other operating systems as well as long as dependencies
|
Linux, but should in principle work fine on other operating systems as well as long as dependencies
|
||||||
are met. It is regularly tested on Debian and Ubuntu Linux, Windows, and macOS.
|
are met. It is regularly tested on Debian and Ubuntu Linux, Windows, and macOS.
|
||||||
|
|
||||||
|
|||||||
|
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.
|
extension, and don't end up with two.
|
||||||
|
|
||||||
The QSS files are Qt Style Sheet files. See Qt's
|
The QSS files are Qt Style Sheet files. See Qt's
|
||||||
`The Style Sheet Syntax <https://doc.qt.io/qt-5/stylesheet-syntax.html>`_` documentation for more
|
`The Style Sheet Syntax <https://doc.qt.io/qt-5/stylesheet-syntax.html>`_ documentation for more
|
||||||
details.
|
details.
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ The following Python packages are needed to run novelWriter:
|
|||||||
* ``lxml`` – needed for full XML support.
|
* ``lxml`` – needed for full XML support.
|
||||||
* ``PyEnchant`` – needed for spell checking (optional).
|
* ``PyEnchant`` – needed for spell checking (optional).
|
||||||
|
|
||||||
PyQt/Qt should be at least 5.3, but ideally 5.10 or higher for nearly all features to work. For
|
PyQt/Qt should be at least 5.10, but ideally 5.13 or higher for nearly all features to work. For
|
||||||
instance, searching using regular expressions with full Unicode support requires 5.13. There is no
|
instance, searching using regular expressions with full Unicode support requires 5.13. There is no
|
||||||
known minimum version requirement for package ``lxml``, but the code was originally written with
|
known minimum version requirement for package ``lxml``, but the code was originally written with
|
||||||
4.2, which is therefore set as the minimum. It may work on lower versions. You have to test it.
|
4.2, which is therefore set as the minimum. It may work on lower versions. You have to test it.
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ Application Data
|
|||||||
|
|
||||||
novelWriter also stores a bit of data that is generated by the user's actions. This includes the
|
novelWriter also stores a bit of data that is generated by the user's actions. This includes the
|
||||||
list of recent projects form the :guilabel:`Open Project` dialog. Custom themes are also saved
|
list of recent projects form the :guilabel:`Open Project` dialog. Custom themes are also saved
|
||||||
here. The system paths are provided by the Qt QStandardPaths_ class and its AppDataLocation value
|
here. The system paths are provided by the Qt QStandardPaths_ class and its AppDataLocation.
|
||||||
on Qt 5.4 or greater, or DataLocation for earlier versions.
|
|
||||||
|
|
||||||
The standard paths are:
|
The standard paths are:
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ The main shorcuts are as follows:
|
|||||||
":kbd:`Ctrl`:kbd:`H`", "Open the search and replace bar and search for the selected word, if any is selected. (On Mac, this is :kbd:`Cmd`:kbd:`=`.)"
|
":kbd:`Ctrl`:kbd:`H`", "Open the search and replace bar and search for the selected word, if any is selected. (On Mac, this is :kbd:`Cmd`:kbd:`=`.)"
|
||||||
":kbd:`Ctrl`:kbd:`I`", "Format selected text, or word under cursor, with emphasis (italic)."
|
":kbd:`Ctrl`:kbd:`I`", "Format selected text, or word under cursor, with emphasis (italic)."
|
||||||
":kbd:`Ctrl`:kbd:`K`", "Activate the insert commands. The commands are listed in :ref:`a_kb_ins`."
|
":kbd:`Ctrl`:kbd:`K`", "Activate the insert commands. The commands are listed in :ref:`a_kb_ins`."
|
||||||
|
":kbd:`Ctrl`:kbd:`L`", "Open the :guilabel:`Quick Links` menu in the Project Tree."
|
||||||
":kbd:`Ctrl`:kbd:`N`", "Create new project item."
|
":kbd:`Ctrl`:kbd:`N`", "Create new project item."
|
||||||
":kbd:`Ctrl`:kbd:`O`", "Open selected document."
|
":kbd:`Ctrl`:kbd:`O`", "Open selected document."
|
||||||
":kbd:`Ctrl`:kbd:`Q`", "Exit novelWriter."
|
":kbd:`Ctrl`:kbd:`Q`", "Exit novelWriter."
|
||||||
@@ -136,6 +137,7 @@ a key or key combination for the inserted content.
|
|||||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`F`", "Insert a ``@focus`` keyword."
|
":kbd:`Ctrl`:kbd:`K`, :kbd:`F`", "Insert a ``@focus`` keyword."
|
||||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`C`", "Insert a ``@char`` keyword."
|
":kbd:`Ctrl`:kbd:`K`, :kbd:`C`", "Insert a ``@char`` keyword."
|
||||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`P`", "Insert a ``@plot`` keyword."
|
":kbd:`Ctrl`:kbd:`K`, :kbd:`P`", "Insert a ``@plot`` keyword."
|
||||||
|
":kbd:`Ctrl`:kbd:`K`, :kbd:`S`", "Insert a synopsis comment."
|
||||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`T`", "Insert a ``@time`` keyword."
|
":kbd:`Ctrl`:kbd:`K`, :kbd:`T`", "Insert a ``@time`` keyword."
|
||||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`L`", "Insert a ``@location`` keyword."
|
":kbd:`Ctrl`:kbd:`K`, :kbd:`L`", "Insert a ``@location`` keyword."
|
||||||
":kbd:`Ctrl`:kbd:`K`, :kbd:`O`", "Insert an ``@object`` keyword."
|
":kbd:`Ctrl`:kbd:`K`, :kbd:`O`", "Insert an ``@object`` keyword."
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import sys
|
|||||||
import getopt
|
import getopt
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt5.QtGui import QIcon
|
|
||||||
from PyQt5.QtWidgets import QApplication, QErrorMessage
|
from PyQt5.QtWidgets import QApplication, QErrorMessage
|
||||||
|
|
||||||
from novelwriter.error import exceptionHandler, logException
|
from novelwriter.error import exceptionHandler, logException
|
||||||
@@ -60,9 +59,9 @@ __license__ = "GPLv3"
|
|||||||
__author__ = "Veronica Berglyd Olsen"
|
__author__ = "Veronica Berglyd Olsen"
|
||||||
__maintainer__ = "Veronica Berglyd Olsen"
|
__maintainer__ = "Veronica Berglyd Olsen"
|
||||||
__email__ = "code@vkbo.net"
|
__email__ = "code@vkbo.net"
|
||||||
__version__ = "1.7-beta1"
|
__version__ = "2.0-rc2"
|
||||||
__hexversion__ = "0x010700b1"
|
__hexversion__ = "0x020000c2"
|
||||||
__date__ = "2022-05-17"
|
__date__ = "2022-11-13"
|
||||||
__status__ = "Stable"
|
__status__ = "Stable"
|
||||||
__domain__ = "novelwriter.io"
|
__domain__ = "novelwriter.io"
|
||||||
__url__ = "https://novelwriter.io"
|
__url__ = "https://novelwriter.io"
|
||||||
@@ -72,32 +71,6 @@ __helpurl__ = "https://github.com/vkbo/novelWriter/discussions"
|
|||||||
__releaseurl__ = "https://github.com/vkbo/novelWriter/releases/latest"
|
__releaseurl__ = "https://github.com/vkbo/novelWriter/releases/latest"
|
||||||
__docurl__ = "https://novelwriter.readthedocs.io"
|
__docurl__ = "https://novelwriter.readthedocs.io"
|
||||||
|
|
||||||
##
|
|
||||||
# Logging
|
|
||||||
# =========
|
|
||||||
# Standard used for logging levels in novelWriter:
|
|
||||||
# CRITICAL Use for errors that result in termination of the program
|
|
||||||
# ERROR Use when an action fails, but execution continues
|
|
||||||
# WARNING When something unexpected, but non-critical happens
|
|
||||||
# INFO Any useful user information like open, save, exit initiated
|
|
||||||
# ----------- SPAM Threshold : Output above should be minimal -----------------
|
|
||||||
# DEBUG Use for descriptions of main program flow
|
|
||||||
# VERBOSE Use for outputting values and program flow details
|
|
||||||
##
|
|
||||||
|
|
||||||
# Add verbose logging level
|
|
||||||
VERBOSE = 5
|
|
||||||
logging.addLevelName(VERBOSE, "VERBOSE")
|
|
||||||
|
|
||||||
|
|
||||||
def logVerbose(self, message, *args, **kws):
|
|
||||||
if self.isEnabledFor(VERBOSE):
|
|
||||||
self._log(VERBOSE, message, args, **kws)
|
|
||||||
|
|
||||||
|
|
||||||
logging.Logger.verbose = logVerbose
|
|
||||||
|
|
||||||
# Initiating logging
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -122,7 +95,6 @@ def main(sysArgs=None):
|
|||||||
"version",
|
"version",
|
||||||
"info",
|
"info",
|
||||||
"debug",
|
"debug",
|
||||||
"verbose",
|
|
||||||
"style=",
|
"style=",
|
||||||
"config=",
|
"config=",
|
||||||
"data=",
|
"data=",
|
||||||
@@ -143,7 +115,6 @@ def main(sysArgs=None):
|
|||||||
" -v, --version Print program version and exit.\n"
|
" -v, --version Print program version and exit.\n"
|
||||||
" --info Print additional runtime information.\n"
|
" --info Print additional runtime information.\n"
|
||||||
" --debug Print debug output. Includes --info.\n"
|
" --debug Print debug output. Includes --info.\n"
|
||||||
" --verbose Increase verbosity of debug output. Includes --debug.\n"
|
|
||||||
" --style= Sets Qt5 style flag. Defaults to 'Fusion'.\n"
|
" --style= Sets Qt5 style flag. Defaults to 'Fusion'.\n"
|
||||||
" --config= Alternative config file.\n"
|
" --config= Alternative config file.\n"
|
||||||
" --data= Alternative user data path.\n"
|
" --data= Alternative user data path.\n"
|
||||||
@@ -181,9 +152,6 @@ def main(sysArgs=None):
|
|||||||
elif inOpt == "--debug":
|
elif inOpt == "--debug":
|
||||||
logLevel = logging.DEBUG
|
logLevel = logging.DEBUG
|
||||||
logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}"
|
logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}"
|
||||||
elif inOpt == "--verbose":
|
|
||||||
logLevel = VERBOSE
|
|
||||||
logFormat = "[{asctime:}] {filename:>17}:{lineno:<4d} {levelname:8} {message:}"
|
|
||||||
elif inOpt == "--style":
|
elif inOpt == "--style":
|
||||||
qtStyle = inArg
|
qtStyle = inArg
|
||||||
elif inOpt == "--config":
|
elif inOpt == "--config":
|
||||||
@@ -193,9 +161,6 @@ def main(sysArgs=None):
|
|||||||
elif inOpt == "--testmode":
|
elif inOpt == "--testmode":
|
||||||
testMode = True
|
testMode = True
|
||||||
|
|
||||||
# Set Config Options
|
|
||||||
CONFIG.cmdOpen = cmdOpen
|
|
||||||
|
|
||||||
# Set Logging
|
# Set Logging
|
||||||
cHandle = logging.StreamHandler()
|
cHandle = logging.StreamHandler()
|
||||||
cHandle.setFormatter(logging.Formatter(fmt=logFormat, style="{"))
|
cHandle.setFormatter(logging.Formatter(fmt=logFormat, style="{"))
|
||||||
@@ -214,14 +179,14 @@ def main(sysArgs=None):
|
|||||||
"At least Python 3.7 is required, found %s" % CONFIG.verPyString
|
"At least Python 3.7 is required, found %s" % CONFIG.verPyString
|
||||||
)
|
)
|
||||||
errorCode |= 0x04
|
errorCode |= 0x04
|
||||||
if CONFIG.verQtValue < 50300:
|
if CONFIG.verQtValue < 51000:
|
||||||
errorData.append(
|
errorData.append(
|
||||||
"At least Qt5 version 5.3 is required, found %s" % CONFIG.verQtString
|
"At least Qt5 version 5.10 is required, found %s" % CONFIG.verQtString
|
||||||
)
|
)
|
||||||
errorCode |= 0x08
|
errorCode |= 0x08
|
||||||
if CONFIG.verPyQtValue < 50300:
|
if CONFIG.verPyQtValue < 51000:
|
||||||
errorData.append(
|
errorData.append(
|
||||||
"At least PyQt5 version 5.3 is required, found %s" % CONFIG.verPyQtString
|
"At least PyQt5 version 5.10 is required, found %s" % CONFIG.verPyQtString
|
||||||
)
|
)
|
||||||
errorCode |= 0x10
|
errorCode |= 0x10
|
||||||
|
|
||||||
@@ -280,7 +245,6 @@ def main(sysArgs=None):
|
|||||||
nwApp = QApplication([CONFIG.appName, (f"-style={qtStyle}")])
|
nwApp = QApplication([CONFIG.appName, (f"-style={qtStyle}")])
|
||||||
nwApp.setApplicationName(CONFIG.appName)
|
nwApp.setApplicationName(CONFIG.appName)
|
||||||
nwApp.setApplicationVersion(__version__)
|
nwApp.setApplicationVersion(__version__)
|
||||||
nwApp.setWindowIcon(QIcon(CONFIG.appIcon))
|
|
||||||
nwApp.setOrganizationDomain(__domain__)
|
nwApp.setOrganizationDomain(__domain__)
|
||||||
|
|
||||||
# Connect the exception handler before making the main GUI
|
# Connect the exception handler before making the main GUI
|
||||||
@@ -289,9 +253,7 @@ def main(sysArgs=None):
|
|||||||
# Launch main GUI
|
# Launch main GUI
|
||||||
CONFIG.initLocalisation(nwApp)
|
CONFIG.initLocalisation(nwApp)
|
||||||
nwGUI = GuiMain()
|
nwGUI = GuiMain()
|
||||||
if not nwGUI.hasProject:
|
nwGUI.postLaunchTasks(cmdOpen)
|
||||||
nwGUI.showProjectLoadDialog()
|
|
||||||
nwGUI.releaseNotes()
|
|
||||||
|
|
||||||
sys.exit(nwApp.exec_())
|
sys.exit(nwApp.exec_())
|
||||||
|
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
|||||||
[Map]
|
[Map]
|
||||||
add = typ_plus.svg
|
add = typ_plus.svg
|
||||||
backward = typ_chevron-left.svg
|
backward = typ_chevron-left.svg
|
||||||
|
bookmark = typ_bookmark.svg
|
||||||
bullet-off = typ_media-record-outline.svg
|
bullet-off = typ_media-record-outline.svg
|
||||||
bullet-on = typ_media-record.svg
|
bullet-on = typ_media-record.svg
|
||||||
check = typ_tick.svg
|
checked = mixed_input-checked.svg
|
||||||
clear = typ_backspace.svg
|
|
||||||
close = typ_times.svg
|
close = typ_times.svg
|
||||||
cls_archive = typ_delete.svg
|
cls_archive = typ_delete.svg
|
||||||
cls_character = typ_user.svg
|
cls_character = typ_user.svg
|
||||||
@@ -35,32 +35,25 @@ cls_timeline = typ_calendar.svg
|
|||||||
cls_trash = typ_trash.svg
|
cls_trash = typ_trash.svg
|
||||||
cls_world = typ_location.svg
|
cls_world = typ_location.svg
|
||||||
cross = typ_times.svg
|
cross = typ_times.svg
|
||||||
delete = typ_delete.svg
|
|
||||||
doc_h0 = mixed_heading0.svg
|
|
||||||
doc_h1 = mixed_heading1.svg
|
|
||||||
doc_h2 = mixed_heading2.svg
|
|
||||||
doc_h3 = mixed_heading3.svg
|
|
||||||
doc_h4 = mixed_heading4.svg
|
|
||||||
done = typ_input-checked.svg
|
|
||||||
down = typ_chevron-down.svg
|
down = typ_chevron-down.svg
|
||||||
edit = typ_pencil.svg
|
edit = typ_pencil.svg
|
||||||
forward = typ_chevron-right.svg
|
forward = typ_chevron-right.svg
|
||||||
hash = typ_hash.svg
|
|
||||||
maximise = typ_arrow-maximise.svg
|
maximise = typ_arrow-maximise.svg
|
||||||
menu = typ_th-menu.svg
|
menu = typ_th-menu.svg
|
||||||
minimise = typ_arrow-minimise.svg
|
minimise = typ_arrow-minimise.svg
|
||||||
|
noncheckable = mixed_input-none.svg
|
||||||
proj_chapter = mixed_document-chapter.svg
|
proj_chapter = mixed_document-chapter.svg
|
||||||
proj_details = typ_th-list-grey.svg
|
proj_details = typ_th-list-grey.svg
|
||||||
proj_document = typ_document-text.svg
|
proj_document = typ_document-text.svg
|
||||||
proj_folder = typ_folder.svg
|
proj_folder = typ_folder.svg
|
||||||
proj_note = mixed_document-note.svg
|
proj_note = mixed_document-note.svg
|
||||||
proj_scene = mixed_document-scene.svg
|
proj_scene = mixed_document-scene.svg
|
||||||
|
proj_section = mixed_document-section.svg
|
||||||
proj_stats = typ_chart-bar-grey.svg
|
proj_stats = typ_chart-bar-grey.svg
|
||||||
proj_title = mixed_document-title.svg
|
proj_title = mixed_document-title.svg
|
||||||
reference = typ_at.svg
|
reference = typ_at.svg
|
||||||
refresh = typ_refresh.svg
|
refresh = typ_refresh.svg
|
||||||
remove = typ_minus.svg
|
remove = typ_minus.svg
|
||||||
save = typ_download.svg
|
|
||||||
search = typ_search.svg
|
search = typ_search.svg
|
||||||
search_cancel = typ_cancel-grey.svg
|
search_cancel = typ_cancel-grey.svg
|
||||||
search_case = nw_search-case.svg
|
search_case = nw_search-case.svg
|
||||||
@@ -78,13 +71,16 @@ status_stats = typ_chart-bar-grey.svg
|
|||||||
status_time = typ_stopwatch-grey.svg
|
status_time = typ_stopwatch-grey.svg
|
||||||
sticky-off = typ_pin-outline.svg
|
sticky-off = typ_pin-outline.svg
|
||||||
sticky-on = typ_pin.svg
|
sticky-on = typ_pin.svg
|
||||||
|
unchecked = mixed_input-unchecked.svg
|
||||||
up = typ_chevron-up.svg
|
up = typ_chevron-up.svg
|
||||||
view_build = typ_export.svg
|
view_build = typ_export.svg
|
||||||
view_editor = mixed_edit.svg
|
view_editor = mixed_edit.svg
|
||||||
view_novel = typ_book-grey.svg
|
view_novel = typ_book-grey.svg
|
||||||
view_outline = typ_puzzle-outline.svg
|
view_outline = typ_puzzle-outline.svg
|
||||||
|
|
||||||
|
deco_doc_h0 = nw_deco-h0.svg
|
||||||
deco_doc_h1 = nw_deco-h1.svg
|
deco_doc_h1 = nw_deco-h1.svg
|
||||||
deco_doc_h2 = nw_deco-h2.svg
|
deco_doc_h2 = nw_deco-h2.svg
|
||||||
deco_doc_h3 = nw_deco-h3.svg
|
deco_doc_h3 = nw_deco-h3.svg
|
||||||
deco_doc_h4 = nw_deco-h4.svg
|
deco_doc_h4 = nw_deco-h4.svg
|
||||||
|
deco_doc_more = nw_deco-noveltree-more.svg
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.2"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="svg2161">
|
||||||
|
<metadata
|
||||||
|
id="metadata2167">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs2165" />
|
||||||
|
<path
|
||||||
|
id="rect12986"
|
||||||
|
style="display:inline;opacity:0.95;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.88635;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 5.8886719 3.4453125 C 5.2152275 3.4453125 4.6660156 3.9925712 4.6660156 4.6660156 L 4.6660156 19.333984 C 4.6660156 20.007428 5.2152275 20.554687 5.8886719 20.554688 L 18.111328 20.554688 C 18.784774 20.554688 19.333984 20.007428 19.333984 19.333984 L 19.333984 4.6660156 C 19.333984 3.9925712 18.784774 3.4453125 18.111328 3.4453125 L 5.8886719 3.4453125 z " />
|
||||||
|
<path
|
||||||
|
id="rect935"
|
||||||
|
style="fill:#999999;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
|
||||||
|
d="M 5.8886719 3.4453125 C 5.2152275 3.4453125 4.6660156 3.9925712 4.6660156 4.6660156 L 4.6660156 6 L 4.6660156 7.1894531 L 4.6660156 10.058594 L 19.333984 10.058594 L 19.333984 7.1894531 L 19.333984 6 L 19.333984 4.6660156 C 19.333984 3.9925712 18.784774 3.4453125 18.111328 3.4453125 L 5.8886719 3.4453125 z " />
|
||||||
|
<path
|
||||||
|
id="path2157"
|
||||||
|
style="display:inline;fill:#595959;fill-opacity:1;stroke-width:1.22222"
|
||||||
|
d="M 7.1113281,13.222656 C 6.7739949,13.222656 6.5,13.49665 6.5,13.833984 c 0,0.337332 0.2739949,0.611328 0.6113281,0.611328 h 9.7773439 c 0.337334,1e-6 0.611328,-0.273996 0.611328,-0.611328 0,-0.337334 -0.273994,-0.611328 -0.611328,-0.611328 z m 0,3.666016 C 6.7739949,16.888672 6.5,17.162666 6.5,17.5 c 0,0.337334 0.2739949,0.611328 0.6113281,0.611328 H 16.888672 C 17.226006,18.111328 17.5,17.837334 17.5,17.5 c 0,-0.337334 -0.273994,-0.611328 -0.611328,-0.611328 z" />
|
||||||
|
<path
|
||||||
|
id="path902"
|
||||||
|
style="display:inline;fill:#848484;fill-opacity:1;stroke-width:1.22222"
|
||||||
|
d="M 5.8886719,1 C 3.8671165,1 2.2226562,2.6444602 2.2226562,4.6660156 V 19.333984 C 2.2226563,21.355538 3.8671165,23 5.8886719,23 H 18.111328 c 2.021556,0 3.666016,-1.644462 3.666016,-3.666016 V 4.6660156 C 21.777344,2.6444602 20.132884,1 18.111328,1 Z m 0,2.4453125 H 18.111328 c 0.673446,0 1.222656,0.5472587 1.222656,1.2207031 V 19.333984 c 0,0.673444 -0.54921,1.220704 -1.222656,1.220704 H 5.8886719 c -0.6734444,-10e-7 -1.2226563,-0.54726 -1.2226563,-1.220704 V 4.6660156 c 0,-0.6734444 0.5492119,-1.2207031 1.2226563,-1.2207031 z" />
|
||||||
|
<path
|
||||||
|
id="path2157-0"
|
||||||
|
style="display:inline;fill:#ffffff;fill-opacity:1;stroke-width:1.22222"
|
||||||
|
d="M 7.1113281 5.8886719 C 6.7739949 5.8886719 6.5 6.1626667 6.5 6.5 C 6.5 6.837334 6.7739949 7.1113281 7.1113281 7.1113281 L 16.888672 7.1113281 C 17.226006 7.1113281 17.5 6.837334 17.5 6.5 C 17.5 6.1626667 17.226006 5.8886719 16.888672 5.8886719 L 7.1113281 5.8886719 z " />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -1,31 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg1897"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
version="1.2">
|
|
||||||
<metadata
|
|
||||||
id="metadata1903">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs1901" />
|
|
||||||
<path
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke-width:1.42859697"
|
|
||||||
id="path1895"
|
|
||||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,37 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg1897"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
version="1.2">
|
|
||||||
<metadata
|
|
||||||
id="metadata1903">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs1901" />
|
|
||||||
<path
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke-width:1.42859697"
|
|
||||||
id="path1895"
|
|
||||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
|
||||||
<circle
|
|
||||||
r="2.2222221"
|
|
||||||
cy="4.2222223"
|
|
||||||
cx="19.470242"
|
|
||||||
id="path2467"
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,43 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg1897"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
version="1.2">
|
|
||||||
<metadata
|
|
||||||
id="metadata1903">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs1901" />
|
|
||||||
<path
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke-width:1.42859697"
|
|
||||||
id="path1895"
|
|
||||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
|
||||||
<circle
|
|
||||||
r="2.2222221"
|
|
||||||
cy="4.2222223"
|
|
||||||
cx="19.470242"
|
|
||||||
id="path2467"
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
<circle
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="circle2469"
|
|
||||||
cx="19.470242"
|
|
||||||
cy="9.4077778"
|
|
||||||
r="2.2222221" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,49 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg1897"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
version="1.2">
|
|
||||||
<metadata
|
|
||||||
id="metadata1903">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs1901" />
|
|
||||||
<path
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke-width:1.42859697"
|
|
||||||
id="path1895"
|
|
||||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
|
||||||
<circle
|
|
||||||
r="2.2222221"
|
|
||||||
cy="4.2222223"
|
|
||||||
cx="19.470242"
|
|
||||||
id="path2467"
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
<circle
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="circle2469"
|
|
||||||
cx="19.470242"
|
|
||||||
cy="9.4077778"
|
|
||||||
r="2.2222221" />
|
|
||||||
<circle
|
|
||||||
r="2.2222221"
|
|
||||||
cy="14.592222"
|
|
||||||
cx="19.470242"
|
|
||||||
id="circle2471"
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,59 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg1897"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
version="1.2">
|
|
||||||
<metadata
|
|
||||||
id="metadata1903">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs1901" />
|
|
||||||
<g
|
|
||||||
transform="matrix(1.1111111,0,0,1.1111111,-1.3333334,-1.3333332)"
|
|
||||||
id="g2616">
|
|
||||||
<path
|
|
||||||
d="m 4.029904,3.7531206 c -1.0041608,1.0041608 -1.0041608,2.6319041 0,3.6360649 L 8.6392724,11.999839 4.029904,16.610493 c -1.0041608,1.004161 -1.0041608,2.631904 0,3.636065 C 4.5313415,20.749281 5.189639,21 5.8479364,21 c 0.658298,0 1.316595,-0.250719 1.818033,-0.753442 L 15.913973,11.999839 7.6659694,3.7531206 c -1.002876,-1.0041608 -2.6331904,-1.0041608 -3.6360654,0 z"
|
|
||||||
id="path1895"
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke-width:1.28573728" />
|
|
||||||
<circle
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.79999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="path2467"
|
|
||||||
cx="18.723217"
|
|
||||||
cy="5"
|
|
||||||
r="2" />
|
|
||||||
<circle
|
|
||||||
r="2"
|
|
||||||
cy="9.6669998"
|
|
||||||
cx="18.723217"
|
|
||||||
id="circle2469"
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.79999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
<circle
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.79999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="circle2471"
|
|
||||||
cx="18.723217"
|
|
||||||
cy="14.333"
|
|
||||||
r="2" />
|
|
||||||
<circle
|
|
||||||
r="2"
|
|
||||||
cy="19"
|
|
||||||
cx="18.723217"
|
|
||||||
id="circle2473"
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.79999995;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
version="1.2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="svg2287">
|
||||||
|
<defs
|
||||||
|
id="defs2291" />
|
||||||
|
<metadata
|
||||||
|
id="metadata2281">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<path
|
||||||
|
d="m17.714 22h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
|
||||||
|
fill="#848484"
|
||||||
|
stroke-width="1.4286"
|
||||||
|
id="path2283" />
|
||||||
|
<path
|
||||||
|
d="m19.02 3.3503c-0.60933 0.010623-1.2003 0.31545-1.5586 0.86719l-5.0977 7.8477-2.3672-3.6445c-0.57329-0.88278-1.7442-1.1319-2.627-0.55859-0.88278 0.57329-1.1319 1.7461-0.55859 2.6289l3.9199 6.0391c0.38514 0.59307 1.0409 0.88965 1.6973 0.85352 0.03023-2.78e-4 0.05972-0.0041 0.08984-0.0059 0.0556-0.0057 0.11074-0.0088 0.16602-0.01953 0.52104-0.07621 1.0077-0.36147 1.3184-0.83984l6.6445-10.23c0.57329-0.88278 0.32419-2.0556-0.55859-2.6289-0.33104-0.21498-0.70276-0.31497-1.0684-0.30859z"
|
||||||
|
fill="#9c9"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1.111"
|
||||||
|
id="path2285" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
version="1.2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="svg2931">
|
||||||
|
<defs
|
||||||
|
id="defs2935" />
|
||||||
|
<metadata
|
||||||
|
id="metadata2925">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<path
|
||||||
|
d="m2 17.714v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h11.429c2.3629 0 4.2857 1.9229 4.2857 4.2857v7.1429c0 0.79-0.64 1.4286-1.4286 1.4286-0.78857 0-1.4286-0.63857-1.4286-1.4286v-7.1429c0-0.78857-0.64143-1.4286-1.4286-1.4286h-11.429c-0.78714 0-1.4286 0.64-1.4286 1.4286v11.429c0 0.78857 0.64143 1.4286 1.4286 1.4286h4.2857c0.78857 0 1.4286 0.63857 1.4286 1.4286s-0.64 1.4286-1.4286 1.4286h-4.2857c-2.3629 0-4.2857-1.9229-4.2857-4.2857zm15.714 4.2857h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
|
||||||
|
fill="#848484"
|
||||||
|
stroke-width="1.4286"
|
||||||
|
id="path2927" />
|
||||||
|
<rect
|
||||||
|
x="7"
|
||||||
|
y="10.1"
|
||||||
|
width="10"
|
||||||
|
height="3.8"
|
||||||
|
rx="1.9"
|
||||||
|
ry="1.9"
|
||||||
|
fill="#f99157"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1.111"
|
||||||
|
id="rect2929" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
version="1.2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="svg3574">
|
||||||
|
<defs
|
||||||
|
id="defs3578" />
|
||||||
|
<metadata
|
||||||
|
id="metadata3568">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<path
|
||||||
|
d="m17.714 22h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
|
||||||
|
fill="#848484"
|
||||||
|
stroke-width="1.4286"
|
||||||
|
id="path3570" />
|
||||||
|
<path
|
||||||
|
d="m17.99 3.3379c-0.48501 0.025418-0.96034 0.23584-1.3125 0.62695l-4.6777 5.1953-1.332-1.4785c-0.70433-0.78223-1.9014-0.84495-2.6836-0.14062-0.78223 0.70433-0.84495 1.9014-0.14062 2.6836l1.5996 1.7754-1.5977 1.7754c-0.70433 0.78223-0.64161 1.9793 0.14062 2.6836 0.78223 0.70433 1.9773 0.64161 2.6816-0.14062l1.332-1.4785 1.332 1.4785c0.70433 0.78224 1.9014 0.84495 2.6836 0.14062 0.78223-0.70433 0.84495-1.9014 0.14062-2.6836l-1.5996-1.7754 4.9453-5.4922c0.70433-0.78223 0.64161-1.9793-0.14062-2.6836-0.39112-0.35216-0.88608-0.51175-1.3711-0.48633z"
|
||||||
|
fill="#d64848"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1.111"
|
||||||
|
id="path3572" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
version="1.2"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="svg2161"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<metadata
|
||||||
|
id="metadata2167">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs2165" />
|
||||||
|
<path
|
||||||
|
d="M 9.7,18.2 16,12 9.7,5.8 C 9.5,5.6 9.2,5.5 9,5.5 8.8,5.5 8.5,5.6 8.3,5.8 8.1,6 8,6.2 8,6.5 v 11 c 0,0.3 0.1,0.5 0.3,0.7 0.2,0.2 0.5,0.3 0.7,0.3 0.2,0 0.5,-0.1 0.7,-0.3 z"
|
||||||
|
id="path3096"
|
||||||
|
style="fill:#848484;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 928 B |
@@ -1,31 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
version="1.2"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
id="svg13156">
|
|
||||||
<metadata
|
|
||||||
id="metadata13162">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs13160" />
|
|
||||||
<path
|
|
||||||
d="M 19.496214,4.4886395 H 9.4810665 c -1.2679176,0 -2.8382927,0.8082224 -3.5754075,1.8397825 L 3.2917056,9.9869555 2.0908895,11.668499 c -0.1211833,0.175265 -0.1221848,0.492744 0.003,0.665006 l 1.1897995,1.66652 2.622967,3.672554 c 0.7361133,1.030558 2.3054869,1.838782 3.574406,1.838782 H 19.496214 C 20.877302,19.511361 22,18.387661 22,17.007573 V 6.9924263 C 22,5.612339 20.877302,4.4886395 19.496214,4.4886395 Z M 17.19974,14.296473 c 0.391593,0.391592 0.391593,1.02455 0,1.416141 -0.195295,0.195296 -0.451683,0.293445 -0.70807,0.293445 -0.256388,0 -0.512776,-0.09815 -0.708071,-0.293445 l -2.296474,-2.296473 -2.296473,2.296473 c -0.195295,0.195296 -0.451683,0.293445 -0.708071,0.293445 -0.256388,0 -0.512775,-0.09815 -0.7080706,-0.293445 -0.3915923,-0.391591 -0.3915923,-1.024549 0,-1.416141 L 12.070984,12 9.7745104,9.7035265 c -0.3915923,-0.3915921 -0.3915923,-1.0245494 0,-1.4161417 0.3915926,-0.3915922 1.0245496,-0.3915922 1.4161416,0 l 2.296473,2.2964732 2.296474,-2.2964732 c 0.391592,-0.3915922 1.024549,-0.3915922 1.416141,0 0.391593,0.3915923 0.391593,1.0245496 0,1.4161417 L 14.903267,12 Z"
|
|
||||||
id="path13154"
|
|
||||||
style="stroke-width:1.00151;fill:#d64848;fill-opacity:1" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -9,9 +9,9 @@
|
|||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
id="svg2608">
|
id="svg901">
|
||||||
<metadata
|
<metadata
|
||||||
id="metadata2614">
|
id="metadata907">
|
||||||
<rdf:RDF>
|
<rdf:RDF>
|
||||||
<cc:Work
|
<cc:Work
|
||||||
rdf:about="">
|
rdf:about="">
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
</rdf:RDF>
|
</rdf:RDF>
|
||||||
</metadata>
|
</metadata>
|
||||||
<defs
|
<defs
|
||||||
id="defs2612" />
|
id="defs905" />
|
||||||
<path
|
<path
|
||||||
d="M 16.203226,2 H 7.796774 C 6.05874,2 4.6443545,3.4143855 4.6443545,5.1524195 V 19.86371 c 0,0.540115 0.1092839,0.994063 0.3236484,1.350287 0.5926549,0.982504 1.9072138,1.059213 2.9559187,0.0084 L 11.25708,17.889239 c 0.394052,-0.393002 1.091787,-0.393002 1.485841,0 l 3.333158,3.333158 C 16.592024,21.738349 17.128987,22 17.673304,22 c 0.837492,0 1.682342,-0.660957 1.682342,-2.13629 V 5.1524195 C 19.355646,3.4143855 17.941259,2 16.203226,2 Z M 7.796774,4.101613 h 8.406452 c 0.578994,0 1.050806,0.4718121 1.050806,1.0508065 V 15.560658 l -2.575527,-2.361162 c -1.477434,-1.35449 -3.880628,-1.353439 -5.3580615,0 l -2.574476,2.361162 V 5.1524195 c 0,-0.5789944 0.4718121,-1.0508065 1.0508065,-1.0508065 z m 6.431987,12.301792 C 13.635055,15.809699 12.843798,15.482898 12,15.482898 c -0.843797,0 -1.635054,0.327852 -2.2287605,0.920507 l -3.025272,3.025272 v -2.442075 l 3.2848215,-3.011611 c 1.085483,-0.995114 2.851889,-0.995114 3.937372,0 l 3.285871,3.011611 v 2.442075 z"
|
d="M 16.203226,2 H 7.796774 C 6.05874,2 4.6443545,3.4143855 4.6443545,5.1524195 V 19.86371 c 0,0.540115 0.1092839,0.994063 0.3236484,1.350287 0.5926549,0.982504 1.9072138,1.059213 2.9559187,0.0084 L 11.25708,17.889239 c 0.394052,-0.393002 1.091787,-0.393002 1.485841,0 l 3.333158,3.333158 C 16.592024,21.738349 17.128987,22 17.673304,22 c 0.837492,0 1.682342,-0.660957 1.682342,-2.13629 V 5.1524195 C 19.355646,3.4143855 17.941259,2 16.203226,2 Z M 7.796774,4.101613 h 8.406452 c 0.578994,0 1.050806,0.4718121 1.050806,1.0508065 V 15.560658 l -2.575527,-2.361162 c -1.477434,-1.35449 -3.880628,-1.353439 -5.3580615,0 l -2.574476,2.361162 V 5.1524195 c 0,-0.5789944 0.4718121,-1.0508065 1.0508065,-1.0508065 z m 6.431987,12.301792 C 13.635055,15.809699 12.843798,15.482898 12,15.482898 c -0.843797,0 -1.635054,0.327852 -2.2287605,0.920507 l -3.025272,3.025272 v -2.442075 l 3.2848215,-3.011611 c 1.085483,-0.995114 2.851889,-0.995114 3.937372,0 l 3.285871,3.011611 v 2.442075 z"
|
||||||
id="path2606"
|
id="path899"
|
||||||
style="stroke-width:1.05081;fill:#6699cc;fill-opacity:1" />
|
style="fill:#6699cc;fill-opacity:1;stroke-width:1.05081" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,35 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
version="1.2"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
id="svg13801">
|
|
||||||
<metadata
|
|
||||||
id="metadata13807">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs13805" />
|
|
||||||
<path
|
|
||||||
style="display:inline;fill:#cda66f;fill-opacity:1"
|
|
||||||
id="path6288"
|
|
||||||
d="m 20.987,16 c 0,-0.105 -0.004,-0.211 -0.039,-0.316 l -2,-6 C 18.812,9.275 18.431,9 18,9 h -0.219 c -0.094,0.188 -0.21,0.368 -0.367,0.525 L 15.932,11 h 1.348 l 1.667,5 H 5.054 L 6.721,11 H 8.069 L 6.586,9.525 C 6.429,9.368 6.312,9.188 6.219,9 H 6 C 5.569,9 5.188,9.275 5.052,9.684 l -2,6 C 3.017,15.789 3.013,15.895 3.013,16 3,16 3,21 3,21 c 0,0.553 0.447,1 1,1 h 16 c 0.553,0 1,-0.447 1,-1 0,0 0,-5 -0.013,-5 z" />
|
|
||||||
<path
|
|
||||||
d="M 16.707,7.404 C 16.518,7.216 16.259,7.121 16,7.121 c -0.259,0 -0.518,0.095 -0.707,0.283 L 13,9.697 V 3 C 13,2.448 12.552,2 12,2 11.448,2 11,2.448 11,3 V 9.697 L 8.707,7.404 C 8.518,7.216 8.267,7.111 8,7.111 c -0.267,0 -0.518,0.105 -0.707,0.293 -0.39,0.39 -0.39,1.024 0,1.414 L 12,13.5 16.709,8.816 C 17.097,8.429 17.097,7.794 16.707,7.404 Z"
|
|
||||||
id="path13799"
|
|
||||||
style="display:inline;fill:#99cc99;fill-opacity:1" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,31 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
version="1.2"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
id="svg3369">
|
|
||||||
<metadata
|
|
||||||
id="metadata3375">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs3373" />
|
|
||||||
<path
|
|
||||||
d="m 10.828125,20 h -4 c -0.553,0 -1,0.447 -1,1 0,0.553 0.447,1 1,1 h 10 c 0.553,0 1,-0.447 1,-1 0,-0.553 -0.447,-1 -1,-1 h -4 v -1.23 c 1.64,-0.371 3.146,-1.188 4.363,-2.406 1.7,-1.7 2.637,-3.96 2.637,-6.364 0,-2.067 -0.692,-4.029 -1.968,-5.619 l 0.675,-0.673 c 0.391,-0.391 0.391,-1.023 10e-4,-1.415 -0.391,-0.391 -1.024,-0.39 -1.415,-0.001 l -2.052,2.049 0.708,0.708 c 1.322,1.322 2.051,3.081 2.051,4.951 0,1.87 -0.729,3.627 -2.051,4.949 -1.322,1.322 -3.079,2.051 -4.949,2.051 -1.87,0 -3.627,-0.729 -4.949,-2.051 -0.391,-0.391 -1.023,-0.391 -1.414,0 -0.391,0.39 -0.391,1.023 0,1.414 1.699,1.7 3.959,2.637 6.363,2.637 z m 0,-16 c 1.657,0 3.157,0.672 4.243,1.757 1.085,1.086 1.757,2.586 1.757,4.243 0,1.656 -0.672,3.156 -1.757,4.242 -1.086,1.086 -2.586,1.758 -4.243,1.758 -1.658,0 -3.157,-0.672 -4.242,-1.757 -1.085,-1.086 -1.756,-2.586 -1.756,-4.243 0,-1.657 0.671,-3.157 1.756,-4.243 1.085,-1.085 2.584,-1.757 4.242,-1.757 z"
|
|
||||||
id="path3367"
|
|
||||||
style="fill:#aeaeae;fill-opacity:1;stroke-width:1" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,31 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
version="1.2"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
id="svg5204">
|
|
||||||
<metadata
|
|
||||||
id="metadata5210">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs5208" />
|
|
||||||
<path
|
|
||||||
id="rect928"
|
|
||||||
d="M 7.75 2 C 6.7805 2 6 2.7805 6 3.75 L 6 5.5 L 4.1621094 5.5 C 3.2410844 5.5 2.5 6.2805 2.5 7.25 C 2.5 8.2195 3.2410844 9 4.1621094 9 L 6 9 L 6 15 L 4.1621094 15 C 3.2410844 15 2.5 15.7805 2.5 16.75 C 2.5 17.7195 3.2410844 18.5 4.1621094 18.5 L 6 18.5 L 6 20.25 C 6 21.2195 6.7805 22 7.75 22 C 8.7195 22 9.5 21.2195 9.5 20.25 L 9.5 18.5 L 14.5 18.5 L 14.5 20.25 C 14.5 21.2195 15.2805 22 16.25 22 C 17.2195 22 18 21.2195 18 20.25 L 18 18.5 L 19.837891 18.5 C 20.758916 18.5 21.5 17.7195 21.5 16.75 C 21.5 15.7805 20.758916 15 19.837891 15 L 18 15 L 18 9 L 19.837891 9 C 20.758916 9 21.5 8.2195 21.5 7.25 C 21.5 6.2805 20.758916 5.5 19.837891 5.5 L 18 5.5 L 18 3.75 C 18 2.7805 17.2195 2 16.25 2 C 15.2805 2 14.5 2.7805 14.5 3.75 L 14.5 5.5 L 9.5 5.5 L 9.5 3.75 C 9.5 2.7805 8.7195 2 7.75 2 z M 9.5 9 L 14.5 9 L 14.5 15 L 9.5 15 L 9.5 9 z "
|
|
||||||
style="fill:#6699cc;fill-opacity:1;stroke:none;stroke-width:1.7544229;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,35 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg11367"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
version="1.2">
|
|
||||||
<metadata
|
|
||||||
id="metadata11373">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs11371" />
|
|
||||||
<path
|
|
||||||
style="display:inline;fill:#99cc99;fill-opacity:1;stroke-width:1.28570998"
|
|
||||||
id="path11365"
|
|
||||||
d="m 13.499143,15.642429 c -0.45,0 -0.885857,-0.178715 -1.209857,-0.502715 L 8.859,11.710714 c -0.668571,-0.669857 -0.668571,-1.755 0,-2.423571 0.669857,-0.669857 1.755,-0.669857 2.426143,0 L 13.105714,11.109 17.573571,4.0954286 c 0.459,-0.828 1.504286,-1.1275715 2.332286,-0.6672857 0.826714,0.4602857 1.125,1.5042857 0.666,2.331 l -5.572286,9.0000001 c -0.261,0.470571 -0.727714,0.790714 -1.26,0.865286 z" />
|
|
||||||
<path
|
|
||||||
d="M 17.142857,21 H 6.8571429 C 4.7305714,21 3,19.269429 3,17.142857 V 6.8571429 C 3,4.7305714 4.7305714,3 6.8571429,3 h 6.4285711 c 0.711,0 1.285715,0.576 1.285715,1.2857143 0,0.7097143 -0.574715,1.2857143 -1.285715,1.2857143 H 6.8571429 c -0.7097143,0 -1.2857143,0.5772857 -1.2857143,1.2857143 V 17.142857 c 0,0.708429 0.576,1.285714 1.2857143,1.285714 H 17.142857 c 0.709714,0 1.285714,-0.577285 1.285714,-1.285714 V 13.285714 C 18.428571,12.576 19.003286,12 19.714286,12 20.425286,12 21,12.576 21,13.285714 v 3.857143 C 21,19.269429 19.269429,21 17.142857,21 Z"
|
|
||||||
id="path16196"
|
|
||||||
style="display:inline;fill:#aeaeae;fill-opacity:1;stroke-width:1.28570998" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,39 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
version="1.2"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
id="svg873">
|
|
||||||
<metadata
|
|
||||||
id="metadata879">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs877" />
|
|
||||||
<path
|
|
||||||
id="path871"
|
|
||||||
style="display:inline;fill:#6699cc;fill-opacity:1;stroke-width:1.19007"
|
|
||||||
d="m 10.330622,1.9999997 c -4.5924663,0 -8.3306193,3.738153 -8.3306193,8.3306193 0,1.391929 0.3467825,2.702587 0.9532665,3.857242 0.084308,-0.108411 0.1590865,-0.228838 0.2534294,-0.323181 L 4.7482003,12.323178 C 4.5237338,11.697056 4.378519,11.032991 4.3785189,10.330619 c 0,-3.2810129 2.6710902,-5.949778 5.9521031,-5.949778 0.702303,0 1.36649,0.1429324 1.992559,0.3673563 L 14.141363,2.9300158 C 12.997884,2.3384803 11.704084,1.9999997 10.330622,1.9999997 Z m 5.635898,3.4852355 -0.60451,0.6045104 2.545919,2.5482442 0.460357,-0.4626829 C 18.332278,8.0413114 18.299136,7.9066594 18.256684,7.7754 Z m 0.292955,4.4245521 c 0.0083,0.1189097 0.01733,0.2373427 0.01861,0.3580557 l 0.169728,-0.169728 z m 2.059986,2.6993717 -5.710299,5.710299 c 0.105352,-0.03032 0.217183,-0.04484 0.320855,-0.07906 l 0.892816,0.89049 1.799581,1.797261 0.07208,0.07208 0.07672,0.06743 C 16.486443,21.66984 17.375591,22 18.275281,22 c 2.054056,0 3.724716,-1.671795 3.724716,-3.72704 0,-0.999657 -0.390465,-1.936781 -1.099745,-2.638921 l -1.129969,-1.174145 -0.639387,-0.639386 -0.890489,-0.892815 c 0.03403,-0.103138 0.04885,-0.213757 0.07906,-0.318531 z m -11.7600542,2.28784 -0.4929085,0.492908 0.5417343,0.541734 0.57196,-0.571959 C 6.960254,15.221393 6.7588057,15.062002 6.5594068,14.896999 Z m 2.7179722,1.285747 -1.209021,1.209021 0.5417345,0.541734 1.6577535,-1.655429 c -0.3372847,-0.0036 -0.668706,-0.03764 -0.990467,-0.09533 z" />
|
|
||||||
<path
|
|
||||||
d="m 15.96652,2.4766329 c -0.396657,0 -0.792437,0.1507259 -1.095095,0.4533829 L 3.57173,14.229712 c -0.3026571,0.302657 -0.5759103,0.751428 -0.7742385,1.227621 -0.1983282,0.47826 -0.3208556,0.99063 -0.3208556,1.418274 v 4.647756 h 4.6477558 c 0.427645,0 0.9376894,-0.122526 1.4159495,-0.320855 0.4782601,-0.198328 0.9272894,-0.471583 1.2299463,-0.774239 L 21.069984,9.1285734 c 0.302656,-0.302657 0.453382,-0.7017958 0.453382,-1.0974191 0,-0.3966562 -0.150726,-0.7924369 -0.453382,-1.0950939 L 17.063939,2.9300158 C 16.761281,2.6273588 16.362143,2.4766329 15.96652,2.4766329 Z m 0,2.2785396 3.275981,3.2759818 -1.334572,1.3368983 -3.275982,-3.2759819 z M 13.901884,6.8221334 15.173682,8.0939305 6.6082326,16.661704 5.3364357,15.387582 Z m 2.004185,2.0041848 1.271796,1.2717968 -8.5654475,8.565448 -1.2741221,-1.271796 z M 4.7063497,16.250176 c 0.010333,-0.02065 3.0644031,3.034179 3.0644031,3.034179 C 7.43814,19.422771 7.2039295,19.456408 7.1243917,19.456408 H 5.5759147 L 4.5435968,18.424084 v -1.548477 c 0,-0.07953 0.033633,-0.315548 0.1627529,-0.625436 z"
|
|
||||||
style="display:inline;fill:#99cc99;fill-opacity:1;stroke-width:1.19007"
|
|
||||||
id="path15590" />
|
|
||||||
<path
|
|
||||||
id="rect19553"
|
|
||||||
d="m 2.4766359,18.937425 v 2.585938 h 2.5605469 z"
|
|
||||||
style="display:inline;opacity:0.95;fill:#0a0a0a;fill-opacity:1;stroke:none;stroke-width:1.88249397;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -1,36 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg7030"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
version="1.2">
|
|
||||||
<metadata
|
|
||||||
id="metadata7036">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs7034" />
|
|
||||||
<g
|
|
||||||
style="fill:#aeaeae;fill-opacity:1"
|
|
||||||
transform="matrix(0.95238095,0,0,0.95238095,0.5714286,0.09523812)"
|
|
||||||
id="g7028">
|
|
||||||
<path
|
|
||||||
style="fill:#aeaeae;fill-opacity:1"
|
|
||||||
id="path7026"
|
|
||||||
d="M 19.414,8.902 C 19.518,8.854 19.62,8.794 19.707,8.707 l 0.5,-0.5 c 0.391,-0.391 0.391,-1.023 0,-1.414 -0.391,-0.391 -1.023,-0.391 -1.414,0 l -0.5,0.5 -0.115,0.173 C 16.791,6.154 14.99,5.276 12.989,5.056 L 13,5 V 4 h 1 C 14.55,4 15,3.55 15,3 15,2.45 14.55,2 14,2 H 10 C 9.45,2 9,2.45 9,3 9,3.55 9.45,4 10,4 h 1 v 1 l 0.012,0.057 C 6.506,5.549 3,9.364 3,14 c 0,4.971 4.029,9 9,9 4.971,0 9,-4.029 9,-9 0,-1.894 -0.588,-3.648 -1.586,-5.098 z M 12,21 C 8.141,21 5,17.86 5,14 5,10.14 8.141,7 12,7 c 3.859,0 7,3.14 7,7 0,3.86 -3.141,7 -7,7 z m 1,-8 v -2 c 0,-0.55 -0.45,-1 -1,-1 -0.55,0 -1,0.45 -1,1 v 3 c 0,0.55 0.45,1 1,1 h 3 c 0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1 z M 12,8 c -3.312,0 -6,2.688 -6,6 0,3.312 2.688,6 6,6 3.312,0 6,-2.688 6,-6 0,-3.312 -2.688,-6 -6,-6 z m 0,11 C 9.243,19 7,16.757 7,14 7,11.243 9.243,9 12,9 c 2.757,0 5,2.243 5,5 0,2.757 -2.243,5 -5,5 z" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,31 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
version="1.2"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
id="svg896">
|
|
||||||
<metadata
|
|
||||||
id="metadata902">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs900" />
|
|
||||||
<path
|
|
||||||
d="M 19.576035,3.3487939 C 18.237131,2.6038813 16.550693,3.0884898 15.809935,4.4246248 L 10.66893,13.676495 7.726664,10.734229 c -1.0813694,-1.0813704 -2.8342677,-1.0813704 -3.9156371,0 -1.0813693,1.081369 -1.0813693,2.834267 0,3.915637 l 5.538383,5.538383 c 0.523378,0.524762 1.2295221,0.812758 1.9578191,0.812758 l 0.383533,-0.02769 c 0.859834,-0.12046 1.614439,-0.636914 2.03674,-1.397057 L 20.650482,7.1148966 C 21.39401,5.777375 20.91217,4.0923218 19.576035,3.3487939 Z"
|
|
||||||
id="path894"
|
|
||||||
style="stroke-width:1.3846;fill:#99cc99;fill-opacity:1" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -18,10 +18,10 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
|||||||
[Map]
|
[Map]
|
||||||
add = typ_plus.svg
|
add = typ_plus.svg
|
||||||
backward = typ_chevron-left.svg
|
backward = typ_chevron-left.svg
|
||||||
|
bookmark = typ_bookmark.svg
|
||||||
bullet-off = typ_media-record-outline.svg
|
bullet-off = typ_media-record-outline.svg
|
||||||
bullet-on = typ_media-record.svg
|
bullet-on = typ_media-record.svg
|
||||||
check = typ_tick.svg
|
checked = mixed_input-checked.svg
|
||||||
clear = typ_backspace.svg
|
|
||||||
close = typ_times.svg
|
close = typ_times.svg
|
||||||
cls_archive = typ_delete.svg
|
cls_archive = typ_delete.svg
|
||||||
cls_character = typ_user.svg
|
cls_character = typ_user.svg
|
||||||
@@ -35,32 +35,25 @@ cls_timeline = typ_calendar.svg
|
|||||||
cls_trash = typ_trash.svg
|
cls_trash = typ_trash.svg
|
||||||
cls_world = typ_location.svg
|
cls_world = typ_location.svg
|
||||||
cross = typ_times.svg
|
cross = typ_times.svg
|
||||||
delete = typ_delete.svg
|
|
||||||
doc_h0 = mixed_heading0.svg
|
|
||||||
doc_h1 = mixed_heading1.svg
|
|
||||||
doc_h2 = mixed_heading2.svg
|
|
||||||
doc_h3 = mixed_heading3.svg
|
|
||||||
doc_h4 = mixed_heading4.svg
|
|
||||||
done = typ_input-checked.svg
|
|
||||||
down = typ_chevron-down.svg
|
down = typ_chevron-down.svg
|
||||||
edit = typ_pencil.svg
|
edit = typ_pencil.svg
|
||||||
forward = typ_chevron-right.svg
|
forward = typ_chevron-right.svg
|
||||||
hash = typ_hash.svg
|
|
||||||
maximise = typ_arrow-maximise.svg
|
maximise = typ_arrow-maximise.svg
|
||||||
menu = typ_th-menu.svg
|
menu = typ_th-menu.svg
|
||||||
minimise = typ_arrow-minimise.svg
|
minimise = typ_arrow-minimise.svg
|
||||||
|
noncheckable = mixed_input-none.svg
|
||||||
proj_chapter = mixed_document-chapter.svg
|
proj_chapter = mixed_document-chapter.svg
|
||||||
proj_details = typ_th-list-grey.svg
|
proj_details = typ_th-list-grey.svg
|
||||||
proj_document = typ_document-text.svg
|
proj_document = typ_document-text.svg
|
||||||
proj_folder = typ_folder.svg
|
proj_folder = typ_folder.svg
|
||||||
proj_note = mixed_document-note.svg
|
proj_note = mixed_document-note.svg
|
||||||
proj_scene = mixed_document-scene.svg
|
proj_scene = mixed_document-scene.svg
|
||||||
|
proj_section = mixed_document-section.svg
|
||||||
proj_stats = typ_chart-bar-grey.svg
|
proj_stats = typ_chart-bar-grey.svg
|
||||||
proj_title = mixed_document-title.svg
|
proj_title = mixed_document-title.svg
|
||||||
reference = typ_at.svg
|
reference = typ_at.svg
|
||||||
refresh = typ_refresh.svg
|
refresh = typ_refresh.svg
|
||||||
remove = typ_minus.svg
|
remove = typ_minus.svg
|
||||||
save = typ_download.svg
|
|
||||||
search = typ_search.svg
|
search = typ_search.svg
|
||||||
search_cancel = typ_cancel-grey.svg
|
search_cancel = typ_cancel-grey.svg
|
||||||
search_case = nw_search-case.svg
|
search_case = nw_search-case.svg
|
||||||
@@ -78,13 +71,16 @@ status_stats = typ_chart-bar-grey.svg
|
|||||||
status_time = typ_stopwatch-grey.svg
|
status_time = typ_stopwatch-grey.svg
|
||||||
sticky-off = typ_pin-outline.svg
|
sticky-off = typ_pin-outline.svg
|
||||||
sticky-on = typ_pin.svg
|
sticky-on = typ_pin.svg
|
||||||
|
unchecked = mixed_input-unchecked.svg
|
||||||
up = typ_chevron-up.svg
|
up = typ_chevron-up.svg
|
||||||
view_build = typ_export.svg
|
view_build = typ_export.svg
|
||||||
view_editor = mixed_edit.svg
|
view_editor = mixed_edit.svg
|
||||||
view_novel = typ_book-grey.svg
|
view_novel = typ_book-grey.svg
|
||||||
view_outline = typ_puzzle-outline.svg
|
view_outline = typ_puzzle-outline.svg
|
||||||
|
|
||||||
|
deco_doc_h0 = nw_deco-h0.svg
|
||||||
deco_doc_h1 = nw_deco-h1.svg
|
deco_doc_h1 = nw_deco-h1.svg
|
||||||
deco_doc_h2 = nw_deco-h2.svg
|
deco_doc_h2 = nw_deco-h2.svg
|
||||||
deco_doc_h3 = nw_deco-h3.svg
|
deco_doc_h3 = nw_deco-h3.svg
|
||||||
deco_doc_h4 = nw_deco-h4.svg
|
deco_doc_h4 = nw_deco-h4.svg
|
||||||
|
deco_doc_more = nw_deco-noveltree-more.svg
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
version="1.2"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="svg2161">
|
||||||
|
<metadata
|
||||||
|
id="metadata2167">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs2165" />
|
||||||
|
<path
|
||||||
|
id="rect12986"
|
||||||
|
style="display:inline;opacity:0.95;fill:#ededed;fill-opacity:1;stroke:none;stroke-width:1.88635;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 5.8886719 3.4453125 C 5.2152275 3.4453125 4.6660156 3.9925712 4.6660156 4.6660156 L 4.6660156 19.333984 C 4.6660156 20.007428 5.2152275 20.554687 5.8886719 20.554688 L 18.111328 20.554688 C 18.784774 20.554688 19.333984 20.007428 19.333984 19.333984 L 19.333984 4.6660156 C 19.333984 3.9925712 18.784774 3.4453125 18.111328 3.4453125 L 5.8886719 3.4453125 z " />
|
||||||
|
<path
|
||||||
|
id="rect935"
|
||||||
|
style="fill:#787878;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
|
||||||
|
d="M 5.8886719 3.4453125 C 5.2152275 3.4453125 4.6660156 3.9925712 4.6660156 4.6660156 L 4.6660156 6 L 4.6660156 7.1894531 L 4.6660156 10.058594 L 19.333984 10.058594 L 19.333984 7.1894531 L 19.333984 6 L 19.333984 4.6660156 C 19.333984 3.9925712 18.784774 3.4453125 18.111328 3.4453125 L 5.8886719 3.4453125 z " />
|
||||||
|
<path
|
||||||
|
id="path2157"
|
||||||
|
style="display:inline;fill:#333333;fill-opacity:1;stroke-width:1.22222"
|
||||||
|
d="M 7.1113281,13.222656 C 6.7739949,13.222656 6.5,13.49665 6.5,13.833984 c 0,0.337332 0.2739949,0.611328 0.6113281,0.611328 h 9.7773439 c 0.337334,1e-6 0.611328,-0.273996 0.611328,-0.611328 0,-0.337334 -0.273994,-0.611328 -0.611328,-0.611328 z m 0,3.666016 C 6.7739949,16.888672 6.5,17.162666 6.5,17.5 c 0,0.337334 0.2739949,0.611328 0.6113281,0.611328 H 16.888672 C 17.226006,18.111328 17.5,17.837334 17.5,17.5 c 0,-0.337334 -0.273994,-0.611328 -0.611328,-0.611328 z" />
|
||||||
|
<path
|
||||||
|
id="path902"
|
||||||
|
style="display:inline;fill:#333333;fill-opacity:1;stroke-width:1.22222"
|
||||||
|
d="M 5.8886719,1 C 3.8671165,1 2.2226562,2.6444602 2.2226562,4.6660156 V 19.333984 C 2.2226563,21.355538 3.8671165,23 5.8886719,23 H 18.111328 c 2.021556,0 3.666016,-1.644462 3.666016,-3.666016 V 4.6660156 C 21.777344,2.6444602 20.132884,1 18.111328,1 Z m 0,2.4453125 H 18.111328 c 0.673446,0 1.222656,0.5472587 1.222656,1.2207031 V 19.333984 c 0,0.673444 -0.54921,1.220704 -1.222656,1.220704 H 5.8886719 c -0.6734444,-10e-7 -1.2226563,-0.54726 -1.2226563,-1.220704 V 4.6660156 c 0,-0.6734444 0.5492119,-1.2207031 1.2226563,-1.2207031 z" />
|
||||||
|
<path
|
||||||
|
id="path2157-0"
|
||||||
|
style="display:inline;fill:#ededed;fill-opacity:1;stroke-width:1.22222"
|
||||||
|
d="M 7.1113281 5.8886719 C 6.7739949 5.8886719 6.5 6.1626667 6.5 6.5 C 6.5 6.837334 6.7739949 7.1113281 7.1113281 7.1113281 L 16.888672 7.1113281 C 17.226006 7.1113281 17.5 6.837334 17.5 6.5 C 17.5 6.1626667 17.226006 5.8886719 16.888672 5.8886719 L 7.1113281 5.8886719 z " />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -1,31 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg1897"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
version="1.2">
|
|
||||||
<metadata
|
|
||||||
id="metadata1903">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs1901" />
|
|
||||||
<path
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
|
|
||||||
id="path1895"
|
|
||||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,37 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg1897"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
version="1.2">
|
|
||||||
<metadata
|
|
||||||
id="metadata1903">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs1901" />
|
|
||||||
<path
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
|
|
||||||
id="path1895"
|
|
||||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
|
||||||
<circle
|
|
||||||
r="2.2222221"
|
|
||||||
cy="4.2222223"
|
|
||||||
cx="19.470242"
|
|
||||||
id="path2467"
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,43 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg1897"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
version="1.2">
|
|
||||||
<metadata
|
|
||||||
id="metadata1903">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs1901" />
|
|
||||||
<path
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
|
|
||||||
id="path1895"
|
|
||||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
|
||||||
<circle
|
|
||||||
r="2.2222221"
|
|
||||||
cy="4.2222223"
|
|
||||||
cx="19.470242"
|
|
||||||
id="path2467"
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
<circle
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="circle2469"
|
|
||||||
cx="19.470242"
|
|
||||||
cy="9.4077778"
|
|
||||||
r="2.2222221" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,49 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg1897"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
version="1.2">
|
|
||||||
<metadata
|
|
||||||
id="metadata1903">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs1901" />
|
|
||||||
<path
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
|
|
||||||
id="path1895"
|
|
||||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
|
||||||
<circle
|
|
||||||
r="2.2222221"
|
|
||||||
cy="4.2222223"
|
|
||||||
cx="19.470242"
|
|
||||||
id="path2467"
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
<circle
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="circle2469"
|
|
||||||
cx="19.470242"
|
|
||||||
cy="9.4077778"
|
|
||||||
r="2.2222221" />
|
|
||||||
<circle
|
|
||||||
r="2.2222221"
|
|
||||||
cy="14.592222"
|
|
||||||
cx="19.470242"
|
|
||||||
id="circle2471"
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,55 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg1897"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
version="1.2">
|
|
||||||
<metadata
|
|
||||||
id="metadata1903">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs1901" />
|
|
||||||
<path
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke-width:1.42859697"
|
|
||||||
id="path1895"
|
|
||||||
d="m 3.1443377,2.8368008 c -1.1157342,1.1157342 -1.1157342,2.9243378 0,4.040072 L 8.2658581,11.999821 3.1443377,17.12277 c -1.1157342,1.115734 -1.1157342,2.924338 0,4.040072 C 3.7014904,21.721423 4.4329321,22 5.1643736,22 5.8958159,22 6.627257,21.721423 7.1844103,21.162842 L 16.348859,11.999821 7.1844103,2.8368008 c -1.1143067,-1.1157343 -2.9257671,-1.1157343 -4.0400726,0 z" />
|
|
||||||
<circle
|
|
||||||
r="2.2222221"
|
|
||||||
cy="4.2222223"
|
|
||||||
cx="19.470242"
|
|
||||||
id="path2467"
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
<circle
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="circle2469"
|
|
||||||
cx="19.470242"
|
|
||||||
cy="9.4077778"
|
|
||||||
r="2.2222221" />
|
|
||||||
<circle
|
|
||||||
r="2.2222221"
|
|
||||||
cy="14.592222"
|
|
||||||
cx="19.470242"
|
|
||||||
id="circle2471"
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
<circle
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.99999988;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="circle2473"
|
|
||||||
cx="19.470242"
|
|
||||||
cy="19.777779"
|
|
||||||
r="2.2222221" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
version="1.2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="svg879">
|
||||||
|
<defs
|
||||||
|
id="defs883" />
|
||||||
|
<metadata
|
||||||
|
id="metadata873">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<path
|
||||||
|
d="m17.714 22h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
|
||||||
|
fill="#333"
|
||||||
|
stroke-width="1.4286"
|
||||||
|
id="path875" />
|
||||||
|
<path
|
||||||
|
d="m19.02 3.3503c-0.60933 0.010623-1.2003 0.31545-1.5586 0.86719l-5.0977 7.8477-2.3672-3.6445c-0.57329-0.88278-1.7442-1.1319-2.627-0.55859-0.88278 0.57329-1.1319 1.7461-0.55859 2.6289l3.9199 6.0391c0.38514 0.59307 1.0409 0.88965 1.6973 0.85352 0.03023-2.78e-4 0.05972-0.0041 0.08984-0.0059 0.0556-0.0057 0.11074-0.0088 0.16602-0.01953 0.52104-0.07621 1.0077-0.36147 1.3184-0.83984l6.6445-10.23c0.57329-0.88278 0.32419-2.0556-0.55859-2.6289-0.33104-0.21498-0.70276-0.31497-1.0684-0.30859z"
|
||||||
|
fill="#718c00"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1.111"
|
||||||
|
id="path877" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
version="1.2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="svg973">
|
||||||
|
<defs
|
||||||
|
id="defs977" />
|
||||||
|
<metadata
|
||||||
|
id="metadata967">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<path
|
||||||
|
d="m2 17.714v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h11.429c2.3629 0 4.2857 1.9229 4.2857 4.2857v7.1429c0 0.79-0.64 1.4286-1.4286 1.4286-0.78857 0-1.4286-0.63857-1.4286-1.4286v-7.1429c0-0.78857-0.64143-1.4286-1.4286-1.4286h-11.429c-0.78714 0-1.4286 0.64-1.4286 1.4286v11.429c0 0.78857 0.64143 1.4286 1.4286 1.4286h4.2857c0.78857 0 1.4286 0.63857 1.4286 1.4286s-0.64 1.4286-1.4286 1.4286h-4.2857c-2.3629 0-4.2857-1.9229-4.2857-4.2857zm15.714 4.2857h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
|
||||||
|
fill="#333"
|
||||||
|
stroke-width="1.4286"
|
||||||
|
id="path969" />
|
||||||
|
<rect
|
||||||
|
x="7"
|
||||||
|
y="10.1"
|
||||||
|
width="10"
|
||||||
|
height="3.8"
|
||||||
|
rx="1.9"
|
||||||
|
ry="1.9"
|
||||||
|
fill="#f5871f"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1.111"
|
||||||
|
id="rect971" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
version="1.2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="svg1616">
|
||||||
|
<defs
|
||||||
|
id="defs1620" />
|
||||||
|
<metadata
|
||||||
|
id="metadata1610">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<path
|
||||||
|
d="m17.714 22h-11.429c-2.3629 0-4.2857-1.9229-4.2857-4.2857v-11.429c0-2.3629 1.9229-4.2857 4.2857-4.2857h7.1429c0.79 0 1.4286 0.64 1.4286 1.4286 0 0.78857-0.63857 1.4286-1.4286 1.4286h-7.1429c-0.78857 0-1.4286 0.64143-1.4286 1.4286v11.429c0 0.78714 0.64 1.4286 1.4286 1.4286h11.429c0.78857 0 1.4286-0.64143 1.4286-1.4286v-4.2857c0-0.78857 0.63857-1.4286 1.4286-1.4286s1.4286 0.64 1.4286 1.4286v4.2857c0 2.3629-1.9229 4.2857-4.2857 4.2857z"
|
||||||
|
fill="#333"
|
||||||
|
stroke-width="1.4286"
|
||||||
|
id="path1612" />
|
||||||
|
<path
|
||||||
|
d="m17.99 3.3379c-0.48501 0.025418-0.96034 0.23584-1.3125 0.62695l-4.6777 5.1953-1.332-1.4785c-0.70433-0.78223-1.9014-0.84495-2.6836-0.14062-0.78223 0.70433-0.84495 1.9014-0.14062 2.6836l1.5996 1.7754-1.5977 1.7754c-0.70433 0.78223-0.64161 1.9793 0.14062 2.6836 0.78223 0.70433 1.9773 0.64161 2.6816-0.14062l1.332-1.4785 1.332 1.4785c0.70433 0.78224 1.9014 0.84495 2.6836 0.14062 0.78223-0.70433 0.84495-1.9014 0.14062-2.6836l-1.5996-1.7754 4.9453-5.4922c0.70433-0.78223 0.64161-1.9793-0.14062-2.6836-0.39112-0.35216-0.88608-0.51175-1.3711-0.48633z"
|
||||||
|
fill="#c82829"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="1.111"
|
||||||
|
id="path1614" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
version="1.2"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="svg2161"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<metadata
|
||||||
|
id="metadata2167">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs2165" />
|
||||||
|
<path
|
||||||
|
d="M 9.7,18.2 16,12 9.7,5.8 C 9.5,5.6 9.2,5.5 9,5.5 8.8,5.5 8.5,5.6 8.3,5.8 8.1,6 8,6.2 8,6.5 v 11 c 0,0.3 0.1,0.5 0.3,0.7 0.2,0.2 0.5,0.3 0.7,0.3 0.2,0 0.5,-0.1 0.7,-0.3 z"
|
||||||
|
id="path3096"
|
||||||
|
style="fill:#848484;fill-opacity:1" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 928 B |
@@ -1,31 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg13156"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
version="1.2">
|
|
||||||
<metadata
|
|
||||||
id="metadata13162">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs13160" />
|
|
||||||
<path
|
|
||||||
style="stroke-width:1.00151;fill:#c82829;fill-opacity:1"
|
|
||||||
id="path13154"
|
|
||||||
d="M 19.496214,4.4886395 H 9.4810665 c -1.2679176,0 -2.8382927,0.8082224 -3.5754075,1.8397825 L 3.2917056,9.9869555 2.0908895,11.668499 c -0.1211833,0.175265 -0.1221848,0.492744 0.003,0.665006 l 1.1897995,1.66652 2.622967,3.672554 c 0.7361133,1.030558 2.3054869,1.838782 3.574406,1.838782 H 19.496214 C 20.877302,19.511361 22,18.387661 22,17.007573 V 6.9924263 C 22,5.612339 20.877302,4.4886395 19.496214,4.4886395 Z M 17.19974,14.296473 c 0.391593,0.391592 0.391593,1.02455 0,1.416141 -0.195295,0.195296 -0.451683,0.293445 -0.70807,0.293445 -0.256388,0 -0.512776,-0.09815 -0.708071,-0.293445 l -2.296474,-2.296473 -2.296473,2.296473 c -0.195295,0.195296 -0.451683,0.293445 -0.708071,0.293445 -0.256388,0 -0.512775,-0.09815 -0.7080706,-0.293445 -0.3915923,-0.391591 -0.3915923,-1.024549 0,-1.416141 L 12.070984,12 9.7745104,9.7035265 c -0.3915923,-0.3915921 -0.3915923,-1.0245494 0,-1.4161417 0.3915926,-0.3915922 1.0245496,-0.3915922 1.4161416,0 l 2.296473,2.2964732 2.296474,-2.2964732 c 0.391592,-0.3915922 1.024549,-0.3915922 1.416141,0 0.391593,0.3915923 0.391593,1.0245496 0,1.4161417 L 14.903267,12 Z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -5,13 +5,13 @@
|
|||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
id="svg2608"
|
version="1.2"
|
||||||
viewBox="0 0 24 24"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
width="24"
|
||||||
version="1.2">
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
id="svg901">
|
||||||
<metadata
|
<metadata
|
||||||
id="metadata2614">
|
id="metadata907">
|
||||||
<rdf:RDF>
|
<rdf:RDF>
|
||||||
<cc:Work
|
<cc:Work
|
||||||
rdf:about="">
|
rdf:about="">
|
||||||
@@ -23,9 +23,9 @@
|
|||||||
</rdf:RDF>
|
</rdf:RDF>
|
||||||
</metadata>
|
</metadata>
|
||||||
<defs
|
<defs
|
||||||
id="defs2612" />
|
id="defs905" />
|
||||||
<path
|
<path
|
||||||
style="stroke-width:1.05081;fill:#4271ae;fill-opacity:1"
|
d="M 16.203226,2 H 7.796774 C 6.05874,2 4.6443545,3.4143855 4.6443545,5.1524195 V 19.86371 c 0,0.540115 0.1092839,0.994063 0.3236484,1.350287 0.5926549,0.982504 1.9072138,1.059213 2.9559187,0.0084 L 11.25708,17.889239 c 0.394052,-0.393002 1.091787,-0.393002 1.485841,0 l 3.333158,3.333158 C 16.592024,21.738349 17.128987,22 17.673304,22 c 0.837492,0 1.682342,-0.660957 1.682342,-2.13629 V 5.1524195 C 19.355646,3.4143855 17.941259,2 16.203226,2 Z M 7.796774,4.101613 h 8.406452 c 0.578994,0 1.050806,0.4718121 1.050806,1.0508065 V 15.560658 l -2.575527,-2.361162 c -1.477434,-1.35449 -3.880628,-1.353439 -5.3580615,0 l -2.574476,2.361162 V 5.1524195 c 0,-0.5789944 0.4718121,-1.0508065 1.0508065,-1.0508065 z m 6.431987,12.301792 C 13.635055,15.809699 12.843798,15.482898 12,15.482898 c -0.843797,0 -1.635054,0.327852 -2.2287605,0.920507 l -3.025272,3.025272 v -2.442075 l 3.2848215,-3.011611 c 1.085483,-0.995114 2.851889,-0.995114 3.937372,0 l 3.285871,3.011611 v 2.442075 z"
|
||||||
id="path2606"
|
id="path899"
|
||||||
d="M 16.203226,2 H 7.796774 C 6.05874,2 4.6443545,3.4143855 4.6443545,5.1524195 V 19.86371 c 0,0.540115 0.1092839,0.994063 0.3236484,1.350287 0.5926549,0.982504 1.9072138,1.059213 2.9559187,0.0084 L 11.25708,17.889239 c 0.394052,-0.393002 1.091787,-0.393002 1.485841,0 l 3.333158,3.333158 C 16.592024,21.738349 17.128987,22 17.673304,22 c 0.837492,0 1.682342,-0.660957 1.682342,-2.13629 V 5.1524195 C 19.355646,3.4143855 17.941259,2 16.203226,2 Z M 7.796774,4.101613 h 8.406452 c 0.578994,0 1.050806,0.4718121 1.050806,1.0508065 V 15.560658 l -2.575527,-2.361162 c -1.477434,-1.35449 -3.880628,-1.353439 -5.3580615,0 l -2.574476,2.361162 V 5.1524195 c 0,-0.5789944 0.4718121,-1.0508065 1.0508065,-1.0508065 z m 6.431987,12.301792 C 13.635055,15.809699 12.843798,15.482898 12,15.482898 c -0.843797,0 -1.635054,0.327852 -2.2287605,0.920507 l -3.025272,3.025272 v -2.442075 l 3.2848215,-3.011611 c 1.085483,-0.995114 2.851889,-0.995114 3.937372,0 l 3.285871,3.011611 v 2.442075 z" />
|
style="fill:#4271ae;fill-opacity:1;stroke-width:1.05081" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,35 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
version="1.2"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
id="svg13801">
|
|
||||||
<metadata
|
|
||||||
id="metadata13807">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs13805" />
|
|
||||||
<path
|
|
||||||
style="display:inline;fill:#a2694f;fill-opacity:1"
|
|
||||||
id="path6288"
|
|
||||||
d="m 20.987,16 c 0,-0.105 -0.004,-0.211 -0.039,-0.316 l -2,-6 C 18.812,9.275 18.431,9 18,9 h -0.219 c -0.094,0.188 -0.21,0.368 -0.367,0.525 L 15.932,11 h 1.348 l 1.667,5 H 5.054 L 6.721,11 H 8.069 L 6.586,9.525 C 6.429,9.368 6.312,9.188 6.219,9 H 6 C 5.569,9 5.188,9.275 5.052,9.684 l -2,6 C 3.017,15.789 3.013,15.895 3.013,16 3,16 3,21 3,21 c 0,0.553 0.447,1 1,1 h 16 c 0.553,0 1,-0.447 1,-1 0,0 0,-5 -0.013,-5 z" />
|
|
||||||
<path
|
|
||||||
d="M 16.707,7.404 C 16.518,7.216 16.259,7.121 16,7.121 c -0.259,0 -0.518,0.095 -0.707,0.283 L 13,9.697 V 3 C 13,2.448 12.552,2 12,2 11.448,2 11,2.448 11,3 V 9.697 L 8.707,7.404 C 8.518,7.216 8.267,7.111 8,7.111 c -0.267,0 -0.518,0.105 -0.707,0.293 -0.39,0.39 -0.39,1.024 0,1.414 L 12,13.5 16.709,8.816 C 17.097,8.429 17.097,7.794 16.707,7.404 Z"
|
|
||||||
id="path13799"
|
|
||||||
style="display:inline;fill:#718c00;fill-opacity:1" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,31 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
id="svg3369"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
height="24"
|
|
||||||
width="24"
|
|
||||||
version="1.2">
|
|
||||||
<metadata
|
|
||||||
id="metadata3375">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs3373" />
|
|
||||||
<path
|
|
||||||
style="fill:#333333;fill-opacity:1;stroke-width:1"
|
|
||||||
id="path3367"
|
|
||||||
d="m 10.828125,20 h -4 c -0.553,0 -1,0.447 -1,1 0,0.553 0.447,1 1,1 h 10 c 0.553,0 1,-0.447 1,-1 0,-0.553 -0.447,-1 -1,-1 h -4 v -1.23 c 1.64,-0.371 3.146,-1.188 4.363,-2.406 1.7,-1.7 2.637,-3.96 2.637,-6.364 0,-2.067 -0.692,-4.029 -1.968,-5.619 l 0.675,-0.673 c 0.391,-0.391 0.391,-1.023 10e-4,-1.415 -0.391,-0.391 -1.024,-0.39 -1.415,-0.001 l -2.052,2.049 0.708,0.708 c 1.322,1.322 2.051,3.081 2.051,4.951 0,1.87 -0.729,3.627 -2.051,4.949 -1.322,1.322 -3.079,2.051 -4.949,2.051 -1.87,0 -3.627,-0.729 -4.949,-2.051 -0.391,-0.391 -1.023,-0.391 -1.414,0 -0.391,0.39 -0.391,1.023 0,1.414 1.699,1.7 3.959,2.637 6.363,2.637 z m 0,-16 c 1.657,0 3.157,0.672 4.243,1.757 1.085,1.086 1.757,2.586 1.757,4.243 0,1.656 -0.672,3.156 -1.757,4.242 -1.086,1.086 -2.586,1.758 -4.243,1.758 -1.658,0 -3.157,-0.672 -4.242,-1.757 -1.085,-1.086 -1.756,-2.586 -1.756,-4.243 0,-1.657 0.671,-3.157 1.756,-4.243 1.085,-1.085 2.584,-1.757 4.242,-1.757 z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,31 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
version="1.2"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
id="svg5204">
|
|
||||||
<metadata
|
|
||||||
id="metadata5210">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs5208" />
|
|
||||||
<path
|
|
||||||
id="rect928"
|
|
||||||
d="M 7.75 2 C 6.7805 2 6 2.7805 6 3.75 L 6 5.5 L 4.1621094 5.5 C 3.2410844 5.5 2.5 6.2805 2.5 7.25 C 2.5 8.2195 3.2410844 9 4.1621094 9 L 6 9 L 6 15 L 4.1621094 15 C 3.2410844 15 2.5 15.7805 2.5 16.75 C 2.5 17.7195 3.2410844 18.5 4.1621094 18.5 L 6 18.5 L 6 20.25 C 6 21.2195 6.7805 22 7.75 22 C 8.7195 22 9.5 21.2195 9.5 20.25 L 9.5 18.5 L 14.5 18.5 L 14.5 20.25 C 14.5 21.2195 15.2805 22 16.25 22 C 17.2195 22 18 21.2195 18 20.25 L 18 18.5 L 19.837891 18.5 C 20.758916 18.5 21.5 17.7195 21.5 16.75 C 21.5 15.7805 20.758916 15 19.837891 15 L 18 15 L 18 9 L 19.837891 9 C 20.758916 9 21.5 8.2195 21.5 7.25 C 21.5 6.2805 20.758916 5.5 19.837891 5.5 L 18 5.5 L 18 3.75 C 18 2.7805 17.2195 2 16.25 2 C 15.2805 2 14.5 2.7805 14.5 3.75 L 14.5 5.5 L 9.5 5.5 L 9.5 3.75 C 9.5 2.7805 8.7195 2 7.75 2 z M 9.5 9 L 14.5 9 L 14.5 15 L 9.5 15 L 9.5 9 z "
|
|
||||||
style="fill:#4271ae;fill-opacity:1;stroke:none;stroke-width:1.7544229;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,35 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
version="1.2"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
id="svg11367">
|
|
||||||
<metadata
|
|
||||||
id="metadata11373">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs11371" />
|
|
||||||
<path
|
|
||||||
d="m 13.499143,15.642429 c -0.45,0 -0.885857,-0.178715 -1.209857,-0.502715 L 8.859,11.710714 c -0.668571,-0.669857 -0.668571,-1.755 0,-2.423571 0.669857,-0.669857 1.755,-0.669857 2.426143,0 L 13.105714,11.109 17.573571,4.0954286 c 0.459,-0.828 1.504286,-1.1275715 2.332286,-0.6672857 0.826714,0.4602857 1.125,1.5042857 0.666,2.331 l -5.572286,9.0000001 c -0.261,0.470571 -0.727714,0.790714 -1.26,0.865286 z"
|
|
||||||
id="path11365"
|
|
||||||
style="display:inline;fill:#718c00;fill-opacity:1;stroke-width:1.28570998" />
|
|
||||||
<path
|
|
||||||
style="display:inline;fill:#333333;fill-opacity:1;stroke-width:1.28570998"
|
|
||||||
id="path16196"
|
|
||||||
d="M 17.142857,21 H 6.8571429 C 4.7305714,21 3,19.269429 3,17.142857 V 6.8571429 C 3,4.7305714 4.7305714,3 6.8571429,3 h 6.4285711 c 0.711,0 1.285715,0.576 1.285715,1.2857143 0,0.7097143 -0.574715,1.2857143 -1.285715,1.2857143 H 6.8571429 c -0.7097143,0 -1.2857143,0.5772857 -1.2857143,1.2857143 V 17.142857 c 0,0.708429 0.576,1.285714 1.2857143,1.285714 H 17.142857 c 0.709714,0 1.285714,-0.577285 1.285714,-1.285714 V 13.285714 C 18.428571,12.576 19.003286,12 19.714286,12 20.425286,12 21,12.576 21,13.285714 v 3.857143 C 21,19.269429 19.269429,21 17.142857,21 Z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
@@ -1,36 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
version="1.2"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
id="svg7030">
|
|
||||||
<metadata
|
|
||||||
id="metadata7036">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs7034" />
|
|
||||||
<g
|
|
||||||
id="g7028"
|
|
||||||
transform="matrix(0.95238095,0,0,0.95238095,0.5714286,0.09523812)"
|
|
||||||
style="fill:#333333;fill-opacity:1">
|
|
||||||
<path
|
|
||||||
d="M 19.414,8.902 C 19.518,8.854 19.62,8.794 19.707,8.707 l 0.5,-0.5 c 0.391,-0.391 0.391,-1.023 0,-1.414 -0.391,-0.391 -1.023,-0.391 -1.414,0 l -0.5,0.5 -0.115,0.173 C 16.791,6.154 14.99,5.276 12.989,5.056 L 13,5 V 4 h 1 C 14.55,4 15,3.55 15,3 15,2.45 14.55,2 14,2 H 10 C 9.45,2 9,2.45 9,3 9,3.55 9.45,4 10,4 h 1 v 1 l 0.012,0.057 C 6.506,5.549 3,9.364 3,14 c 0,4.971 4.029,9 9,9 4.971,0 9,-4.029 9,-9 0,-1.894 -0.588,-3.648 -1.586,-5.098 z M 12,21 C 8.141,21 5,17.86 5,14 5,10.14 8.141,7 12,7 c 3.859,0 7,3.14 7,7 0,3.86 -3.141,7 -7,7 z m 1,-8 v -2 c 0,-0.55 -0.45,-1 -1,-1 -0.55,0 -1,0.45 -1,1 v 3 c 0,0.55 0.45,1 1,1 h 3 c 0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1 z M 12,8 c -3.312,0 -6,2.688 -6,6 0,3.312 2.688,6 6,6 3.312,0 6,-2.688 6,-6 0,-3.312 -2.688,-6 -6,-6 z m 0,11 C 9.243,19 7,16.757 7,14 7,11.243 9.243,9 12,9 c 2.757,0 5,2.243 5,5 0,2.757 -2.243,5 -5,5 z"
|
|
||||||
id="path7026"
|
|
||||||
style="fill:#333333;fill-opacity:1" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,31 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
version="1.2"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
id="svg896">
|
|
||||||
<metadata
|
|
||||||
id="metadata902">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs900" />
|
|
||||||
<path
|
|
||||||
d="M 19.576035,3.3487939 C 18.237131,2.6038813 16.550693,3.0884898 15.809935,4.4246248 L 10.66893,13.676495 7.726664,10.734229 c -1.0813694,-1.0813704 -2.8342677,-1.0813704 -3.9156371,0 -1.0813693,1.081369 -1.0813693,2.834267 0,3.915637 l 5.538383,5.538383 c 0.523378,0.524762 1.2295221,0.812758 1.9578191,0.812758 l 0.383533,-0.02769 c 0.859834,-0.12046 1.614439,-0.636914 2.03674,-1.397057 L 20.650482,7.1148966 C 21.39401,5.777375 20.91217,4.0923218 19.576035,3.3487939 Z"
|
|
||||||
id="path894"
|
|
||||||
style="stroke-width:1.3846;fill:#718c00;fill-opacity:1" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -2,11 +2,11 @@
|
|||||||
<html>
|
<html>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<h2>Release Notes for 1.7 Beta 1</h2>
|
<h2>Release Notes for 2.0 RC 2</h2>
|
||||||
<p><i>Released on 17 May 2022</i></p>
|
<p><i>Released on 13 November 2022</i></p>
|
||||||
|
|
||||||
<p>This is a beta release of the next release version, and is intended for testing purposes. Please
|
<p>This is a release candidate of the next release version, and is intended for testing purposes.
|
||||||
be careful when using this version on live writing projects, and make sure you take frequent
|
Please be careful when using this version on live writing projects, and make sure you take frequent
|
||||||
backups.</p>
|
backups.</p>
|
||||||
<p>Please check the changelog for an overview of changes. The full release notes will be added to
|
<p>Please check the changelog for an overview of changes. The full release notes will be added to
|
||||||
the final release.</p>
|
the final release.</p>
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
[Main]
|
[Main]
|
||||||
name = Default System Theme
|
name = Default Theme
|
||||||
|
description = Qt standard colours
|
||||||
|
icontheme = typicons_light
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
[Main]
|
[Main]
|
||||||
name = Default Dark Theme
|
name = Default Dark Theme
|
||||||
author = Veronica Berglyd Olsen
|
description = The novelWriter standard dark theme
|
||||||
credit = Veronica Berglyd Olsen
|
author = Veronica Berglyd Olsen
|
||||||
url = https://github.com/vkbo/novelWriter
|
credit = Veronica Berglyd Olsen
|
||||||
license = CC BY-SA 4.0
|
url = https://github.com/vkbo/novelWriter
|
||||||
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
license = CC BY-SA 4.0
|
||||||
|
licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
|
||||||
|
icontheme = typicons_dark
|
||||||
|
|
||||||
[Palette]
|
[Palette]
|
||||||
window = 54, 54, 54
|
window = 54, 54, 54
|
||||||
windowtext = 174, 174, 174
|
windowtext = 174, 174, 174
|
||||||
base = 62, 62, 62
|
base = 62, 62, 62
|
||||||
alternatebase = 67, 67, 67
|
alternatebase = 78, 78, 78
|
||||||
text = 174, 174, 174
|
text = 174, 174, 174
|
||||||
tooltipbase = 255, 255, 192
|
tooltipbase = 255, 255, 192
|
||||||
tooltiptext = 21, 21, 13
|
tooltiptext = 21, 21, 13
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
/**
|
|
||||||
* Default Theme: Dark
|
|
||||||
* This theme doesn't use any custom styles, so the file is only here as
|
|
||||||
* an example. There doesn't have to be a styles.qss file in the folder.
|
|
||||||
*/
|
|
||||||
@@ -5,12 +5,13 @@ credit = Ethan Schoonover
|
|||||||
url = https://ethanschoonover.com/solarized/
|
url = https://ethanschoonover.com/solarized/
|
||||||
license = MIT
|
license = MIT
|
||||||
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
|
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
|
||||||
|
icontheme = typicons_dark
|
||||||
|
|
||||||
[Palette]
|
[Palette]
|
||||||
window = 0, 43, 54
|
window = 0, 43, 54
|
||||||
windowtext = 253, 246, 227
|
windowtext = 253, 246, 227
|
||||||
base = 7, 54, 66
|
base = 7, 54, 66
|
||||||
alternatebase = 67, 67, 67
|
alternatebase = 0, 43, 54
|
||||||
text = 253, 246, 227
|
text = 253, 246, 227
|
||||||
tooltipbase = 133, 153, 0
|
tooltipbase = 133, 153, 0
|
||||||
tooltiptext = 0, 43, 54
|
tooltiptext = 0, 43, 54
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ credit = Ethan Schoonover
|
|||||||
url = https://ethanschoonover.com/solarized/
|
url = https://ethanschoonover.com/solarized/
|
||||||
license = MIT
|
license = MIT
|
||||||
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
|
licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
|
||||||
|
icontheme = typicons_light
|
||||||
|
|
||||||
[Palette]
|
[Palette]
|
||||||
window = 238, 232, 213
|
window = 238, 232, 213
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
|
||||||
@@ -45,52 +46,55 @@ logger = logging.getLogger(__name__)
|
|||||||
# Checker Functions
|
# Checker Functions
|
||||||
# =============================================================================================== #
|
# =============================================================================================== #
|
||||||
|
|
||||||
def checkString(value, default, allowNone=False):
|
def checkStringNone(value, default):
|
||||||
"""Check if a variable is a string or a None.
|
"""Check if a variable is a string or a None.
|
||||||
"""
|
"""
|
||||||
if allowNone and (value is None or value == "None"):
|
if value is None or value == "None":
|
||||||
return None
|
return None
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
return str(value)
|
return str(value)
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def checkInt(value, default, allowNone=False):
|
def checkString(value, default):
|
||||||
"""Check if a variable is an integer or a None.
|
"""Check if a variable is a string.
|
||||||
|
"""
|
||||||
|
if isinstance(value, str):
|
||||||
|
return str(value)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def checkInt(value, default):
|
||||||
|
"""Check if a variable is an integer.
|
||||||
"""
|
"""
|
||||||
if allowNone and (value is None or value == "None"):
|
|
||||||
return None
|
|
||||||
try:
|
try:
|
||||||
return int(value)
|
return int(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def checkFloat(value, default, allowNone=False):
|
def checkFloat(value, default):
|
||||||
"""Check if a variable is a float or a None.
|
"""Check if a variable is a float.
|
||||||
"""
|
"""
|
||||||
if allowNone and (value is None or value == "None"):
|
|
||||||
return None
|
|
||||||
try:
|
try:
|
||||||
return float(value)
|
return float(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def checkBool(value, default, allowNone=False):
|
def checkBool(value, default):
|
||||||
"""Check if a variable is a boolean or a None.
|
"""Check if a variable is a boolean.
|
||||||
"""
|
"""
|
||||||
if allowNone and (value is None or value == "None"):
|
if isinstance(value, bool):
|
||||||
return None
|
return value
|
||||||
|
elif isinstance(value, str):
|
||||||
if isinstance(value, str):
|
check = value.lower()
|
||||||
if value == "True":
|
if check in ("true", "yes", "on"):
|
||||||
return True
|
return True
|
||||||
elif value == "False":
|
elif check in ("false", "no", "off"):
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
elif isinstance(value, int):
|
elif isinstance(value, int):
|
||||||
if value == 1:
|
if value == 1:
|
||||||
return True
|
return True
|
||||||
@@ -98,7 +102,6 @@ def checkBool(value, default, allowNone=False):
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return default
|
return default
|
||||||
|
|
||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
@@ -112,6 +115,26 @@ def checkHandle(value, default, allowNone=False):
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def checkUuid(value, default):
|
||||||
|
"""Try to process a value as an uuid, or return a default.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return str(uuid.UUID(value))
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def checkPath(value, default):
|
||||||
|
"""Check if a value is a valid path. Non-empty strings are accepted.
|
||||||
|
"""
|
||||||
|
if isinstance(value, Path):
|
||||||
|
return value
|
||||||
|
elif isinstance(value, str):
|
||||||
|
if value.strip():
|
||||||
|
return Path(value)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================================== #
|
# =============================================================================================== #
|
||||||
# Validator Functions
|
# Validator Functions
|
||||||
# =============================================================================================== #
|
# =============================================================================================== #
|
||||||
@@ -174,16 +197,6 @@ def hexToInt(value, default=0):
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def checkIntRange(value, first, last, default):
|
|
||||||
"""Check that an int is in a given range. If it isn't, return the
|
|
||||||
default value.
|
|
||||||
"""
|
|
||||||
if isinstance(value, int):
|
|
||||||
if value >= first and value <= last:
|
|
||||||
return value
|
|
||||||
return default
|
|
||||||
|
|
||||||
|
|
||||||
def minmax(value, minVal, maxVal):
|
def minmax(value, minVal, maxVal):
|
||||||
"""Make sure an integer is between min and max value (inclusive).
|
"""Make sure an integer is between min and max value (inclusive).
|
||||||
"""
|
"""
|
||||||
@@ -225,25 +238,25 @@ def formatInt(value):
|
|||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
def formatTimeStamp(theTime, fileSafe=False):
|
def formatTimeStamp(value, fileSafe=False):
|
||||||
"""Take a number (on the format returned by time.time()) and convert
|
"""Take a number (on the format returned by time.time()) and convert
|
||||||
it to a timestamp string.
|
it to a timestamp string.
|
||||||
"""
|
"""
|
||||||
if fileSafe:
|
if fileSafe:
|
||||||
return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_FSTAMP)
|
return datetime.fromtimestamp(value).strftime(nwConst.FMT_FSTAMP)
|
||||||
else:
|
else:
|
||||||
return datetime.fromtimestamp(theTime).strftime(nwConst.FMT_TSTAMP)
|
return datetime.fromtimestamp(value).strftime(nwConst.FMT_TSTAMP)
|
||||||
|
|
||||||
|
|
||||||
def formatTime(tS):
|
def formatTime(t):
|
||||||
"""Format a time in seconds in HH:MM:SS format or d-HH:MM:SS format
|
"""Format a time in seconds in HH:MM:SS format or d-HH:MM:SS format
|
||||||
if a full day or longer.
|
if a full day or longer.
|
||||||
"""
|
"""
|
||||||
if isinstance(tS, int):
|
if isinstance(t, int):
|
||||||
if tS >= 86400:
|
if t >= 86400:
|
||||||
return f"{tS//86400:d}-{tS%86400//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}"
|
return f"{t//86400:d}-{t%86400//3600:02d}:{t%3600//60:02d}:{t%60:02d}"
|
||||||
else:
|
else:
|
||||||
return f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}"
|
return f"{t//3600:02d}:{t%3600//60:02d}:{t%60:02d}"
|
||||||
return "ERROR"
|
return "ERROR"
|
||||||
|
|
||||||
|
|
||||||
@@ -258,12 +271,18 @@ def simplified(string):
|
|||||||
return " ".join(str(string).strip().split())
|
return " ".join(str(string).strip().split())
|
||||||
|
|
||||||
|
|
||||||
|
def yesNo(value):
|
||||||
|
"""Convert a boolean evaluated variable to a yes or no.
|
||||||
|
"""
|
||||||
|
return "yes" if value else "no"
|
||||||
|
|
||||||
|
|
||||||
def splitVersionNumber(value):
|
def splitVersionNumber(value):
|
||||||
"""Split a version string on the form aa.bb.cc into major, minor
|
"""Split a version string on the form aa.bb.cc into major, minor
|
||||||
and patch, and computes an integer value aabbcc.
|
and patch, and computes an integer value aabbcc.
|
||||||
"""
|
"""
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
return [0, 0, 0, 0]
|
return 0, 0, 0, 0
|
||||||
|
|
||||||
vMajor = 0
|
vMajor = 0
|
||||||
vMinor = 0
|
vMinor = 0
|
||||||
@@ -282,114 +301,114 @@ def splitVersionNumber(value):
|
|||||||
|
|
||||||
vInt = vMajor*10000 + vMinor*100 + vPatch
|
vInt = vMajor*10000 + vMinor*100 + vPatch
|
||||||
|
|
||||||
return [vMajor, vMinor, vPatch, vInt]
|
return vMajor, vMinor, vPatch, vInt
|
||||||
|
|
||||||
|
|
||||||
def transferCase(theSource, theTarget):
|
def transferCase(source, target):
|
||||||
"""Transfers the case of the source word to the target word. This
|
"""Transfers the case of the source word to the target word. This
|
||||||
will consider all upper or lower, and first char capitalisation.
|
will consider all upper or lower, and first char capitalisation.
|
||||||
"""
|
"""
|
||||||
theResult = theTarget
|
theResult = target
|
||||||
|
|
||||||
if not isinstance(theSource, str) or not isinstance(theTarget, str):
|
if not isinstance(source, str) or not isinstance(target, str):
|
||||||
return theResult
|
return theResult
|
||||||
if len(theTarget) < 1 or len(theSource) < 1:
|
if len(target) < 1 or len(source) < 1:
|
||||||
return theResult
|
return theResult
|
||||||
|
|
||||||
if theSource.istitle():
|
if source.istitle():
|
||||||
theResult = theTarget.title()
|
theResult = target.title()
|
||||||
|
|
||||||
if theSource.isupper():
|
if source.isupper():
|
||||||
theResult = theTarget.upper()
|
theResult = target.upper()
|
||||||
elif theSource.islower():
|
elif source.islower():
|
||||||
theResult = theTarget.lower()
|
theResult = target.lower()
|
||||||
|
|
||||||
return theResult
|
return theResult
|
||||||
|
|
||||||
|
|
||||||
def fuzzyTime(secDiff):
|
def fuzzyTime(seconds):
|
||||||
"""Converts a time difference in seconds into a fuzzy time string.
|
"""Converts a time difference in seconds into a fuzzy time string.
|
||||||
"""
|
"""
|
||||||
if secDiff < 0:
|
if seconds < 0:
|
||||||
return QCoreApplication.translate(
|
return QCoreApplication.translate(
|
||||||
"Common", "in the future"
|
"Common", "in the future"
|
||||||
)
|
)
|
||||||
elif secDiff < 30:
|
elif seconds < 30:
|
||||||
return QCoreApplication.translate(
|
return QCoreApplication.translate(
|
||||||
"Common", "just now"
|
"Common", "just now"
|
||||||
)
|
)
|
||||||
elif secDiff < 90:
|
elif seconds < 90:
|
||||||
return QCoreApplication.translate(
|
return QCoreApplication.translate(
|
||||||
"Common", "a minute ago"
|
"Common", "a minute ago"
|
||||||
)
|
)
|
||||||
elif secDiff < 3300: # 55 minutes
|
elif seconds < 3300: # 55 minutes
|
||||||
return QCoreApplication.translate(
|
return QCoreApplication.translate(
|
||||||
"Common", "{0} minutes ago"
|
"Common", "{0} minutes ago"
|
||||||
).format(int(round(secDiff/60)))
|
).format(int(round(seconds/60)))
|
||||||
elif secDiff < 5400: # 90 minutes
|
elif seconds < 5400: # 90 minutes
|
||||||
return QCoreApplication.translate(
|
return QCoreApplication.translate(
|
||||||
"Common", "an hour ago"
|
"Common", "an hour ago"
|
||||||
)
|
)
|
||||||
elif secDiff < 84600: # 23.5 hours
|
elif seconds < 84600: # 23.5 hours
|
||||||
return QCoreApplication.translate(
|
return QCoreApplication.translate(
|
||||||
"Common", "{0} hours ago"
|
"Common", "{0} hours ago"
|
||||||
).format(int(round(secDiff/3600)))
|
).format(int(round(seconds/3600)))
|
||||||
elif secDiff < 129600: # 1.5 days
|
elif seconds < 129600: # 1.5 days
|
||||||
return QCoreApplication.translate(
|
return QCoreApplication.translate(
|
||||||
"Common", "a day ago"
|
"Common", "a day ago"
|
||||||
)
|
)
|
||||||
elif secDiff < 561600: # 6.5 days
|
elif seconds < 561600: # 6.5 days
|
||||||
return QCoreApplication.translate(
|
return QCoreApplication.translate(
|
||||||
"Common", "{0} days ago"
|
"Common", "{0} days ago"
|
||||||
).format(int(round(secDiff/86400)))
|
).format(int(round(seconds/86400)))
|
||||||
elif secDiff < 907200: # 10.5 days
|
elif seconds < 907200: # 10.5 days
|
||||||
return QCoreApplication.translate(
|
return QCoreApplication.translate(
|
||||||
"Common", "a week ago"
|
"Common", "a week ago"
|
||||||
)
|
)
|
||||||
elif secDiff < 2419200: # 28 days
|
elif seconds < 2419200: # 28 days
|
||||||
return QCoreApplication.translate(
|
return QCoreApplication.translate(
|
||||||
"Common", "{0} weeks ago"
|
"Common", "{0} weeks ago"
|
||||||
).format(int(round(secDiff/604800)))
|
).format(int(round(seconds/604800)))
|
||||||
elif secDiff < 3888000: # 45 days
|
elif seconds < 3888000: # 45 days
|
||||||
return QCoreApplication.translate(
|
return QCoreApplication.translate(
|
||||||
"Common", "a month ago"
|
"Common", "a month ago"
|
||||||
)
|
)
|
||||||
elif secDiff < 29808000: # 345 days
|
elif seconds < 29808000: # 345 days
|
||||||
return QCoreApplication.translate(
|
return QCoreApplication.translate(
|
||||||
"Common", "{0} months ago"
|
"Common", "{0} months ago"
|
||||||
).format(int(round(secDiff/2592000)))
|
).format(int(round(seconds/2592000)))
|
||||||
elif secDiff < 47336400: # 1.5 years
|
elif seconds < 47336400: # 1.5 years
|
||||||
return QCoreApplication.translate(
|
return QCoreApplication.translate(
|
||||||
"Common", "a year ago"
|
"Common", "a year ago"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return QCoreApplication.translate(
|
return QCoreApplication.translate(
|
||||||
"Common", "{0} years ago"
|
"Common", "{0} years ago"
|
||||||
).format(int(round(secDiff/31557600)))
|
).format(int(round(seconds/31557600)))
|
||||||
|
|
||||||
|
|
||||||
def numberToRoman(numVal, toLower=False):
|
def numberToRoman(value, toLower=False):
|
||||||
"""Convert an integer to a Roman number.
|
"""Convert an integer to a Roman number.
|
||||||
"""
|
"""
|
||||||
if not isinstance(numVal, int):
|
if not isinstance(value, int):
|
||||||
return "NAN"
|
return "NAN"
|
||||||
if numVal < 1 or numVal > 4999:
|
if value < 1 or value > 4999:
|
||||||
return "OOR"
|
return "OOR"
|
||||||
|
|
||||||
theValues = [
|
lookup = [
|
||||||
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"),
|
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"), (100, "C"), (90, "XC"),
|
||||||
(50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
|
(50, "L"), (40, "XL"), (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I"),
|
||||||
]
|
]
|
||||||
|
|
||||||
romNum = ""
|
roman = ""
|
||||||
for theDiv, theSym in theValues:
|
for divisor, symbol in lookup:
|
||||||
n = numVal//theDiv
|
n = value//divisor
|
||||||
romNum += n*theSym
|
roman += n*symbol
|
||||||
numVal -= n*theDiv
|
value -= n*divisor
|
||||||
if numVal <= 0:
|
if value <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
return romNum.lower() if toLower else romNum
|
return roman.lower() if toLower else roman
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================================== #
|
# =============================================================================================== #
|
||||||
@@ -447,51 +466,47 @@ def jsonEncode(data, n=0, nmax=0):
|
|||||||
# File and File System Functions
|
# File and File System Functions
|
||||||
# =============================================================================================== #
|
# =============================================================================================== #
|
||||||
|
|
||||||
def readTextFile(filePath):
|
def readTextFile(path):
|
||||||
"""Read the content of a text file in a robust manner.
|
"""Read the content of a text file in a robust manner.
|
||||||
"""
|
"""
|
||||||
if not os.path.isfile(filePath):
|
path = Path(path)
|
||||||
|
if not path.is_file():
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
fileText = ""
|
|
||||||
try:
|
try:
|
||||||
with open(filePath, mode="r", encoding="utf-8") as inFile:
|
return path.read_text(encoding="utf-8")
|
||||||
fileText = inFile.read()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Could not read file: %s", filePath)
|
logger.error("Could not read file: %s", path)
|
||||||
logException()
|
logException()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
return fileText
|
|
||||||
|
|
||||||
|
|
||||||
def makeFileNameSafe(value):
|
def makeFileNameSafe(value):
|
||||||
"""Returns a filename safe string of the value.
|
"""Returns a filename safe string of the value.
|
||||||
"""
|
"""
|
||||||
cleanName = ""
|
clean = ""
|
||||||
for c in str(value).strip():
|
for c in str(value).strip():
|
||||||
if c.isalpha() or c.isdigit() or c == " ":
|
if c.isalpha() or c.isdigit() or c == " ":
|
||||||
cleanName += c
|
clean += c
|
||||||
return cleanName
|
return clean
|
||||||
|
|
||||||
|
|
||||||
def sha256sum(filePath):
|
def sha256sum(path):
|
||||||
"""Make a shasum of a file using a buffer.
|
"""Make a shasum of a file using a buffer.
|
||||||
Based on: https://stackoverflow.com/a/44873382/5825851
|
Based on: https://stackoverflow.com/a/44873382/5825851
|
||||||
"""
|
"""
|
||||||
hDigest = hashlib.sha256()
|
digest = hashlib.sha256()
|
||||||
bData = bytearray(65536)
|
bData = bytearray(65536)
|
||||||
mData = memoryview(bData)
|
mData = memoryview(bData)
|
||||||
try:
|
try:
|
||||||
with open(filePath, mode="rb", buffering=0) as inFile:
|
with open(path, mode="rb", buffering=0) as inFile:
|
||||||
for n in iter(lambda: inFile.readinto(mData), 0):
|
for n in iter(lambda: inFile.readinto(mData), 0):
|
||||||
hDigest.update(mData[:n])
|
digest.update(mData[:n])
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Could not create sha256sum of: %s", filePath)
|
logger.error("Could not create sha256sum of: %s", path)
|
||||||
logException()
|
logException()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return hDigest.hexdigest()
|
return digest.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================================== #
|
# =============================================================================================== #
|
||||||
@@ -513,87 +528,64 @@ def getGuiItem(objName):
|
|||||||
|
|
||||||
class NWConfigParser(ConfigParser):
|
class NWConfigParser(ConfigParser):
|
||||||
|
|
||||||
CNF_STR = 0
|
|
||||||
CNF_INT = 1
|
|
||||||
CNF_FLOAT = 2
|
|
||||||
CNF_BOOL = 3
|
|
||||||
CNF_S_LST = 4
|
|
||||||
CNF_I_LST = 5
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def rdStr(self, section, option, default):
|
def rdStr(self, section, option, default):
|
||||||
"""Read string value.
|
"""Read string value.
|
||||||
"""
|
"""
|
||||||
return self._parseLine(section, option, default, self.CNF_STR)
|
return self.get(section, option, fallback=default)
|
||||||
|
|
||||||
def rdInt(self, section, option, default):
|
def rdInt(self, section, option, default):
|
||||||
"""Read integer value.
|
"""Read integer value.
|
||||||
"""
|
"""
|
||||||
return self._parseLine(section, option, default, self.CNF_INT)
|
try:
|
||||||
|
return self.getint(section, option, fallback=default)
|
||||||
|
except ValueError:
|
||||||
|
logger.error("Could not read '%s':'%s' from config", section, option)
|
||||||
|
return default
|
||||||
|
|
||||||
def rdFlt(self, section, option, default):
|
def rdFlt(self, section, option, default):
|
||||||
"""Read float value.
|
"""Read float value.
|
||||||
"""
|
"""
|
||||||
return self._parseLine(section, option, default, self.CNF_FLOAT)
|
try:
|
||||||
|
return self.getfloat(section, option, fallback=default)
|
||||||
|
except ValueError:
|
||||||
|
logger.error("Could not read '%s':'%s' from config", section, option)
|
||||||
|
return default
|
||||||
|
|
||||||
def rdBool(self, section, option, default):
|
def rdBool(self, section, option, default):
|
||||||
"""Read boolean value.
|
"""Read boolean value.
|
||||||
"""
|
"""
|
||||||
return self._parseLine(section, option, default, self.CNF_BOOL)
|
try:
|
||||||
|
return self.getboolean(section, option, fallback=default)
|
||||||
|
except ValueError:
|
||||||
|
logger.error("Could not read '%s':'%s' from config", section, option)
|
||||||
|
return default
|
||||||
|
|
||||||
|
def rdPath(self, section, option, default):
|
||||||
|
"""Read a path value.
|
||||||
|
"""
|
||||||
|
return checkPath(self.get(section, option, fallback=default), default)
|
||||||
|
|
||||||
def rdStrList(self, section, option, default):
|
def rdStrList(self, section, option, default):
|
||||||
"""Read string list.
|
"""Read string list.
|
||||||
"""
|
"""
|
||||||
return self._parseLine(section, option, default, self.CNF_S_LST)
|
result = default.copy() if isinstance(default, list) else []
|
||||||
|
if self.has_option(section, option):
|
||||||
|
data = self.get(section, option, fallback="").split(",")
|
||||||
|
for i in range(min(len(data), len(result))):
|
||||||
|
result[i] = data[i].strip()
|
||||||
|
return result
|
||||||
|
|
||||||
def rdIntList(self, section, option, default):
|
def rdIntList(self, section, option, default):
|
||||||
"""Read integer list.
|
"""Read integer list.
|
||||||
"""
|
"""
|
||||||
return self._parseLine(section, option, default, self.CNF_I_LST)
|
result = default.copy() if isinstance(default, list) else []
|
||||||
|
|
||||||
##
|
|
||||||
# Internal Functions
|
|
||||||
##
|
|
||||||
|
|
||||||
def _unpackList(self, value, default, type):
|
|
||||||
"""Unpack a comma-separated string of items into a list.
|
|
||||||
"""
|
|
||||||
inList = value.split(",")
|
|
||||||
outList = []
|
|
||||||
if isinstance(default, list):
|
|
||||||
outList = default.copy()
|
|
||||||
for i in range(min(len(inList), len(outList))):
|
|
||||||
try:
|
|
||||||
if type == self.CNF_S_LST:
|
|
||||||
outList[i] = inList[i].strip()
|
|
||||||
elif type == self.CNF_I_LST:
|
|
||||||
outList[i] = int(inList[i].strip())
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
return outList
|
|
||||||
|
|
||||||
def _parseLine(self, section, option, default, type):
|
|
||||||
"""Parse a line and return the correct datatype.
|
|
||||||
"""
|
|
||||||
if self.has_option(section, option):
|
if self.has_option(section, option):
|
||||||
try:
|
data = self.get(section, option, fallback="").split(",")
|
||||||
if type == self.CNF_STR:
|
for i in range(min(len(data), len(result))):
|
||||||
return self.get(section, option)
|
result[i] = checkInt(data[i].strip(), result[i])
|
||||||
elif type == self.CNF_INT:
|
return result
|
||||||
return self.getint(section, option)
|
|
||||||
elif type == self.CNF_FLOAT:
|
|
||||||
return self.getfloat(section, option)
|
|
||||||
elif type == self.CNF_BOOL:
|
|
||||||
return self.getboolean(section, option)
|
|
||||||
elif type in (self.CNF_I_LST, self.CNF_S_LST):
|
|
||||||
return self._unpackList(self.get(section, option), default, type)
|
|
||||||
except ValueError:
|
|
||||||
logger.error("Could not read '%s':'%s' from config", str(section), str(option))
|
|
||||||
logException()
|
|
||||||
return default
|
|
||||||
|
|
||||||
return default
|
|
||||||
|
|
||||||
# END Class NWConfigParser
|
# END Class NWConfigParser
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class nwHeaders:
|
|||||||
|
|
||||||
class nwFiles:
|
class nwFiles:
|
||||||
|
|
||||||
|
CONF_FILE = "novelwriter.conf"
|
||||||
PROJ_FILE = "nwProject.nwx"
|
PROJ_FILE = "nwProject.nwx"
|
||||||
PROJ_DICT = "wordlist.txt"
|
PROJ_DICT = "wordlist.txt"
|
||||||
PROJ_LOCK = "nwProject.lock"
|
PROJ_LOCK = "nwProject.lock"
|
||||||
@@ -157,6 +158,7 @@ class nwLabels:
|
|||||||
"doc_h1": QT_TRANSLATE_NOOP("Constant", "Novel Title Page"),
|
"doc_h1": QT_TRANSLATE_NOOP("Constant", "Novel Title Page"),
|
||||||
"doc_h2": QT_TRANSLATE_NOOP("Constant", "Novel Chapter"),
|
"doc_h2": QT_TRANSLATE_NOOP("Constant", "Novel Chapter"),
|
||||||
"doc_h3": QT_TRANSLATE_NOOP("Constant", "Novel Scene"),
|
"doc_h3": QT_TRANSLATE_NOOP("Constant", "Novel Scene"),
|
||||||
|
"doc_h4": QT_TRANSLATE_NOOP("Constant", "Novel Section"),
|
||||||
"note": QT_TRANSLATE_NOOP("Constant", "Project Note"),
|
"note": QT_TRANSLATE_NOOP("Constant", "Project Note"),
|
||||||
}
|
}
|
||||||
KEY_NAME = {
|
KEY_NAME = {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from novelwriter.core.document import NWDoc
|
from novelwriter.core.coretools import DocMerger, DocSplitter, ProjectBuilder
|
||||||
from novelwriter.core.index import countWords
|
from novelwriter.core.index import countWords
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
from novelwriter.core.spellcheck import NWSpellEnchant
|
from novelwriter.core.spellcheck import NWSpellEnchant
|
||||||
@@ -28,8 +28,10 @@ from novelwriter.core.toodt import ToOdt
|
|||||||
from novelwriter.core.tomd import ToMarkdown
|
from novelwriter.core.tomd import ToMarkdown
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"DocMerger",
|
||||||
|
"DocSplitter",
|
||||||
|
"ProjectBuilder",
|
||||||
"countWords",
|
"countWords",
|
||||||
"NWDoc",
|
|
||||||
"NWProject",
|
"NWProject",
|
||||||
"NWSpellEnchant",
|
"NWSpellEnchant",
|
||||||
"ToHtml",
|
"ToHtml",
|
||||||
|
|||||||
@@ -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/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from novelwriter.enum import nwItemLayout, nwItemClass
|
from novelwriter.enum import nwItemLayout, nwItemClass
|
||||||
from novelwriter.error import formatException
|
from novelwriter.error import formatException
|
||||||
from novelwriter.common import isHandle, sha256sum
|
from novelwriter.common import isHandle, sha256sum
|
||||||
@@ -33,7 +34,7 @@ from novelwriter.common import isHandle, sha256sum
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class NWDoc():
|
class NWDocument:
|
||||||
|
|
||||||
def __init__(self, theProject, theHandle):
|
def __init__(self, theProject, theHandle):
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ class NWDoc():
|
|||||||
return
|
return
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<NWDoc handle={self._docHandle}>"
|
return f"<NWDocument handle={self._docHandle}>"
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self):
|
||||||
return self._docHandle is not None and bool(self._theItem)
|
return self._docHandle is not None and bool(self._theItem)
|
||||||
@@ -73,7 +74,7 @@ class NWDoc():
|
|||||||
empty string. If something went wrong, return None.
|
empty string. If something went wrong, return None.
|
||||||
"""
|
"""
|
||||||
self._docError = ""
|
self._docError = ""
|
||||||
if self._docHandle is None:
|
if not isinstance(self._docHandle, str):
|
||||||
logger.error("No document handle set")
|
logger.error("No document handle set")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -81,17 +82,22 @@ class NWDoc():
|
|||||||
logger.error("Unknown novelWriter document")
|
logger.error("Unknown novelWriter document")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
contentPath = self.theProject.storage.contentPath
|
||||||
|
if not isinstance(contentPath, Path):
|
||||||
|
logger.error("No content path set")
|
||||||
|
return None
|
||||||
|
|
||||||
docFile = self._docHandle+".nwd"
|
docFile = self._docHandle+".nwd"
|
||||||
logger.debug("Opening document: %s", docFile)
|
logger.debug("Opening document: %s", docFile)
|
||||||
|
|
||||||
docPath = os.path.join(self.theProject.projContent, docFile)
|
docPath = contentPath / docFile
|
||||||
self._fileLoc = docPath
|
self._fileLoc = docPath
|
||||||
|
|
||||||
theText = ""
|
theText = ""
|
||||||
self._docMeta = {}
|
self._docMeta = {}
|
||||||
self._prevHash = None
|
self._prevHash = None
|
||||||
|
|
||||||
if os.path.isfile(docPath):
|
if docPath.exists():
|
||||||
self._prevHash = sha256sum(docPath)
|
self._prevHash = sha256sum(docPath)
|
||||||
try:
|
try:
|
||||||
with open(docPath, mode="r", encoding="utf-8") as inFile:
|
with open(docPath, mode="r", encoding="utf-8") as inFile:
|
||||||
@@ -125,17 +131,20 @@ class NWDoc():
|
|||||||
if not.
|
if not.
|
||||||
"""
|
"""
|
||||||
self._docError = ""
|
self._docError = ""
|
||||||
if self._docHandle is None:
|
if not isinstance(self._docHandle, str):
|
||||||
logger.error("No document handle set")
|
logger.error("No document handle set")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.theProject.ensureFolderStructure()
|
contentPath = self.theProject.storage.contentPath
|
||||||
|
if not isinstance(contentPath, Path):
|
||||||
|
logger.error("No content path set")
|
||||||
|
return False
|
||||||
|
|
||||||
docFile = self._docHandle+".nwd"
|
docFile = self._docHandle+".nwd"
|
||||||
logger.debug("Saving document: %s", docFile)
|
logger.debug("Saving document: %s", docFile)
|
||||||
|
|
||||||
docPath = os.path.join(self.theProject.projContent, docFile)
|
docPath = contentPath / docFile
|
||||||
docTemp = os.path.join(self.theProject.projContent, docFile+"~")
|
docTemp = docPath.with_suffix(".tmp")
|
||||||
|
|
||||||
if self._prevHash is not None and not forceWrite:
|
if self._prevHash is not None and not forceWrite:
|
||||||
self._currHash = sha256sum(docPath)
|
self._currHash = sha256sum(docPath)
|
||||||
@@ -164,7 +173,7 @@ class NWDoc():
|
|||||||
# If we're here, the file was successfully saved, so we can
|
# If we're here, the file was successfully saved, so we can
|
||||||
# replace the temp file with the actual file
|
# replace the temp file with the actual file
|
||||||
try:
|
try:
|
||||||
os.replace(docTemp, docPath)
|
docTemp.replace(docPath)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
self._docError = formatException(exc)
|
self._docError = formatException(exc)
|
||||||
return False
|
return False
|
||||||
@@ -179,23 +188,28 @@ class NWDoc():
|
|||||||
from the project data folder.
|
from the project data folder.
|
||||||
"""
|
"""
|
||||||
self._docError = ""
|
self._docError = ""
|
||||||
if self._docHandle is None:
|
if not isinstance(self._docHandle, str):
|
||||||
logger.error("No document handle set")
|
logger.error("No document handle set")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
chkList = [
|
contentPath = self.theProject.storage.contentPath
|
||||||
os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd"),
|
if not isinstance(contentPath, Path):
|
||||||
os.path.join(self.theProject.projContent, f"{self._docHandle}.nwd~"),
|
logger.error("No content path set")
|
||||||
]
|
return False
|
||||||
|
|
||||||
for chkFile in chkList:
|
docPath = contentPath / f"{self._docHandle}.nwd"
|
||||||
if os.path.isfile(chkFile):
|
docTemp = docPath.with_suffix(".tmp")
|
||||||
try:
|
|
||||||
os.unlink(chkFile)
|
try:
|
||||||
logger.debug("Deleted: %s", chkFile)
|
# ToDo: When Python 3.7 is dropped, these can be changed to
|
||||||
except Exception as exc:
|
# path.unlink(missing_ok=True)
|
||||||
self._docError = formatException(exc)
|
if docPath.exists():
|
||||||
return False
|
docPath.unlink()
|
||||||
|
if docTemp.exists():
|
||||||
|
docTemp.unlink()
|
||||||
|
except Exception as exc:
|
||||||
|
self._docError = formatException(exc)
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -206,7 +220,7 @@ class NWDoc():
|
|||||||
def getFileLocation(self):
|
def getFileLocation(self):
|
||||||
"""Return the file location of the current document.
|
"""Return the file location of the current document.
|
||||||
"""
|
"""
|
||||||
return self._fileLoc
|
return str(self._fileLoc)
|
||||||
|
|
||||||
def getCurrentItem(self):
|
def getCurrentItem(self):
|
||||||
"""Return a pointer to the currently open NWItem.
|
"""Return a pointer to the currently open NWItem.
|
||||||
@@ -263,4 +277,4 @@ class NWDoc():
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# END Class NWDoc
|
# END Class NWDocument
|
||||||
|
|||||||
@@ -26,16 +26,15 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from time import time
|
from time import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from novelwriter.enum import nwItemType, nwItemLayout
|
from novelwriter.enum import nwItemType, nwItemLayout
|
||||||
from novelwriter.error import logException
|
from novelwriter.error import logException
|
||||||
from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode, nwHeaders
|
from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode, nwHeaders
|
||||||
from novelwriter.core.document import NWDoc
|
|
||||||
from novelwriter.common import (
|
from novelwriter.common import (
|
||||||
checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
|
checkInt, isHandle, isItemClass, isTitleTag, jsonEncode
|
||||||
)
|
)
|
||||||
@@ -59,23 +58,23 @@ class NWIndex:
|
|||||||
The index data is cached in a JSON file between writing sessions.
|
The index data is cached in a JSON file between writing sessions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, theProject):
|
def __init__(self, project):
|
||||||
|
|
||||||
self.theProject = theProject
|
self._project = project
|
||||||
|
|
||||||
# Storage and State
|
# Storage and State
|
||||||
self._tagsIndex = TagsIndex()
|
self._tagsIndex = TagsIndex()
|
||||||
self._itemIndex = ItemIndex(theProject)
|
self._itemIndex = ItemIndex(project)
|
||||||
self._indexBroken = False
|
self._indexBroken = False
|
||||||
|
|
||||||
# TimeStamps
|
# TimeStamps
|
||||||
self._indexChange = 0
|
self._indexChange = 0.0
|
||||||
self._rootChange = {}
|
self._rootChange = {}
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<NWIndex project='{self.theProject.projName}'>"
|
return f"<NWIndex project='{self._project.data.name}'>"
|
||||||
|
|
||||||
##
|
##
|
||||||
# Properties
|
# Properties
|
||||||
@@ -94,10 +93,22 @@ class NWIndex:
|
|||||||
"""
|
"""
|
||||||
self._tagsIndex.clear()
|
self._tagsIndex.clear()
|
||||||
self._itemIndex.clear()
|
self._itemIndex.clear()
|
||||||
self._indexChange = 0
|
self._indexChange = 0.0
|
||||||
self._rootChange = {}
|
self._rootChange = {}
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def rebuildIndex(self):
|
||||||
|
"""Rebuild the entire index from scratch.
|
||||||
|
"""
|
||||||
|
self.clearIndex()
|
||||||
|
for nwItem in self._project.tree:
|
||||||
|
if nwItem is not None and nwItem.isFileType():
|
||||||
|
tHandle = nwItem.itemHandle
|
||||||
|
theDoc = self._project.storage.getDocument(tHandle)
|
||||||
|
self.scanText(tHandle, theDoc.readDocument() or "")
|
||||||
|
self._indexBroken = False
|
||||||
|
return
|
||||||
|
|
||||||
def deleteHandle(self, tHandle):
|
def deleteHandle(self, tHandle):
|
||||||
"""Delete all entries of a given document handle.
|
"""Delete all entries of a given document handle.
|
||||||
"""
|
"""
|
||||||
@@ -114,11 +125,11 @@ class NWIndex:
|
|||||||
moved from the archive or trash folders back into the active
|
moved from the archive or trash folders back into the active
|
||||||
project.
|
project.
|
||||||
"""
|
"""
|
||||||
if not self.theProject.tree.checkType(tHandle, nwItemType.FILE):
|
if not self._project.tree.checkType(tHandle, nwItemType.FILE):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.debug("Re-indexing item '%s'", tHandle)
|
logger.debug("Re-indexing item '%s'", tHandle)
|
||||||
theDoc = NWDoc(self.theProject, tHandle)
|
theDoc = self._project.storage.getDocument(tHandle)
|
||||||
self.scanText(tHandle, theDoc.readDocument() or "")
|
self.scanText(tHandle, theDoc.readDocument() or "")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -126,13 +137,13 @@ class NWIndex:
|
|||||||
def indexChangedSince(self, checkTime):
|
def indexChangedSince(self, checkTime):
|
||||||
"""Check if the index has changed since a given time.
|
"""Check if the index has changed since a given time.
|
||||||
"""
|
"""
|
||||||
return self._indexChange > checkTime
|
return self._indexChange > float(checkTime)
|
||||||
|
|
||||||
def rootChangedSince(self, rootHandle, checkTime):
|
def rootChangedSince(self, rootHandle, checkTime):
|
||||||
"""Check if the index has changed since a given time for a
|
"""Check if the index has changed since a given time for a
|
||||||
given root item.
|
given root item.
|
||||||
"""
|
"""
|
||||||
return self._rootChange.get(rootHandle, self._indexChange) > checkTime
|
return self._rootChange.get(rootHandle, self._indexChange) > float(checkTime)
|
||||||
|
|
||||||
##
|
##
|
||||||
# Load and Save Index to/from File
|
# Load and Save Index to/from File
|
||||||
@@ -141,12 +152,15 @@ class NWIndex:
|
|||||||
def loadIndex(self):
|
def loadIndex(self):
|
||||||
"""Load index from last session from the project meta folder.
|
"""Load index from last session from the project meta folder.
|
||||||
"""
|
"""
|
||||||
|
indexFile = self._project.storage.getMetaFile(nwFiles.INDEX_FILE)
|
||||||
|
if not isinstance(indexFile, Path):
|
||||||
|
return False
|
||||||
|
|
||||||
theData = {}
|
theData = {}
|
||||||
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
|
|
||||||
tStart = time()
|
tStart = time()
|
||||||
|
|
||||||
self._indexBroken = False
|
self._indexBroken = False
|
||||||
if os.path.isfile(indexFile):
|
if indexFile.exists():
|
||||||
logger.debug("Loading index file")
|
logger.debug("Loading index file")
|
||||||
try:
|
try:
|
||||||
with open(indexFile, mode="r", encoding="utf-8") as inFile:
|
with open(indexFile, mode="r", encoding="utf-8") as inFile:
|
||||||
@@ -169,14 +183,14 @@ class NWIndex:
|
|||||||
logger.debug("Checking index")
|
logger.debug("Checking index")
|
||||||
|
|
||||||
# Check that all files are indexed
|
# Check that all files are indexed
|
||||||
for fHandle in self.theProject.projFiles:
|
for fHandle in self._project.projFiles:
|
||||||
if fHandle not in self._itemIndex:
|
if fHandle not in self._itemIndex:
|
||||||
logger.warning("Item '%s' is not in the index", fHandle)
|
logger.warning("Item '%s' is not in the index", fHandle)
|
||||||
self.reIndexHandle(fHandle)
|
self.reIndexHandle(fHandle)
|
||||||
|
|
||||||
self._indexChange = round(time())
|
self._indexChange = time()
|
||||||
|
|
||||||
logger.verbose("Index loaded in %.3f ms", (time() - tStart)*1000)
|
logger.debug("Index loaded in %.3f ms", (time() - tStart)*1000)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -184,8 +198,11 @@ class NWIndex:
|
|||||||
"""Save the current index as a json file in the project meta
|
"""Save the current index as a json file in the project meta
|
||||||
data folder.
|
data folder.
|
||||||
"""
|
"""
|
||||||
|
indexFile = self._project.storage.getMetaFile(nwFiles.INDEX_FILE)
|
||||||
|
if not isinstance(indexFile, Path):
|
||||||
|
return False
|
||||||
|
|
||||||
logger.debug("Saving index file")
|
logger.debug("Saving index file")
|
||||||
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
|
|
||||||
tStart = time()
|
tStart = time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -202,7 +219,7 @@ class NWIndex:
|
|||||||
logException()
|
logException()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.verbose("Index saved in %.3f ms", (time() - tStart)*1000)
|
logger.debug("Index saved in %.3f ms", (time() - tStart)*1000)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -217,11 +234,11 @@ class NWIndex:
|
|||||||
files before we save them, in which case we already have the
|
files before we save them, in which case we already have the
|
||||||
text.
|
text.
|
||||||
"""
|
"""
|
||||||
theItem = self.theProject.tree[tHandle]
|
theItem = self._project.tree[tHandle]
|
||||||
if theItem is None:
|
if theItem is None:
|
||||||
logger.info("Not indexing unknown item '%s'", tHandle)
|
logger.info("Not indexing unknown item '%s'", tHandle)
|
||||||
return False
|
return False
|
||||||
if theItem.itemType != nwItemType.FILE:
|
if not theItem.isFileType():
|
||||||
logger.info("Not indexing non-file item '%s'", tHandle)
|
logger.info("Not indexing non-file item '%s'", tHandle)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -243,20 +260,43 @@ class NWIndex:
|
|||||||
if theItem.itemParent is None:
|
if theItem.itemParent is None:
|
||||||
logger.info("Not indexing orphaned item '%s'", tHandle)
|
logger.info("Not indexing orphaned item '%s'", tHandle)
|
||||||
return False
|
return False
|
||||||
if theItem.isInactive():
|
|
||||||
logger.debug("Not indexing inactive item '%s'", tHandle)
|
|
||||||
return False
|
|
||||||
|
|
||||||
logger.debug("Indexing item with handle '%s'", tHandle)
|
logger.debug("Indexing item with handle '%s'", tHandle)
|
||||||
|
if theItem.isInactive():
|
||||||
|
self._scanInactive(theItem, theText)
|
||||||
|
else:
|
||||||
|
self._scanActive(tHandle, theItem, theText, itemTags)
|
||||||
|
|
||||||
# Scan the text content
|
# Update timestamps for index changes
|
||||||
|
nowTime = time()
|
||||||
|
self._indexChange = nowTime
|
||||||
|
self._rootChange[theItem.itemRoot] = nowTime
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
##
|
||||||
|
# Internal Indexer Helpers
|
||||||
|
##
|
||||||
|
|
||||||
|
def _scanActive(self, tHandle, theItem, theText, itemTags):
|
||||||
|
"""Scan an active document for meta data.
|
||||||
|
"""
|
||||||
nTitle = 0
|
nTitle = 0
|
||||||
|
findHeader = True
|
||||||
theLines = theText.splitlines()
|
theLines = theText.splitlines()
|
||||||
|
|
||||||
for nLine, aLine in enumerate(theLines, start=1):
|
for nLine, aLine in enumerate(theLines, start=1):
|
||||||
|
|
||||||
if len(aLine.strip()) == 0:
|
if len(aLine.strip()) == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if aLine.startswith("#"):
|
if aLine.startswith("#"):
|
||||||
|
if findHeader:
|
||||||
|
hDepth, _ = self._splitHeading(aLine)
|
||||||
|
if hDepth != "H0":
|
||||||
|
theItem.setMainHeading(hDepth)
|
||||||
|
findHeader = False
|
||||||
|
|
||||||
isTitle = self._indexTitle(tHandle, aLine, nLine)
|
isTitle = self._indexTitle(tHandle, aLine, nLine)
|
||||||
if isTitle and nLine > 0:
|
if isTitle and nLine > 0:
|
||||||
if nTitle > 0:
|
if nTitle > 0:
|
||||||
@@ -289,48 +329,49 @@ class NWIndex:
|
|||||||
# Prune no longer used tags
|
# Prune no longer used tags
|
||||||
for tTag, isActive in itemTags.items():
|
for tTag, isActive in itemTags.items():
|
||||||
if not isActive:
|
if not isActive:
|
||||||
logger.verbose("Deleting removed tag '%s'", tTag)
|
logger.debug("Deleting removed tag '%s'", tTag)
|
||||||
del self._tagsIndex[tTag]
|
del self._tagsIndex[tTag]
|
||||||
|
|
||||||
# Update timestamps for index changes
|
return
|
||||||
nowTime = round(time())
|
|
||||||
self._indexChange = nowTime
|
|
||||||
self._rootChange[theItem.itemRoot] = nowTime
|
|
||||||
|
|
||||||
return True
|
def _scanInactive(self, theItem, theText):
|
||||||
|
"""Scan an inactive document for meta data.
|
||||||
|
"""
|
||||||
|
for aLine in theText.splitlines():
|
||||||
|
if aLine.startswith("#"):
|
||||||
|
hDepth, _ = self._splitHeading(aLine)
|
||||||
|
if hDepth != "H0":
|
||||||
|
theItem.setMainHeading(hDepth)
|
||||||
|
break
|
||||||
|
return
|
||||||
|
|
||||||
##
|
def _splitHeading(self, aLine):
|
||||||
# Internal Indexer Helpers
|
"""Split a heading into its header level and text value.
|
||||||
##
|
"""
|
||||||
|
if aLine.startswith("# "):
|
||||||
|
return "H1", aLine[2:].strip()
|
||||||
|
elif aLine.startswith("## "):
|
||||||
|
return "H2", aLine[3:].strip()
|
||||||
|
elif aLine.startswith("### "):
|
||||||
|
return "H3", aLine[4:].strip()
|
||||||
|
elif aLine.startswith("#### "):
|
||||||
|
return "H4", aLine[5:].strip()
|
||||||
|
elif aLine.startswith("#! "):
|
||||||
|
return "H1", aLine[3:].strip()
|
||||||
|
elif aLine.startswith("##! "):
|
||||||
|
return "H2", aLine[4:].strip()
|
||||||
|
return "H0", ""
|
||||||
|
|
||||||
def _indexTitle(self, tHandle, aLine, nTitle):
|
def _indexTitle(self, tHandle, aLine, nTitle):
|
||||||
"""Save information about the title and its location in the
|
"""Save information about the title and its location in the
|
||||||
file to the index.
|
file to the index.
|
||||||
"""
|
"""
|
||||||
if aLine.startswith("# "):
|
hDepth, hText = self._splitHeading(aLine)
|
||||||
hDepth = "H1"
|
if hDepth == "H0":
|
||||||
hText = aLine[2:].strip()
|
|
||||||
elif aLine.startswith("## "):
|
|
||||||
hDepth = "H2"
|
|
||||||
hText = aLine[3:].strip()
|
|
||||||
elif aLine.startswith("### "):
|
|
||||||
hDepth = "H3"
|
|
||||||
hText = aLine[4:].strip()
|
|
||||||
elif aLine.startswith("#### "):
|
|
||||||
hDepth = "H4"
|
|
||||||
hText = aLine[5:].strip()
|
|
||||||
elif aLine.startswith("#! "):
|
|
||||||
hDepth = "H1"
|
|
||||||
hText = aLine[3:].strip()
|
|
||||||
elif aLine.startswith("##! "):
|
|
||||||
hDepth = "H2"
|
|
||||||
hText = aLine[4:].strip()
|
|
||||||
else:
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
sTitle = f"T{nTitle:06d}"
|
sTitle = f"T{nTitle:06d}"
|
||||||
self._itemIndex.addItemHeading(tHandle, sTitle, hDepth, hText)
|
self._itemIndex.addItemHeading(tHandle, sTitle, hDepth, hText)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _indexWordCounts(self, tHandle, theText, nTitle):
|
def _indexWordCounts(self, tHandle, theText, nTitle):
|
||||||
@@ -493,18 +534,15 @@ class NWIndex:
|
|||||||
for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle)
|
for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle)
|
||||||
]
|
]
|
||||||
|
|
||||||
def getHandleHeaderLevel(self, tHandle):
|
def getTableOfContents(self, rootHandle, maxDepth, skipExcl=True):
|
||||||
"""Get the header level of the first header of a handle.
|
|
||||||
"""
|
|
||||||
return self._itemIndex.mainItemHeader(tHandle)
|
|
||||||
|
|
||||||
def getTableOfContents(self, maxDepth, skipExcl=True):
|
|
||||||
"""Generate a table of contents up to a maximum depth.
|
"""Generate a table of contents up to a maximum depth.
|
||||||
"""
|
"""
|
||||||
tOrder = []
|
tOrder = []
|
||||||
tData = {}
|
tData = {}
|
||||||
pKey = None
|
pKey = None
|
||||||
for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(skipExcl=skipExcl):
|
for tHandle, sTitle, hItem in self._itemIndex.iterNovelStructure(
|
||||||
|
rootHandle=rootHandle, skipExcl=skipExcl
|
||||||
|
):
|
||||||
tKey = f"{tHandle}:{sTitle}"
|
tKey = f"{tHandle}:{sTitle}"
|
||||||
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
|
iLevel = nwHeaders.H_LEVEL.get(hItem.level, 0)
|
||||||
if iLevel > maxDepth:
|
if iLevel > maxDepth:
|
||||||
@@ -647,23 +685,17 @@ class TagsIndex:
|
|||||||
def tagHandle(self, tagKey):
|
def tagHandle(self, tagKey):
|
||||||
"""Get the handle of a given tag.
|
"""Get the handle of a given tag.
|
||||||
"""
|
"""
|
||||||
if tagKey in self._tags:
|
return self._tags.get(tagKey, {}).get("handle", None)
|
||||||
return self._tags.get(tagKey).get("handle")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def tagHeading(self, tagKey):
|
def tagHeading(self, tagKey):
|
||||||
"""Get the heading of a given tag.
|
"""Get the heading of a given tag.
|
||||||
"""
|
"""
|
||||||
if tagKey in self._tags:
|
return self._tags.get(tagKey, {}).get("heading", nwHeaders.TT_NONE)
|
||||||
return self._tags.get(tagKey).get("heading")
|
|
||||||
return nwHeaders.TT_NONE
|
|
||||||
|
|
||||||
def tagClass(self, tagKey):
|
def tagClass(self, tagKey):
|
||||||
"""Get the class of a given tag.
|
"""Get the class of a given tag.
|
||||||
"""
|
"""
|
||||||
if tagKey in self._tags:
|
return self._tags.get(tagKey, {}).get("class", None)
|
||||||
return self._tags.get(tagKey).get("class")
|
|
||||||
return None
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Pack/Unpack
|
# Pack/Unpack
|
||||||
@@ -717,8 +749,8 @@ class ItemIndex:
|
|||||||
IndexHeading object for each header of the text.
|
IndexHeading object for each header of the text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, theProject):
|
def __init__(self, project):
|
||||||
self.theProject = theProject
|
self._project = project
|
||||||
self._items = {}
|
self._items = {}
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -755,13 +787,6 @@ class ItemIndex:
|
|||||||
self._items[tHandle] = IndexItem(tHandle, tItem)
|
self._items[tHandle] = IndexItem(tHandle, tItem)
|
||||||
return
|
return
|
||||||
|
|
||||||
def mainItemHeader(self, tHandle):
|
|
||||||
"""Return the primary item header for an item.
|
|
||||||
"""
|
|
||||||
if tHandle in self._items:
|
|
||||||
return self._items[tHandle].level
|
|
||||||
return "H0"
|
|
||||||
|
|
||||||
def allItemTags(self, tHandle):
|
def allItemTags(self, tHandle):
|
||||||
"""Get all tags set for headings of an item.
|
"""Get all tags set for headings of an item.
|
||||||
"""
|
"""
|
||||||
@@ -789,12 +814,12 @@ class ItemIndex:
|
|||||||
"""Iterate over all items and headers in the novel structure for
|
"""Iterate over all items and headers in the novel structure for
|
||||||
a given root handle, or for all if root handle is None.
|
a given root handle, or for all if root handle is None.
|
||||||
"""
|
"""
|
||||||
for tItem in self.theProject.tree:
|
for tItem in self._project.tree:
|
||||||
if tItem is None:
|
if tItem is None:
|
||||||
continue
|
continue
|
||||||
if tItem.itemLayout == nwItemLayout.NOTE:
|
if tItem.isNoteLayout():
|
||||||
continue
|
continue
|
||||||
if skipExcl and not tItem.isExported:
|
if skipExcl and not tItem.isActive:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tHandle = tItem.itemHandle
|
tHandle = tItem.itemHandle
|
||||||
@@ -807,8 +832,6 @@ class ItemIndex:
|
|||||||
elif tItem.itemRoot == rootHandle:
|
elif tItem.itemRoot == rootHandle:
|
||||||
for sTitle in self._items[tHandle].headings():
|
for sTitle in self._items[tHandle].headings():
|
||||||
yield tHandle, sTitle, self._items[tHandle][sTitle]
|
yield tHandle, sTitle, self._items[tHandle][sTitle]
|
||||||
else:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -821,7 +844,6 @@ class ItemIndex:
|
|||||||
"""
|
"""
|
||||||
if tHandle in self._items:
|
if tHandle in self._items:
|
||||||
tItem = self._items[tHandle]
|
tItem = self._items[tHandle]
|
||||||
tItem.updateLevel(hDepth)
|
|
||||||
tItem.addHeading(IndexHeading(sTitle, hDepth, hText))
|
tItem.addHeading(IndexHeading(sTitle, hDepth, hText))
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -875,7 +897,7 @@ class ItemIndex:
|
|||||||
if not isHandle(tHandle):
|
if not isHandle(tHandle):
|
||||||
raise ValueError("itemIndex keys must be handles")
|
raise ValueError("itemIndex keys must be handles")
|
||||||
|
|
||||||
nwItem = self.theProject.tree[tHandle]
|
nwItem = self._project.tree[tHandle]
|
||||||
if nwItem is not None:
|
if nwItem is not None:
|
||||||
tItem = IndexItem(tHandle, nwItem)
|
tItem = IndexItem(tHandle, nwItem)
|
||||||
tItem.unpackData(tData)
|
tItem.unpackData(tData)
|
||||||
@@ -897,7 +919,6 @@ class IndexItem:
|
|||||||
def __init__(self, tHandle, tItem):
|
def __init__(self, tHandle, tItem):
|
||||||
self._handle = tHandle
|
self._handle = tHandle
|
||||||
self._item = tItem
|
self._item = tItem
|
||||||
self._level = "H0"
|
|
||||||
self._headings = {}
|
self._headings = {}
|
||||||
self._index = 0
|
self._index = 0
|
||||||
|
|
||||||
@@ -917,21 +938,10 @@ class IndexItem:
|
|||||||
def item(self):
|
def item(self):
|
||||||
return self._item
|
return self._item
|
||||||
|
|
||||||
@property
|
|
||||||
def level(self):
|
|
||||||
return self._level
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Setters
|
# Setters
|
||||||
##
|
##
|
||||||
|
|
||||||
def updateLevel(self, level):
|
|
||||||
"""Set the level only if it has not already been set.
|
|
||||||
"""
|
|
||||||
if self._level == "H0":
|
|
||||||
self._level = level
|
|
||||||
return
|
|
||||||
|
|
||||||
def addHeading(self, tHeading):
|
def addHeading(self, tHeading):
|
||||||
"""Add a heading to the item. Also remove the placeholder entry
|
"""Add a heading to the item. Also remove the placeholder entry
|
||||||
if it exists.
|
if it exists.
|
||||||
@@ -1011,7 +1021,7 @@ class IndexItem:
|
|||||||
if hRefs:
|
if hRefs:
|
||||||
refs[sTitle] = hRefs
|
refs[sTitle] = hRefs
|
||||||
|
|
||||||
data = {"level": self._level}
|
data = {}
|
||||||
data["headings"] = heads
|
data["headings"] = heads
|
||||||
if refs:
|
if refs:
|
||||||
data["references"] = refs
|
data["references"] = refs
|
||||||
@@ -1021,7 +1031,6 @@ class IndexItem:
|
|||||||
def unpackData(self, data):
|
def unpackData(self, data):
|
||||||
"""Unpack an item entry from the data.
|
"""Unpack an item entry from the data.
|
||||||
"""
|
"""
|
||||||
self._level = data.get("level", "H0")
|
|
||||||
references = data.get("references", {})
|
references = data.get("references", {})
|
||||||
for sTitle, hData in data.get("headings", {}).items():
|
for sTitle, hData in data.get("headings", {}).items():
|
||||||
if not isTitleTag(sTitle):
|
if not isTitleTag(sTitle):
|
||||||
@@ -1030,6 +1039,7 @@ class IndexItem:
|
|||||||
tHeading.unpackData(hData)
|
tHeading.unpackData(hData)
|
||||||
tHeading.unpackReferences(references.get(sTitle, {}))
|
tHeading.unpackReferences(references.get(sTitle, {}))
|
||||||
self.addHeading(tHeading)
|
self.addHeading(tHeading)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# END Class IndexItem
|
# END Class IndexItem
|
||||||
|
|||||||
@@ -25,23 +25,27 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from lxml import etree
|
|
||||||
|
|
||||||
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
|
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
|
||||||
from novelwriter.common import (
|
from novelwriter.common import (
|
||||||
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified
|
checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified, yesNo
|
||||||
)
|
)
|
||||||
from novelwriter.constants import nwLabels, trConst
|
from novelwriter.constants import nwHeaders, nwLabels, trConst
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class NWItem():
|
class NWItem:
|
||||||
|
|
||||||
def __init__(self, theProject):
|
__slots__ = (
|
||||||
|
"_project", "_name", "_handle", "_parent", "_root", "_order",
|
||||||
|
"_type", "_class", "_layout", "_status", "_import", "_active",
|
||||||
|
"_expanded", "_heading", "_charCount", "_wordCount",
|
||||||
|
"_paraCount", "_cursorPos", "_initCount",
|
||||||
|
)
|
||||||
|
|
||||||
self.theProject = theProject
|
def __init__(self, project):
|
||||||
|
|
||||||
|
self._project = project
|
||||||
self._name = ""
|
self._name = ""
|
||||||
self._handle = None
|
self._handle = None
|
||||||
self._parent = None
|
self._parent = None
|
||||||
@@ -52,15 +56,16 @@ class NWItem():
|
|||||||
self._layout = nwItemLayout.NO_LAYOUT
|
self._layout = nwItemLayout.NO_LAYOUT
|
||||||
self._status = None
|
self._status = None
|
||||||
self._import = None
|
self._import = None
|
||||||
|
self._active = True
|
||||||
self._expanded = False
|
self._expanded = False
|
||||||
self._exported = True
|
|
||||||
|
|
||||||
# Document Meta Data
|
# Document Meta Data
|
||||||
self._charCount = 0 # Current character count
|
self._heading = "H0" # The main heading
|
||||||
self._wordCount = 0 # Current word count
|
self._charCount = 0 # Current character count
|
||||||
self._paraCount = 0 # Current paragraph count
|
self._wordCount = 0 # Current word count
|
||||||
self._cursorPos = 0 # Last cursor position
|
self._paraCount = 0 # Current paragraph count
|
||||||
self._initCount = 0 # Initial word count
|
self._cursorPos = 0 # Last cursor position
|
||||||
|
self._initCount = 0 # Initial word count
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -114,13 +119,17 @@ class NWItem():
|
|||||||
def itemImport(self):
|
def itemImport(self):
|
||||||
return self._import
|
return self._import
|
||||||
|
|
||||||
|
@property
|
||||||
|
def isActive(self):
|
||||||
|
return self._active
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def isExpanded(self):
|
def isExpanded(self):
|
||||||
return self._expanded
|
return self._expanded
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def isExported(self):
|
def mainHeading(self):
|
||||||
return self._exported
|
return self._heading
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def charCount(self):
|
def charCount(self):
|
||||||
@@ -143,101 +152,75 @@ class NWItem():
|
|||||||
return self._cursorPos
|
return self._cursorPos
|
||||||
|
|
||||||
##
|
##
|
||||||
# XML Pack/Unpack
|
# Pack/Unpack Data
|
||||||
##
|
##
|
||||||
|
|
||||||
def packXML(self, xParent):
|
def pack(self):
|
||||||
"""Pack all the data in the class instance into an XML object.
|
"""Pack all the data in the class instance into a dictionary.
|
||||||
"""
|
"""
|
||||||
itemAttrib = {}
|
item = {}
|
||||||
itemAttrib["handle"] = str(self._handle)
|
meta = {}
|
||||||
itemAttrib["parent"] = str(self._parent)
|
name = {}
|
||||||
itemAttrib["root"] = str(self._root)
|
|
||||||
itemAttrib["order"] = str(self._order)
|
item["handle"] = str(self._handle)
|
||||||
itemAttrib["type"] = str(self._type.name)
|
item["parent"] = str(self._parent)
|
||||||
itemAttrib["class"] = str(self._class.name)
|
item["root"] = str(self._root)
|
||||||
|
item["order"] = str(self._order)
|
||||||
|
item["type"] = str(self._type.name)
|
||||||
|
item["class"] = str(self._class.name)
|
||||||
|
meta["expanded"] = yesNo(self._expanded)
|
||||||
|
name["status"] = str(self._status)
|
||||||
|
name["import"] = str(self._import)
|
||||||
|
|
||||||
if self._type == nwItemType.FILE:
|
if self._type == nwItemType.FILE:
|
||||||
itemAttrib["layout"] = str(self._layout.name)
|
item["layout"] = str(self._layout.name)
|
||||||
|
meta["heading"] = str(self._heading)
|
||||||
|
meta["charCount"] = str(self._charCount)
|
||||||
|
meta["wordCount"] = str(self._wordCount)
|
||||||
|
meta["paraCount"] = str(self._paraCount)
|
||||||
|
meta["cursorPos"] = str(self._cursorPos)
|
||||||
|
name["active"] = yesNo(self._active)
|
||||||
|
|
||||||
metaAttrib = {}
|
data = {
|
||||||
metaAttrib["expanded"] = str(self._expanded)
|
"name": str(self._name),
|
||||||
if self._type == nwItemType.FILE:
|
"itemAttr": item,
|
||||||
metaAttrib["charCount"] = str(self._charCount)
|
"metaAttr": meta,
|
||||||
metaAttrib["wordCount"] = str(self._wordCount)
|
"nameAttr": name,
|
||||||
metaAttrib["paraCount"] = str(self._paraCount)
|
}
|
||||||
metaAttrib["cursorPos"] = str(self._cursorPos)
|
|
||||||
|
|
||||||
nameAttrib = {}
|
return data
|
||||||
nameAttrib["status"] = str(self._status)
|
|
||||||
nameAttrib["import"] = str(self._import)
|
|
||||||
if self._type == nwItemType.FILE:
|
|
||||||
nameAttrib["exported"] = str(self._exported)
|
|
||||||
|
|
||||||
xPack = etree.SubElement(xParent, "item", attrib=itemAttrib)
|
def unpack(self, data):
|
||||||
self._subPack(xPack, "meta", attrib=metaAttrib)
|
"""Set the values from a data dictionary.
|
||||||
self._subPack(xPack, "name", text=str(self._name), attrib=nameAttrib)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def unpackXML(self, xItem):
|
|
||||||
"""Set the values from an XML entry of type 'item'.
|
|
||||||
"""
|
"""
|
||||||
if xItem.tag != "item":
|
item = data.get("itemAttr", {})
|
||||||
logger.error("XML entry is not an NWItem")
|
meta = data.get("metaAttr", {})
|
||||||
return False
|
name = data.get("nameAttr", {})
|
||||||
|
|
||||||
if "handle" in xItem.attrib:
|
if "handle" in item:
|
||||||
self.setHandle(xItem.attrib["handle"])
|
self.setHandle(item["handle"])
|
||||||
else:
|
else:
|
||||||
logger.error("XML item entry does not have a handle")
|
logger.error("Item does not have a handle")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.setParent(xItem.attrib.get("parent", None))
|
self.setName(data.get("name", ""))
|
||||||
self.setRoot(xItem.attrib.get("root", None))
|
self.setParent(item.get("parent", None))
|
||||||
self.setOrder(xItem.attrib.get("order", 0))
|
self.setRoot(item.get("root", None))
|
||||||
self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE))
|
self.setOrder(item.get("order", 0))
|
||||||
self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS))
|
self.setType(item.get("type", nwItemType.NO_TYPE))
|
||||||
self.setLayout(xItem.attrib.get("layout", nwItemLayout.NO_LAYOUT))
|
self.setClass(item.get("class", nwItemClass.NO_CLASS))
|
||||||
|
self.setExpanded(meta.get("expanded", False))
|
||||||
|
self.setStatus(name.get("status", None))
|
||||||
|
self.setImport(name.get("import", None))
|
||||||
|
|
||||||
for xValue in xItem:
|
if self._type == nwItemType.FILE:
|
||||||
if xValue.tag == "meta":
|
self.setLayout(item.get("layout", nwItemLayout.NO_LAYOUT))
|
||||||
self.setExpanded(xValue.attrib.get("expanded", False))
|
self.setMainHeading(meta.get("heading", "H0"))
|
||||||
self.setCharCount(xValue.attrib.get("charCount", 0))
|
self.setCharCount(meta.get("charCount", 0))
|
||||||
self.setWordCount(xValue.attrib.get("wordCount", 0))
|
self.setWordCount(meta.get("wordCount", 0))
|
||||||
self.setParaCount(xValue.attrib.get("paraCount", 0))
|
self.setParaCount(meta.get("paraCount", 0))
|
||||||
self.setCursorPos(xValue.attrib.get("cursorPos", 0))
|
self.setCursorPos(meta.get("cursorPos", 0))
|
||||||
elif xValue.tag == "name":
|
self.setActive(name.get("active", True))
|
||||||
self.setName(xValue.text)
|
|
||||||
self.setStatus(xValue.attrib.get("status", None))
|
|
||||||
self.setImport(xValue.attrib.get("import", None))
|
|
||||||
self.setExported(xValue.attrib.get("exported", True))
|
|
||||||
|
|
||||||
# Legacy Format (1.3 and earlier)
|
|
||||||
elif xValue.tag == "status":
|
|
||||||
self.setImportStatus(xValue.text)
|
|
||||||
elif xValue.tag == "type":
|
|
||||||
self.setType(xValue.text)
|
|
||||||
elif xValue.tag == "class":
|
|
||||||
self.setClass(xValue.text)
|
|
||||||
elif xValue.tag == "layout":
|
|
||||||
self.setLayout(xValue.text)
|
|
||||||
elif xValue.tag == "expanded":
|
|
||||||
self.setExpanded(xValue.text)
|
|
||||||
elif xValue.tag == "exported":
|
|
||||||
self.setExported(xValue.text)
|
|
||||||
elif xValue.tag == "charCount":
|
|
||||||
self.setCharCount(xValue.text)
|
|
||||||
elif xValue.tag == "wordCount":
|
|
||||||
self.setWordCount(xValue.text)
|
|
||||||
elif xValue.tag == "paraCount":
|
|
||||||
self.setParaCount(xValue.text)
|
|
||||||
elif xValue.tag == "cursorPos":
|
|
||||||
self.setCursorPos(xValue.text)
|
|
||||||
else:
|
|
||||||
# Sliently skip as we may otherwise cause orphaned
|
|
||||||
# items if an otherwise valid file is opened by a
|
|
||||||
# version of novelWriter that doesn't know the tag
|
|
||||||
logger.error("Unknown tag '%s'", xValue.tag)
|
|
||||||
|
|
||||||
# Make some checks to ensure consistency
|
# Make some checks to ensure consistency
|
||||||
if self._type == nwItemType.ROOT:
|
if self._type == nwItemType.ROOT:
|
||||||
@@ -245,31 +228,22 @@ class NWItem():
|
|||||||
self._parent = None # Root items cannot have a parent
|
self._parent = None # Root items cannot have a parent
|
||||||
|
|
||||||
if self._type != nwItemType.FILE:
|
if self._type != nwItemType.FILE:
|
||||||
self._charCount = 0 # Only set for files
|
# Reset values that should only be set for files
|
||||||
self._wordCount = 0 # Only set for files
|
self._layout = nwItemLayout.NO_LAYOUT
|
||||||
self._paraCount = 0 # Only set for files
|
self._heading = "H0"
|
||||||
self._cursorPos = 0 # Only set for files
|
self._active = False
|
||||||
|
self._charCount = 0
|
||||||
|
self._wordCount = 0
|
||||||
|
self._paraCount = 0
|
||||||
|
self._cursorPos = 0
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _subPack(xParent, name, attrib=None, text=None, none=True):
|
|
||||||
"""Pack the values into an XML element.
|
|
||||||
"""
|
|
||||||
if not none and (text is None or text == "None"):
|
|
||||||
return None
|
|
||||||
xAttr = {} if attrib is None else attrib
|
|
||||||
xSub = etree.SubElement(xParent, name, attrib=xAttr)
|
|
||||||
if text is not None:
|
|
||||||
xSub.text = text
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Lookup Methods
|
# Lookup Methods
|
||||||
##
|
##
|
||||||
|
|
||||||
def describeMe(self, hLevel=None):
|
def describeMe(self):
|
||||||
"""Return a string description of the item.
|
"""Return a string description of the item.
|
||||||
"""
|
"""
|
||||||
descKey = "none"
|
descKey = "none"
|
||||||
@@ -279,12 +253,14 @@ class NWItem():
|
|||||||
descKey = "folder"
|
descKey = "folder"
|
||||||
elif self._type == nwItemType.FILE:
|
elif self._type == nwItemType.FILE:
|
||||||
if self._layout == nwItemLayout.DOCUMENT:
|
if self._layout == nwItemLayout.DOCUMENT:
|
||||||
if hLevel == "H1":
|
if self._heading == "H1":
|
||||||
descKey = "doc_h1"
|
descKey = "doc_h1"
|
||||||
elif hLevel == "H2":
|
elif self._heading == "H2":
|
||||||
descKey = "doc_h2"
|
descKey = "doc_h2"
|
||||||
elif hLevel == "H3":
|
elif self._heading == "H3":
|
||||||
descKey = "doc_h3"
|
descKey = "doc_h3"
|
||||||
|
elif self._heading == "H4":
|
||||||
|
descKey = "doc_h4"
|
||||||
else:
|
else:
|
||||||
descKey = "document"
|
descKey = "document"
|
||||||
elif self._layout == nwItemLayout.NOTE:
|
elif self._layout == nwItemLayout.NOTE:
|
||||||
@@ -292,6 +268,22 @@ class NWItem():
|
|||||||
|
|
||||||
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
|
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
|
||||||
|
|
||||||
|
def getImportStatus(self, incIcon=True):
|
||||||
|
"""Return the relevant importance or status label and icon for
|
||||||
|
the current item based on its class.
|
||||||
|
"""
|
||||||
|
if self.isNovelLike():
|
||||||
|
stName = self._project.data.itemStatus.name(self._status)
|
||||||
|
stIcon = self._project.data.itemStatus.icon(self._status) if incIcon else None
|
||||||
|
else:
|
||||||
|
stName = self._project.data.itemImport.name(self._import)
|
||||||
|
stIcon = self._project.data.itemImport.icon(self._import) if incIcon else None
|
||||||
|
return stName, stIcon
|
||||||
|
|
||||||
|
##
|
||||||
|
# Checker Methods
|
||||||
|
##
|
||||||
|
|
||||||
def isNovelLike(self):
|
def isNovelLike(self):
|
||||||
"""Returns true if the item is of a novel-like class.
|
"""Returns true if the item is of a novel-like class.
|
||||||
"""
|
"""
|
||||||
@@ -307,17 +299,20 @@ class NWItem():
|
|||||||
"""
|
"""
|
||||||
return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH)
|
return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH)
|
||||||
|
|
||||||
def getImportStatus(self):
|
def isRootType(self):
|
||||||
"""Return the relevant importance or status label and icon for
|
return self._type == nwItemType.ROOT
|
||||||
the current item based on its class.
|
|
||||||
"""
|
def isFolderType(self):
|
||||||
if self.isNovelLike():
|
return self._type == nwItemType.FOLDER
|
||||||
stName = self.theProject.statusItems.name(self._status)
|
|
||||||
stIcon = self.theProject.statusItems.icon(self._status)
|
def isFileType(self):
|
||||||
else:
|
return self._type == nwItemType.FILE
|
||||||
stName = self.theProject.importItems.name(self._import)
|
|
||||||
stIcon = self.theProject.importItems.icon(self._import)
|
def isNoteLayout(self):
|
||||||
return stName, stIcon
|
return self._layout == nwItemLayout.NOTE
|
||||||
|
|
||||||
|
def isDocumentLayout(self):
|
||||||
|
return self._layout == nwItemLayout.DOCUMENT
|
||||||
|
|
||||||
##
|
##
|
||||||
# Special Setters
|
# Special Setters
|
||||||
@@ -419,8 +414,6 @@ class NWItem():
|
|||||||
self._type = value
|
self._type = value
|
||||||
elif isItemType(value):
|
elif isItemType(value):
|
||||||
self._type = nwItemType[value]
|
self._type = nwItemType[value]
|
||||||
elif value == "TRASH":
|
|
||||||
self._type = nwItemType.ROOT
|
|
||||||
else:
|
else:
|
||||||
logger.error("Unrecognised item type '%s'", value)
|
logger.error("Unrecognised item type '%s'", value)
|
||||||
self._type = nwItemType.NO_TYPE
|
self._type = nwItemType.NO_TYPE
|
||||||
@@ -447,8 +440,6 @@ class NWItem():
|
|||||||
self._layout = value
|
self._layout = value
|
||||||
elif isItemLayout(value):
|
elif isItemLayout(value):
|
||||||
self._layout = nwItemLayout[value]
|
self._layout = nwItemLayout[value]
|
||||||
elif value in ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"):
|
|
||||||
self._layout = nwItemLayout.DOCUMENT
|
|
||||||
else:
|
else:
|
||||||
logger.error("Unrecognised item layout '%s'", value)
|
logger.error("Unrecognised item layout '%s'", value)
|
||||||
self._layout = nwItemLayout.NO_LAYOUT
|
self._layout = nwItemLayout.NO_LAYOUT
|
||||||
@@ -458,60 +449,79 @@ class NWItem():
|
|||||||
"""Set the item status by looking it up in the valid status
|
"""Set the item status by looking it up in the valid status
|
||||||
items of the current project.
|
items of the current project.
|
||||||
"""
|
"""
|
||||||
self._status = self.theProject.statusItems.check(value)
|
self._status = self._project.data.itemStatus.check(value)
|
||||||
return
|
return
|
||||||
|
|
||||||
def setImport(self, value):
|
def setImport(self, value):
|
||||||
"""Set the item importance by looking it up in the valid import
|
"""Set the item importance by looking it up in the valid import
|
||||||
items of the current project.
|
items of the current project.
|
||||||
"""
|
"""
|
||||||
self._import = self.theProject.importItems.check(value)
|
self._import = self._project.data.itemImport.check(value)
|
||||||
|
return
|
||||||
|
|
||||||
|
def setActive(self, state):
|
||||||
|
"""Set the active flag.
|
||||||
|
"""
|
||||||
|
if isinstance(state, bool):
|
||||||
|
self._active = state
|
||||||
|
else:
|
||||||
|
self._active = False
|
||||||
return
|
return
|
||||||
|
|
||||||
def setExpanded(self, state):
|
def setExpanded(self, state):
|
||||||
"""Set the expanded status of an item in the project tree.
|
"""Set the expanded status of an item in the project tree.
|
||||||
"""
|
"""
|
||||||
if isinstance(state, str):
|
if isinstance(state, bool):
|
||||||
self._expanded = (state == str(True))
|
self._expanded = state
|
||||||
else:
|
else:
|
||||||
self._expanded = (state is True)
|
self._expanded = False
|
||||||
return
|
|
||||||
|
|
||||||
def setExported(self, state):
|
|
||||||
"""Set the export flag.
|
|
||||||
"""
|
|
||||||
if isinstance(state, str):
|
|
||||||
self._exported = (state == str(True))
|
|
||||||
else:
|
|
||||||
self._exported = (state is True)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Set Document Meta Data
|
# Set Document Meta Data
|
||||||
##
|
##
|
||||||
|
|
||||||
|
def setMainHeading(self, value):
|
||||||
|
"""Set the main heading level.
|
||||||
|
"""
|
||||||
|
if value in nwHeaders.H_LEVEL:
|
||||||
|
self._heading = value
|
||||||
|
return
|
||||||
|
|
||||||
def setCharCount(self, count):
|
def setCharCount(self, count):
|
||||||
"""Set the character count, and ensure that it is an integer.
|
"""Set the character count, and ensure that it is an integer.
|
||||||
"""
|
"""
|
||||||
self._charCount = max(0, checkInt(count, 0))
|
if isinstance(count, int):
|
||||||
|
self._charCount = max(0, count)
|
||||||
|
else:
|
||||||
|
self._charCount = 0
|
||||||
return
|
return
|
||||||
|
|
||||||
def setWordCount(self, count):
|
def setWordCount(self, count):
|
||||||
"""Set the word count, and ensure that it is an integer.
|
"""Set the word count, and ensure that it is an integer.
|
||||||
"""
|
"""
|
||||||
self._wordCount = max(0, checkInt(count, 0))
|
if isinstance(count, int):
|
||||||
|
self._wordCount = max(0, count)
|
||||||
|
else:
|
||||||
|
self._wordCount = 0
|
||||||
return
|
return
|
||||||
|
|
||||||
def setParaCount(self, count):
|
def setParaCount(self, count):
|
||||||
"""Set the paragraph count, and ensure that it is an integer.
|
"""Set the paragraph count, and ensure that it is an integer.
|
||||||
"""
|
"""
|
||||||
self._paraCount = max(0, checkInt(count, 0))
|
if isinstance(count, int):
|
||||||
|
self._paraCount = max(0, count)
|
||||||
|
else:
|
||||||
|
self._paraCount = 0
|
||||||
return
|
return
|
||||||
|
|
||||||
def setCursorPos(self, position):
|
def setCursorPos(self, position):
|
||||||
"""Set the cursor position, and ensure that it is an integer.
|
"""Set the cursor position, and ensure that it is an integer.
|
||||||
"""
|
"""
|
||||||
self._cursorPos = max(0, checkInt(position, 0))
|
if isinstance(position, int):
|
||||||
|
self._cursorPos = max(0, position)
|
||||||
|
else:
|
||||||
|
self._cursorPos = 0
|
||||||
return
|
return
|
||||||
|
|
||||||
def saveInitialCount(self):
|
def saveInitialCount(self):
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from novelwriter.error import logException
|
from novelwriter.error import logException
|
||||||
from novelwriter.common import checkBool, checkFloat, checkInt, checkString
|
from novelwriter.common import checkBool, checkFloat, checkInt, checkString
|
||||||
@@ -40,30 +40,30 @@ VALID_MAP = {
|
|||||||
"GuiWritingStats": {
|
"GuiWritingStats": {
|
||||||
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
|
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
|
||||||
"widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes",
|
"widthCol3", "sortCol", "sortOrder", "incNovel", "incNotes",
|
||||||
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax"
|
"hideZeros", "hideNegative", "groupByDay", "showIdleTime", "histMax",
|
||||||
},
|
},
|
||||||
"GuiDocSplit": {"spLevel"},
|
"GuiDocSplit": {"spLevel", "intoFolder", "docHierarchy"},
|
||||||
"GuiBuildNovel": {
|
"GuiBuildNovel": {
|
||||||
"winWidth", "winHeight", "boxWidth", "docWidth", "hideScene",
|
"winWidth", "winHeight", "boxWidth", "docWidth", "hideScene",
|
||||||
"hideSection", "addNovel", "addNotes", "ignoreFlag", "justifyText",
|
"hideSection", "addNovel", "addNotes", "ignoreFlag", "justifyText",
|
||||||
"excludeBody", "textFont", "textSize", "lineHeight", "noStyling",
|
"excludeBody", "textFont", "textSize", "lineHeight", "noStyling",
|
||||||
"incSynopsis", "incComments", "incKeywords", "incBodyText",
|
"incSynopsis", "incComments", "incKeywords", "incBodyText",
|
||||||
"replaceTabs", "replaceUCode"
|
"replaceTabs", "replaceUCode", "rootFilter",
|
||||||
},
|
},
|
||||||
"GuiOutline": {"headerOrder", "columnWidth", "columnHidden"},
|
"GuiOutline": {"headerOrder", "columnWidth", "columnHidden"},
|
||||||
"GuiProjectSettings": {
|
"GuiProjectSettings": {
|
||||||
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW"
|
"winWidth", "winHeight", "replaceColW", "statusColW", "importColW",
|
||||||
},
|
},
|
||||||
"GuiProjectDetails": {
|
"GuiProjectDetails": {
|
||||||
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
|
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
|
||||||
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble"
|
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble",
|
||||||
},
|
},
|
||||||
"GuiWordList": {"winWidth", "winHeight"},
|
"GuiWordList": {"winWidth", "winHeight"},
|
||||||
"GuiNovelView": {"lastCol"},
|
"GuiNovelView": {"lastCol"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class OptionState():
|
class OptionState:
|
||||||
|
|
||||||
def __init__(self, theProject):
|
def __init__(self, theProject):
|
||||||
self.theProject = theProject
|
self.theProject = theProject
|
||||||
@@ -77,13 +77,12 @@ class OptionState():
|
|||||||
def loadSettings(self):
|
def loadSettings(self):
|
||||||
"""Load the options dictionary from the project settings file.
|
"""Load the options dictionary from the project settings file.
|
||||||
"""
|
"""
|
||||||
if self.theProject.projMeta is None:
|
stateFile = self.theProject.storage.getMetaFile(nwFiles.OPTS_FILE)
|
||||||
|
if not isinstance(stateFile, Path):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
|
|
||||||
theState = {}
|
theState = {}
|
||||||
|
if stateFile.exists():
|
||||||
if os.path.isfile(stateFile):
|
|
||||||
logger.debug("Loading GUI options file")
|
logger.debug("Loading GUI options file")
|
||||||
try:
|
try:
|
||||||
with open(stateFile, mode="r", encoding="utf-8") as inFile:
|
with open(stateFile, mode="r", encoding="utf-8") as inFile:
|
||||||
@@ -106,12 +105,11 @@ class OptionState():
|
|||||||
def saveSettings(self):
|
def saveSettings(self):
|
||||||
"""Save the options dictionary to the project settings file.
|
"""Save the options dictionary to the project settings file.
|
||||||
"""
|
"""
|
||||||
if self.theProject.projMeta is None:
|
stateFile = self.theProject.storage.getMetaFile(nwFiles.OPTS_FILE)
|
||||||
|
if not isinstance(stateFile, Path):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
stateFile = os.path.join(self.theProject.projMeta, nwFiles.OPTS_FILE)
|
|
||||||
logger.debug("Saving GUI options file")
|
logger.debug("Saving GUI options file")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(stateFile, mode="w+", encoding="utf-8") as outFile:
|
with open(stateFile, mode="w+", encoding="utf-8") as outFile:
|
||||||
json.dump(self._theState, outFile, indent=2)
|
json.dump(self._theState, outFile, indent=2)
|
||||||
@@ -188,8 +186,7 @@ class OptionState():
|
|||||||
the default value.
|
the default value.
|
||||||
"""
|
"""
|
||||||
if group in self._theState:
|
if group in self._theState:
|
||||||
if name in self._theState[group]:
|
return checkBool(self._theState[group].get(name, default), default)
|
||||||
return checkBool(self._theState[group].get(name, default), default)
|
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def getEnum(self, group, name, lookup, default):
|
def getEnum(self, group, name, lookup, default):
|
||||||
|
|||||||
@@ -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/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from novelwriter.error import logException
|
from novelwriter.error import logException
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class NWSpellEnchant():
|
class NWSpellEnchant:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
@@ -46,36 +48,47 @@ class NWSpellEnchant():
|
|||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Getters and Setters
|
# Properties
|
||||||
##
|
##
|
||||||
|
|
||||||
|
@property
|
||||||
def spellLanguage(self):
|
def spellLanguage(self):
|
||||||
return self._spellLanguage
|
return self._spellLanguage
|
||||||
|
|
||||||
|
##
|
||||||
|
# Setters
|
||||||
|
##
|
||||||
|
|
||||||
def setLanguage(self, theLang, projectDict=None):
|
def setLanguage(self, theLang, projectDict=None):
|
||||||
"""Load a dictionary for the language specified in the config.
|
"""Load a dictionary for the language specified in the config.
|
||||||
If that fails, we load a mock dictionary so that lookups don't
|
If that fails, we load a mock dictionary so that lookups don't
|
||||||
crash.
|
crash. Note that enchant will allow loading an empty string as
|
||||||
|
a tag, but this will fail later on. See issue #1096.
|
||||||
"""
|
"""
|
||||||
|
self._theBroker = None
|
||||||
|
self._theDict = None
|
||||||
|
self._spellLanguage = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import enchant
|
import enchant
|
||||||
if self._theBroker is not None:
|
|
||||||
logger.debug("Deleting old pyenchant broker")
|
|
||||||
del self._theBroker
|
|
||||||
|
|
||||||
self._theBroker = enchant.Broker()
|
if theLang and enchant.dict_exists(theLang):
|
||||||
self._theDict = self._theBroker.request_dict(theLang)
|
self._theBroker = enchant.Broker()
|
||||||
self._spellLanguage = theLang
|
self._theDict = self._theBroker.request_dict(theLang)
|
||||||
logger.debug("Enchant spell checking for language '%s' loaded", theLang)
|
self._spellLanguage = theLang
|
||||||
|
logger.debug("Enchant spell checking for language '%s' loaded", theLang)
|
||||||
|
else:
|
||||||
|
logger.warning("Enchant found no dictionary for language '%s'", theLang)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Failed to load enchant spell checking for language '%s'", theLang)
|
logger.error("Failed to load enchant spell checking for language '%s'", theLang)
|
||||||
self._theDict = FakeEnchant()
|
|
||||||
self._spellLanguage = None
|
|
||||||
|
|
||||||
self._readProjectDictionary(projectDict)
|
if self._theDict is None:
|
||||||
for pWord in self._projDict:
|
self._theDict = FakeEnchant()
|
||||||
self._theDict.add_to_session(pWord)
|
else:
|
||||||
|
self._readProjectDictionary(projectDict)
|
||||||
|
for pWord in self._projDict:
|
||||||
|
self._theDict.add_to_session(pWord)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -160,10 +173,10 @@ class NWSpellEnchant():
|
|||||||
self._projDict = set()
|
self._projDict = set()
|
||||||
self._projectDict = projectDict
|
self._projectDict = projectDict
|
||||||
|
|
||||||
if projectDict is None:
|
if not isinstance(projectDict, Path):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not os.path.isfile(projectDict):
|
if not projectDict.exists():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -189,6 +202,9 @@ class FakeEnchant:
|
|||||||
"""Fallback for when Enchant is selected, but not installed.
|
"""Fallback for when Enchant is selected, but not installed.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.tag = ""
|
||||||
|
self.provider = namedtuple("provider", "name")
|
||||||
|
self.provider.name = ""
|
||||||
return
|
return
|
||||||
|
|
||||||
def check(self, theWord):
|
def check(self, theWord):
|
||||||
|
|||||||
@@ -28,16 +28,15 @@ import random
|
|||||||
import logging
|
import logging
|
||||||
import novelwriter
|
import novelwriter
|
||||||
|
|
||||||
from lxml import etree
|
from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor
|
||||||
|
from PyQt5.QtCore import QRectF, Qt
|
||||||
|
|
||||||
from PyQt5.QtGui import QIcon, QPixmap, QColor
|
from novelwriter.common import minmax, simplified
|
||||||
|
|
||||||
from novelwriter.common import checkInt, minmax, simplified
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class NWStatus():
|
class NWStatus:
|
||||||
|
|
||||||
STATUS = 1
|
STATUS = 1
|
||||||
IMPORT = 2
|
IMPORT = 2
|
||||||
@@ -46,13 +45,17 @@ class NWStatus():
|
|||||||
|
|
||||||
self._type = type
|
self._type = type
|
||||||
self._store = {}
|
self._store = {}
|
||||||
self._reverse = {}
|
|
||||||
self._default = None
|
self._default = None
|
||||||
|
|
||||||
self._iconSize = novelwriter.CONFIG.pxInt(32)
|
self._iPX = novelwriter.CONFIG.pxInt(24)
|
||||||
pixmap = QPixmap(self._iconSize, self._iconSize)
|
|
||||||
pixmap.fill(QColor(100, 100, 100))
|
pA = novelwriter.CONFIG.pxInt(2)
|
||||||
self._defaultIcon = QIcon(pixmap)
|
pB = novelwriter.CONFIG.pxInt(20)
|
||||||
|
pR = float(novelwriter.CONFIG.pxInt(4))
|
||||||
|
self._iconPath = QPainterPath()
|
||||||
|
self._iconPath.addRoundedRect(QRectF(pA, pA, pB, pB), pR, pR)
|
||||||
|
|
||||||
|
self._defaultIcon = self._createIcon(100, 100, 100)
|
||||||
|
|
||||||
if self._type == self.STATUS:
|
if self._type == self.STATUS:
|
||||||
self._prefix = "s"
|
self._prefix = "s"
|
||||||
@@ -63,31 +66,30 @@ class NWStatus():
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def write(self, key, name, cols, count=None):
|
def write(self, key, name, col, count=None):
|
||||||
"""Add or update a status entry. If the key is invalid, a new
|
"""Add or update a status entry. If the key is invalid, a new
|
||||||
key is generated.
|
key is generated.
|
||||||
"""
|
"""
|
||||||
if not self._isKey(key):
|
if not self._isKey(key):
|
||||||
key = self._newKey()
|
key = self._newKey()
|
||||||
if not isinstance(cols, tuple):
|
if not isinstance(col, tuple):
|
||||||
cols = (100, 100, 100)
|
col = (100, 100, 100)
|
||||||
if len(cols) != 3:
|
if len(col) != 3:
|
||||||
cols = (100, 100, 100)
|
col = (100, 100, 100)
|
||||||
|
|
||||||
pixmap = QPixmap(self._iconSize, self._iconSize)
|
|
||||||
pixmap.fill(QColor(*cols))
|
|
||||||
|
|
||||||
|
cR = minmax(col[0], 0, 255)
|
||||||
|
cG = minmax(col[1], 0, 255)
|
||||||
|
cB = minmax(col[2], 0, 255)
|
||||||
name = simplified(name)
|
name = simplified(name)
|
||||||
if count is None:
|
if count is None:
|
||||||
count = self._store[key]["count"] if key in self._store else 0
|
count = self._store.get(key, {}).get("count", 0)
|
||||||
|
|
||||||
self._store[key] = {
|
self._store[key] = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"icon": QIcon(pixmap),
|
"icon": self._createIcon(cR, cG, cB),
|
||||||
"cols": cols,
|
"cols": (cR, cG, cB),
|
||||||
"count": count,
|
"count": count,
|
||||||
}
|
}
|
||||||
self._reverse[name] = key
|
|
||||||
|
|
||||||
if self._default is None:
|
if self._default is None:
|
||||||
self._default = key
|
self._default = key
|
||||||
@@ -103,7 +105,6 @@ class NWStatus():
|
|||||||
if self._store[key]["count"] > 0:
|
if self._store[key]["count"] > 0:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
del self._reverse[self._store[key]["name"]]
|
|
||||||
del self._store[key]
|
del self._store[key]
|
||||||
|
|
||||||
keys = list(self._store.keys())
|
keys = list(self._store.keys())
|
||||||
@@ -120,8 +121,6 @@ class NWStatus():
|
|||||||
"""
|
"""
|
||||||
if self._isKey(value) and value in self._store:
|
if self._isKey(value) and value in self._store:
|
||||||
return value
|
return value
|
||||||
elif value in self._reverse:
|
|
||||||
return self._reverse[value]
|
|
||||||
elif self._default is not None:
|
elif self._default is not None:
|
||||||
return self._default
|
return self._default
|
||||||
else:
|
else:
|
||||||
@@ -203,37 +202,30 @@ class NWStatus():
|
|||||||
self._store[key]["count"] += 1
|
self._store[key]["count"] += 1
|
||||||
return
|
return
|
||||||
|
|
||||||
def packXML(self, xParent):
|
def pack(self):
|
||||||
"""Pack the status entries into an XML object for saving to the
|
"""Pack the status entries into a dictionary.
|
||||||
main project file.
|
|
||||||
"""
|
"""
|
||||||
for key, data in self._store.items():
|
for key, data in self._store.items():
|
||||||
xSub = etree.SubElement(xParent, "entry", attrib={
|
yield (data["name"], {
|
||||||
"key": key,
|
"key": key,
|
||||||
"count": str(data["count"]),
|
"count": str(data["count"]),
|
||||||
"red": str(data["cols"][0]),
|
"red": str(data["cols"][0]),
|
||||||
"green": str(data["cols"][1]),
|
"green": str(data["cols"][1]),
|
||||||
"blue": str(data["cols"][2]),
|
"blue": str(data["cols"][2]),
|
||||||
})
|
})
|
||||||
xSub.text = data["name"]
|
return
|
||||||
|
|
||||||
return True
|
def unpack(self, data):
|
||||||
|
"""Unpack a data dictionary and set the class values.
|
||||||
def unpackXML(self, xParent):
|
|
||||||
"""Unpack an XML tree and set the class values.
|
|
||||||
"""
|
"""
|
||||||
self._store = {}
|
self._store = {}
|
||||||
self._reverse = {}
|
|
||||||
self._default = None
|
self._default = None
|
||||||
|
|
||||||
for xChild in xParent:
|
for key, entry in data.items():
|
||||||
key = xChild.attrib.get("key", None)
|
label = entry.get("label", "")
|
||||||
name = xChild.text.strip()
|
colour = entry.get("colour", (100, 100, 100))
|
||||||
count = max(checkInt(xChild.attrib.get("count", 0), 0), 0)
|
count = entry.get("count", 0)
|
||||||
red = minmax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255)
|
self.write(key, label, colour, count)
|
||||||
green = minmax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255)
|
|
||||||
blue = minmax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255)
|
|
||||||
self.write(key, name, (red, green, blue), count)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -267,6 +259,19 @@ class NWStatus():
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _createIcon(self, red, green, blue):
|
||||||
|
"""Generate an icon for a status label.
|
||||||
|
"""
|
||||||
|
pixmap = QPixmap(self._iPX, self._iPX)
|
||||||
|
pixmap.fill(Qt.transparent)
|
||||||
|
|
||||||
|
painter = QPainter(pixmap)
|
||||||
|
painter.setRenderHint(QPainter.Antialiasing)
|
||||||
|
painter.fillPath(self._iconPath, QColor(red, green, blue))
|
||||||
|
painter.end()
|
||||||
|
|
||||||
|
return QIcon(pixmap)
|
||||||
|
|
||||||
##
|
##
|
||||||
# Iterator Bits
|
# Iterator Bits
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -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
|
M_EBOOK = 2 # Tweak output for converting to epub
|
||||||
|
|
||||||
def __init__(self, theProject):
|
def __init__(self, theProject):
|
||||||
Tokenizer.__init__(self, theProject)
|
super().__init__(theProject)
|
||||||
|
|
||||||
self._genMode = self.M_EXPORT
|
self._genMode = self.M_EXPORT
|
||||||
self._cssStyles = True
|
self._cssStyles = True
|
||||||
@@ -107,7 +107,7 @@ class ToHtml(Tokenizer):
|
|||||||
"""Extend the auto-replace to also properly encode some unicode
|
"""Extend the auto-replace to also properly encode some unicode
|
||||||
characters into their respective HTML entities.
|
characters into their respective HTML entities.
|
||||||
"""
|
"""
|
||||||
Tokenizer.doPreProcessing(self)
|
super().doPreProcessing()
|
||||||
self._theText = self._theText.translate(self._trMap)
|
self._theText = self._theText.translate(self._trMap)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -315,7 +315,7 @@ class ToHtml(Tokenizer):
|
|||||||
"</body>\n"
|
"</body>\n"
|
||||||
"</html>\n"
|
"</html>\n"
|
||||||
).format(
|
).format(
|
||||||
projTitle=self.theProject.projName,
|
projTitle=self.theProject.data.name,
|
||||||
htmlStyle="\n".join(theStyle),
|
htmlStyle="\n".join(theStyle),
|
||||||
bodyText=bodyText,
|
bodyText=bodyText,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ from PyQt5.QtCore import QCoreApplication, QRegularExpression
|
|||||||
from novelwriter.enum import nwItemLayout, nwItemType
|
from novelwriter.enum import nwItemLayout, nwItemType
|
||||||
from novelwriter.common import numberToRoman, checkInt
|
from novelwriter.common import numberToRoman, checkInt
|
||||||
from novelwriter.constants import nwConst, nwRegEx, nwUnicode
|
from novelwriter.constants import nwConst, nwRegEx, nwUnicode
|
||||||
from novelwriter.core.document import NWDoc
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -304,16 +303,10 @@ class Tokenizer(ABC):
|
|||||||
if self._theItem is None:
|
if self._theItem is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._theText = ""
|
if theText is None:
|
||||||
if theText is not None:
|
theText = self.theProject.storage.getDocument(theHandle).readDocument() or ""
|
||||||
# If the text is set, just use that
|
|
||||||
self._theText = theText
|
self._theText = theText
|
||||||
else:
|
|
||||||
# Otherwise, load it from file
|
|
||||||
theDoc = NWDoc(self.theProject, theHandle)
|
|
||||||
theText = theDoc.readDocument()
|
|
||||||
if theText:
|
|
||||||
self._theText = theText
|
|
||||||
|
|
||||||
docSize = len(self._theText)
|
docSize = len(self._theText)
|
||||||
if docSize > nwConst.MAX_DOCSIZE:
|
if docSize > nwConst.MAX_DOCSIZE:
|
||||||
@@ -333,9 +326,10 @@ class Tokenizer(ABC):
|
|||||||
"""Run trough the various replace doctionaries.
|
"""Run trough the various replace doctionaries.
|
||||||
"""
|
"""
|
||||||
# Process the user's auto-replace dictionary
|
# Process the user's auto-replace dictionary
|
||||||
if len(self.theProject.autoReplace) > 0:
|
autoReplace = self.theProject.data.autoReplace
|
||||||
|
if len(autoReplace) > 0:
|
||||||
repDict = {}
|
repDict = {}
|
||||||
for aKey, aVal in self.theProject.autoReplace.items():
|
for aKey, aVal in autoReplace.items():
|
||||||
repDict[f"<{aKey}>"] = aVal
|
repDict[f"<{aKey}>"] = aVal
|
||||||
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
|
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
|
||||||
self._theText = xRep.sub(lambda x: repDict[x.group(0)], self._theText)
|
self._theText = xRep.sub(lambda x: repDict[x.group(0)], self._theText)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class ToMarkdown(Tokenizer):
|
|||||||
M_GH = 1 # GitHub Markdown
|
M_GH = 1 # GitHub Markdown
|
||||||
|
|
||||||
def __init__(self, theProject):
|
def __init__(self, theProject):
|
||||||
Tokenizer.__init__(self, theProject)
|
super().__init__(theProject)
|
||||||
|
|
||||||
self._genMode = self.M_STD
|
self._genMode = self.M_STD
|
||||||
self._fullMD = []
|
self._fullMD = []
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ M_DEL = ~X_DEL
|
|||||||
class ToOdt(Tokenizer):
|
class ToOdt(Tokenizer):
|
||||||
|
|
||||||
def __init__(self, theProject, isFlat):
|
def __init__(self, theProject, isFlat):
|
||||||
Tokenizer.__init__(self, theProject)
|
super().__init__(theProject)
|
||||||
|
|
||||||
self._isFlat = isFlat # Flat: .fodt, otherwise .odt
|
self._isFlat = isFlat # Flat: .fodt, otherwise .odt
|
||||||
|
|
||||||
@@ -261,8 +261,8 @@ class ToOdt(Tokenizer):
|
|||||||
# ===============
|
# ===============
|
||||||
|
|
||||||
if self._headerText == "":
|
if self._headerText == "":
|
||||||
theTitle = self.theProject.bookTitle
|
theTitle = self.theProject.data.title
|
||||||
theAuth = self.theProject.getAuthors()
|
theAuth = self.theProject.getFormattedAuthors()
|
||||||
self._headerText = f"{theTitle} / {theAuth} /"
|
self._headerText = f"{theTitle} / {theAuth} /"
|
||||||
|
|
||||||
# Create Roots
|
# Create Roots
|
||||||
@@ -994,7 +994,7 @@ class ToOdt(Tokenizer):
|
|||||||
# Auto-Style Classes
|
# Auto-Style Classes
|
||||||
# =============================================================================================== #
|
# =============================================================================================== #
|
||||||
|
|
||||||
class ODTParagraphStyle():
|
class ODTParagraphStyle:
|
||||||
"""Wrapper class for the paragraph style setting used by the
|
"""Wrapper class for the paragraph style setting used by the
|
||||||
exporter. Only the used settings are exposed here to keep the class
|
exporter. Only the used settings are exposed here to keep the class
|
||||||
minimal and fast.
|
minimal and fast.
|
||||||
@@ -1208,7 +1208,7 @@ class ODTParagraphStyle():
|
|||||||
# END Class ODTParagraphStyle
|
# END Class ODTParagraphStyle
|
||||||
|
|
||||||
|
|
||||||
class ODTTextStyle():
|
class ODTTextStyle:
|
||||||
"""Wrapper class for the text style setting used by the exporter.
|
"""Wrapper class for the text style setting used by the exporter.
|
||||||
Only the used settings are exposed here to keep the class minimal
|
Only the used settings are exposed here to keep the class minimal
|
||||||
and fast.
|
and fast.
|
||||||
@@ -1297,7 +1297,7 @@ X_SPAN_TEXT = 2
|
|||||||
X_SPAN_SING = 3
|
X_SPAN_SING = 3
|
||||||
|
|
||||||
|
|
||||||
class XMLParagraph():
|
class XMLParagraph:
|
||||||
"""This is a helper class to manage the text content of a single
|
"""This is a helper class to manage the text content of a single
|
||||||
XML element using mixed content tags.
|
XML element using mixed content tags.
|
||||||
|
|
||||||
|
|||||||
@@ -23,13 +23,12 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import random
|
import random
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from lxml import etree
|
from pathlib import Path
|
||||||
|
|
||||||
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
|
from novelwriter.enum import nwItemClass, nwItemLayout
|
||||||
from novelwriter.error import logException
|
from novelwriter.error import logException
|
||||||
from novelwriter.common import checkHandle
|
from novelwriter.common import checkHandle
|
||||||
from novelwriter.constants import nwFiles
|
from novelwriter.constants import nwFiles
|
||||||
@@ -38,7 +37,7 @@ from novelwriter.core.item import NWItem
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class NWTree():
|
class NWTree:
|
||||||
|
|
||||||
MAX_DEPTH = 1000 # Cap of tree traversing for loops
|
MAX_DEPTH = 1000 # Cap of tree traversing for loops
|
||||||
|
|
||||||
@@ -89,20 +88,20 @@ class NWTree():
|
|||||||
logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
|
logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.verbose("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
|
logger.debug("Adding item '%s' with parent '%s'", str(tHandle), str(pHandle))
|
||||||
|
|
||||||
nwItem.setHandle(tHandle)
|
nwItem.setHandle(tHandle)
|
||||||
nwItem.setParent(pHandle)
|
nwItem.setParent(pHandle)
|
||||||
|
|
||||||
if nwItem.itemType == nwItemType.ROOT:
|
if nwItem.isRootType():
|
||||||
logger.verbose("Item '%s' is a root item", str(tHandle))
|
logger.debug("Item '%s' is a root item", str(tHandle))
|
||||||
self._treeRoots[tHandle] = nwItem
|
self._treeRoots[tHandle] = nwItem
|
||||||
if nwItem.itemClass == nwItemClass.ARCHIVE:
|
if nwItem.itemClass == nwItemClass.ARCHIVE:
|
||||||
logger.verbose("Item '%s' is the archive folder", str(tHandle))
|
logger.debug("Item '%s' is the archive folder", str(tHandle))
|
||||||
self._archRoot = tHandle
|
self._archRoot = tHandle
|
||||||
elif nwItem.itemClass == nwItemClass.TRASH:
|
elif nwItem.itemClass == nwItemClass.TRASH:
|
||||||
if self._trashRoot is None:
|
if self._trashRoot is None:
|
||||||
logger.verbose("Item '%s' is the trash folder", str(tHandle))
|
logger.debug("Item '%s' is the trash folder", str(tHandle))
|
||||||
self._trashRoot = tHandle
|
self._trashRoot = tHandle
|
||||||
else:
|
else:
|
||||||
logger.error("Only one trash folder allowed")
|
logger.error("Only one trash folder allowed")
|
||||||
@@ -114,30 +113,25 @@ class NWTree():
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def packXML(self, xParent):
|
def pack(self):
|
||||||
"""Pack the content of the tree into the provided XML object. In
|
"""Pack the content of the tree into the provided XML object. In
|
||||||
the order defined by the _treeOrder list.
|
the order defined by the _treeOrder list.
|
||||||
"""
|
"""
|
||||||
xContent = etree.SubElement(xParent, "content", attrib={
|
tree = []
|
||||||
"count": str(len(self._treeOrder))}
|
|
||||||
)
|
|
||||||
for tHandle in self._treeOrder:
|
for tHandle in self._treeOrder:
|
||||||
tItem = self.__getitem__(tHandle)
|
tItem = self.__getitem__(tHandle)
|
||||||
tItem.packXML(xContent)
|
if tItem:
|
||||||
return
|
tree.append(tItem.pack())
|
||||||
|
return tree
|
||||||
|
|
||||||
def unpackXML(self, xContent):
|
def unpack(self, data):
|
||||||
"""Iterate through all items of a content XML object and add
|
"""Iterate through all items of a list and add them to the
|
||||||
them to the project tree.
|
project tree.
|
||||||
"""
|
"""
|
||||||
if xContent.tag != "content":
|
|
||||||
logger.error("XML entry is not a NWTree")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.clear()
|
self.clear()
|
||||||
for xItem in xContent:
|
for item in data:
|
||||||
nwItem = NWItem(self.theProject)
|
nwItem = NWItem(self.theProject)
|
||||||
if nwItem.unpackXML(xItem):
|
if nwItem.unpack(item):
|
||||||
self.append(nwItem.itemHandle, nwItem.itemParent, nwItem)
|
self.append(nwItem.itemHandle, nwItem.itemParent, nwItem)
|
||||||
nwItem.saveInitialCount()
|
nwItem.saveInitialCount()
|
||||||
|
|
||||||
@@ -147,16 +141,22 @@ class NWTree():
|
|||||||
"""Write the convenience table of contents file in the root of
|
"""Write the convenience table of contents file in the root of
|
||||||
the project directory.
|
the project directory.
|
||||||
"""
|
"""
|
||||||
|
runtimePath = self.theProject.storage.runtimePath
|
||||||
|
contentPath = self.theProject.storage.contentPath
|
||||||
|
if not (isinstance(contentPath, Path) and isinstance(runtimePath, Path)):
|
||||||
|
return False
|
||||||
|
|
||||||
tocList = []
|
tocList = []
|
||||||
tocLen = 0
|
tocLen = 0
|
||||||
for tHandle in self._treeOrder:
|
for tHandle in self._treeOrder:
|
||||||
tItem = self.__getitem__(tHandle)
|
tItem = self.__getitem__(tHandle)
|
||||||
if tItem is None:
|
if tItem is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tFile = tHandle+".nwd"
|
tFile = tHandle+".nwd"
|
||||||
if os.path.isfile(os.path.join(self.theProject.projContent, tFile)):
|
if (contentPath / tFile).is_file():
|
||||||
tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format(
|
tocLine = "{0:<25s} {1:<9s} {2:<8s} {3:s}".format(
|
||||||
os.path.join("content", tFile),
|
str(Path("content") / tFile),
|
||||||
tItem.itemClass.name,
|
tItem.itemClass.name,
|
||||||
tItem.itemLayout.name,
|
tItem.itemLayout.name,
|
||||||
tItem.itemName,
|
tItem.itemName,
|
||||||
@@ -166,7 +166,7 @@ class NWTree():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Dump the text
|
# Dump the text
|
||||||
tocText = os.path.join(self.theProject.projPath, nwFiles.TOC_TXT)
|
tocText = runtimePath / nwFiles.TOC_TXT
|
||||||
with open(tocText, mode="w", encoding="utf-8") as outFile:
|
with open(tocText, mode="w", encoding="utf-8") as outFile:
|
||||||
outFile.write("\n")
|
outFile.write("\n")
|
||||||
outFile.write("Table of Contents\n")
|
outFile.write("Table of Contents\n")
|
||||||
@@ -274,11 +274,13 @@ class NWTree():
|
|||||||
return rootClasses
|
return rootClasses
|
||||||
|
|
||||||
def iterRoots(self, itemClass):
|
def iterRoots(self, itemClass):
|
||||||
"""Iterate over all items of a given class.
|
"""Iterate over all root items of a given class in order.
|
||||||
"""
|
"""
|
||||||
for tHandle, nwItem in self._treeRoots.items():
|
for tHandle in self._treeOrder:
|
||||||
if nwItem.itemClass == itemClass:
|
nwItem = self.__getitem__(tHandle)
|
||||||
yield tHandle, nwItem
|
if nwItem is not None and nwItem.isRootType():
|
||||||
|
if itemClass is None or nwItem.itemClass == itemClass:
|
||||||
|
yield tHandle, nwItem
|
||||||
return
|
return
|
||||||
|
|
||||||
def isRoot(self, tHandle):
|
def isRoot(self, tHandle):
|
||||||
@@ -329,25 +331,20 @@ class NWTree():
|
|||||||
def setOrder(self, newOrder):
|
def setOrder(self, newOrder):
|
||||||
"""Reorders the tree based on a list of items.
|
"""Reorders the tree based on a list of items.
|
||||||
"""
|
"""
|
||||||
tmpOrder = []
|
tmpOrder = [tHandle for tHandle in newOrder if tHandle in self._projTree]
|
||||||
|
if not (len(tmpOrder) == len(newOrder) == len(self._treeOrder)):
|
||||||
# Add all known elements to a new temp list
|
# Something is wrong, so let's debug it
|
||||||
for tHandle in newOrder:
|
for tHandle in newOrder:
|
||||||
if tHandle in self._projTree:
|
if tHandle not in self._projTree:
|
||||||
tmpOrder.append(tHandle)
|
logger.error("Handle '%s' in new tree order is not in old order", tHandle)
|
||||||
else:
|
for tHandle in self._treeOrder:
|
||||||
logger.error("Handle '%s' in new tree order is not in project tree", tHandle)
|
if tHandle not in tmpOrder:
|
||||||
|
logger.warning("Handle '%s' in old tree order is not in new order", tHandle)
|
||||||
# Do a reverse lookup to check for items that will be lost
|
|
||||||
# This is mainly for debugging purposes
|
|
||||||
for tHandle in self._treeOrder:
|
|
||||||
if tHandle not in tmpOrder:
|
|
||||||
logger.warning("Handle '%s' in old tree order is not in new tree order", tHandle)
|
|
||||||
|
|
||||||
# Save the temp list
|
# Save the temp list
|
||||||
self._treeOrder = tmpOrder
|
self._treeOrder = tmpOrder
|
||||||
self._setTreeChanged(True)
|
self._setTreeChanged(True)
|
||||||
logger.verbose("Project tree order updated")
|
logger.debug("Project tree order updated")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -357,7 +354,7 @@ class NWTree():
|
|||||||
tItem = self.__getitem__(tHandle)
|
tItem = self.__getitem__(tHandle)
|
||||||
if tItem is None:
|
if tItem is None:
|
||||||
return False
|
return False
|
||||||
if tItem.itemType != nwItemType.FILE:
|
if not tItem.isFileType():
|
||||||
logger.error("Item '%s' is not a file", tHandle)
|
logger.error("Item '%s' is not a file", tHandle)
|
||||||
return False
|
return False
|
||||||
if not isinstance(itemLayout, nwItemLayout):
|
if not isinstance(itemLayout, nwItemLayout):
|
||||||
@@ -457,7 +454,7 @@ class NWTree():
|
|||||||
"""Generate a unique item handle. In the event that the key
|
"""Generate a unique item handle. In the event that the key
|
||||||
already exists, generate a new one.
|
already exists, generate a new one.
|
||||||
"""
|
"""
|
||||||
logger.verbose("Generating new handle")
|
logger.debug("Generating new handle")
|
||||||
handle = f"{random.getrandbits(52):013x}"
|
handle = f"{random.getrandbits(52):013x}"
|
||||||
if handle in self._projTree:
|
if handle in self._projTree:
|
||||||
logger.warning("Duplicate handle encountered! Retrying ...")
|
logger.warning("Duplicate handle encountered! Retrying ...")
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ class QConfigLayout(QGridLayout):
|
|||||||
class QHelpLabel(QLabel):
|
class QHelpLabel(QLabel):
|
||||||
|
|
||||||
def __init__(self, theText, textCol, fontSize=0.9):
|
def __init__(self, theText, textCol, fontSize=0.9):
|
||||||
QLabel.__init__(self, theText)
|
super().__init__(theText)
|
||||||
|
|
||||||
if isinstance(textCol, QColor):
|
if isinstance(textCol, QColor):
|
||||||
qCol = textCol
|
qCol = textCol
|
||||||
@@ -267,8 +267,8 @@ class QSwitch(QAbstractButton):
|
|||||||
return self._offset
|
return self._offset
|
||||||
|
|
||||||
@offset.setter
|
@offset.setter
|
||||||
def offset(self, theOffset):
|
def offset(self, offset):
|
||||||
self._offset = theOffset
|
self._offset = offset
|
||||||
self.update()
|
self.update()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -276,11 +276,11 @@ class QSwitch(QAbstractButton):
|
|||||||
# Getters and Setters
|
# Getters and Setters
|
||||||
##
|
##
|
||||||
|
|
||||||
def setChecked(self, isChecked):
|
def setChecked(self, checked):
|
||||||
"""Overload setChecked to also alter the offset.
|
"""Overload setChecked to also alter the offset.
|
||||||
"""
|
"""
|
||||||
super().setChecked(isChecked)
|
super().setChecked(checked)
|
||||||
if isChecked:
|
if checked:
|
||||||
self.offset = self._xW - self._xR
|
self.offset = self._xW - self._xR
|
||||||
else:
|
else:
|
||||||
self.offset = self._xR
|
self.offset = self._xR
|
||||||
@@ -290,10 +290,10 @@ class QSwitch(QAbstractButton):
|
|||||||
# Events
|
# Events
|
||||||
##
|
##
|
||||||
|
|
||||||
def resizeEvent(self, theEvent):
|
def resizeEvent(self, event):
|
||||||
"""Overload resize to ensure correct offset.
|
"""Overload resize to ensure correct offset.
|
||||||
"""
|
"""
|
||||||
super().resizeEvent(theEvent)
|
super().resizeEvent(event)
|
||||||
if self.isChecked():
|
if self.isChecked():
|
||||||
self.offset = self._xW - self._xR
|
self.offset = self._xW - self._xR
|
||||||
else:
|
else:
|
||||||
@@ -377,7 +377,7 @@ class QSwitch(QAbstractButton):
|
|||||||
class PagedDialog(QDialog):
|
class PagedDialog(QDialog):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QDialog.__init__(self, parent=parent)
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
self._tabBar = VerticalTabBar(self)
|
self._tabBar = VerticalTabBar(self)
|
||||||
self._tabBar.setExpanding(False)
|
self._tabBar.setExpanding(False)
|
||||||
@@ -410,31 +410,37 @@ class PagedDialog(QDialog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def addTab(self, widget, label):
|
def addTab(self, widget, label):
|
||||||
"""Forwards the adding of tabs to the QTabWidget.
|
"""Forward the adding of tabs to the QTabWidget.
|
||||||
"""
|
"""
|
||||||
self._tabBox.addTab(widget, label)
|
self._tabBox.addTab(widget, label)
|
||||||
return
|
return
|
||||||
|
|
||||||
def addControls(self, buttonBar):
|
def addControls(self, buttonBar):
|
||||||
"""Adds a button bar to the dialog.
|
"""Add a button bar to the dialog.
|
||||||
"""
|
"""
|
||||||
self._buttonBox.addWidget(buttonBar)
|
self._buttonBox.addWidget(buttonBar)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def setCurrentWidget(self, widget):
|
||||||
|
"""Forward the changing of tab to the QTabWidget.
|
||||||
|
"""
|
||||||
|
self._tabBox.setCurrentWidget(widget)
|
||||||
|
return
|
||||||
|
|
||||||
# END Class PagedDialog
|
# END Class PagedDialog
|
||||||
|
|
||||||
|
|
||||||
class VerticalTabBar(QTabBar):
|
class VerticalTabBar(QTabBar):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
QTabBar.__init__(self, parent=parent)
|
super().__init__(parent=parent)
|
||||||
self._mW = novelwriter.CONFIG.pxInt(150)
|
self._mW = novelwriter.CONFIG.pxInt(150)
|
||||||
return
|
return
|
||||||
|
|
||||||
def tabSizeHint(self, index):
|
def tabSizeHint(self, index):
|
||||||
"""Returns a transposed size hint for the rotated bar.
|
"""Return a transposed size hint for the rotated bar.
|
||||||
"""
|
"""
|
||||||
tSize = QTabBar.tabSizeHint(self, index)
|
tSize = super().tabSizeHint(index)
|
||||||
tSize.transpose()
|
tSize.transpose()
|
||||||
tSize.setWidth(min(tSize.width(), self._mW))
|
tSize.setWidth(min(tSize.width(), self._mW))
|
||||||
return tSize
|
return tSize
|
||||||
@@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
import novelwriter
|
import novelwriter
|
||||||
|
|
||||||
@@ -44,7 +43,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class GuiAbout(QDialog):
|
class GuiAbout(QDialog):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, mainGui):
|
||||||
QDialog.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
logger.debug("Initialising GuiAbout ...")
|
logger.debug("Initialising GuiAbout ...")
|
||||||
self.setObjectName("GuiAbout")
|
self.setObjectName("GuiAbout")
|
||||||
@@ -234,7 +233,7 @@ class GuiAbout(QDialog):
|
|||||||
def _fillNotesPage(self):
|
def _fillNotesPage(self):
|
||||||
"""Load the content for the Release Notes page.
|
"""Load the content for the Release Notes page.
|
||||||
"""
|
"""
|
||||||
docPath = os.path.join(self.mainConf.assetPath, "text", "release_notes.htm")
|
docPath = self.mainConf.assetPath("text") / "release_notes.htm"
|
||||||
docText = readTextFile(docPath)
|
docText = readTextFile(docPath)
|
||||||
if docText:
|
if docText:
|
||||||
self.pageNotes.setHtml(docText)
|
self.pageNotes.setHtml(docText)
|
||||||
@@ -245,7 +244,7 @@ class GuiAbout(QDialog):
|
|||||||
def _fillLicensePage(self):
|
def _fillLicensePage(self):
|
||||||
"""Load the content for the Licence page.
|
"""Load the content for the Licence page.
|
||||||
"""
|
"""
|
||||||
docPath = os.path.join(self.mainConf.assetPath, "text", "gplv3_en.htm")
|
docPath = self.mainConf.assetPath("text") / "gplv3_en.htm"
|
||||||
docText = readTextFile(docPath)
|
docText = readTextFile(docPath)
|
||||||
if docText:
|
if docText:
|
||||||
self.pageLicense.setHtml(docText)
|
self.pageLicense.setHtml(docText)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
novelWriter – GUI Doc Merge Tool
|
novelWriter – GUI Doc Merge Dialog
|
||||||
================================
|
==================================
|
||||||
GUI class for merging multiple documents to one document
|
Custom dialog class for merging documents.
|
||||||
|
|
||||||
File History:
|
File History:
|
||||||
Created: 2020-01-23 [0.4.3]
|
Created: 2020-01-23 [0.4.3]
|
||||||
|
Rewritten: 2022-10-06 [2.0b1]
|
||||||
|
|
||||||
This file is a part of novelWriter
|
This file is a part of novelWriter
|
||||||
Copyright 2018–2022, Veronica Berglyd Olsen
|
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 logging
|
||||||
import novelwriter
|
import novelwriter
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt, QSize
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QLabel, QListWidget, QAbstractItemView,
|
QAbstractItemView, QDialog, QDialogButtonBox, QGridLayout, QLabel,
|
||||||
QListWidgetItem, QDialogButtonBox
|
QListWidget, QListWidgetItem, QVBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter.core import NWDoc
|
from novelwriter.custom import QHelpLabel, QSwitch
|
||||||
from novelwriter.enum import nwAlert, nwItemType
|
|
||||||
from novelwriter.gui.custom import QHelpLabel
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class GuiDocMerge(QDialog):
|
class GuiDocMerge(QDialog):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, mainGui, sHandle, itemList):
|
||||||
QDialog.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
logger.debug("Initialising GuiDocMerge ...")
|
logger.debug("Initialising GuiDocMerge ...")
|
||||||
self.setObjectName("GuiDocMerge")
|
self.setObjectName("GuiDocMerge")
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.mainGui = mainGui
|
||||||
|
self.mainTheme = mainGui.mainTheme
|
||||||
self.theProject = mainGui.theProject
|
self.theProject = mainGui.theProject
|
||||||
self.sourceItem = None
|
|
||||||
|
|
||||||
self.outerBox = QVBoxLayout()
|
self._data = {}
|
||||||
|
|
||||||
self.setWindowTitle(self.tr("Merge Documents"))
|
self.setWindowTitle(self.tr("Merge Documents"))
|
||||||
|
|
||||||
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge")))
|
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge")))
|
||||||
self.helpLabel = QHelpLabel(
|
self.helpLabel = QHelpLabel(self.tr(
|
||||||
self.tr("Drag and drop items to change the order."), self.mainGui.mainTheme.helpText
|
"Drag and drop items to change the order, or uncheck to exclude."
|
||||||
)
|
), self.mainTheme.helpText)
|
||||||
|
|
||||||
|
iPx = self.mainTheme.baseIconSize
|
||||||
|
hSp = self.mainConf.pxInt(12)
|
||||||
|
vSp = self.mainConf.pxInt(8)
|
||||||
|
bSp = self.mainConf.pxInt(12)
|
||||||
|
|
||||||
self.listBox = QListWidget()
|
self.listBox = QListWidget()
|
||||||
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
|
self.listBox.setIconSize(QSize(iPx, iPx))
|
||||||
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
|
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
|
||||||
self.listBox.setMinimumHeight(self.mainConf.pxInt(180))
|
self.listBox.setMinimumHeight(self.mainConf.pxInt(180))
|
||||||
|
self.listBox.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
|
self.listBox.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||||
|
self.listBox.setDragDropMode(QAbstractItemView.InternalMove)
|
||||||
|
|
||||||
|
# Merge Options
|
||||||
|
self.trashLabel = QLabel(self.tr("Move merged items to Trash"))
|
||||||
|
self.trashSwitch = QSwitch(width=2*iPx, height=iPx)
|
||||||
|
|
||||||
|
self.optBox = QGridLayout()
|
||||||
|
self.optBox.addWidget(self.trashLabel, 0, 0)
|
||||||
|
self.optBox.addWidget(self.trashSwitch, 0, 1)
|
||||||
|
self.optBox.setHorizontalSpacing(hSp)
|
||||||
|
self.optBox.setColumnStretch(2, 1)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||||
self.buttonBox.accepted.connect(self._doMerge)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self._doClose)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
|
|
||||||
|
self.resetButton = self.buttonBox.addButton(QDialogButtonBox.Reset)
|
||||||
|
self.resetButton.clicked.connect(self._resetList)
|
||||||
|
|
||||||
|
# Assemble
|
||||||
|
self.outerBox = QVBoxLayout()
|
||||||
self.outerBox.setSpacing(0)
|
self.outerBox.setSpacing(0)
|
||||||
self.outerBox.addWidget(self.headLabel)
|
self.outerBox.addWidget(self.headLabel)
|
||||||
self.outerBox.addWidget(self.helpLabel)
|
self.outerBox.addWidget(self.helpLabel)
|
||||||
self.outerBox.addSpacing(self.mainConf.pxInt(8))
|
self.outerBox.addSpacing(vSp)
|
||||||
self.outerBox.addWidget(self.listBox)
|
self.outerBox.addWidget(self.listBox)
|
||||||
self.outerBox.addSpacing(self.mainConf.pxInt(12))
|
self.outerBox.addSpacing(vSp)
|
||||||
|
self.outerBox.addLayout(self.optBox)
|
||||||
|
self.outerBox.addSpacing(bSp)
|
||||||
self.outerBox.addWidget(self.buttonBox)
|
self.outerBox.addWidget(self.buttonBox)
|
||||||
self.setLayout(self.outerBox)
|
self.setLayout(self.outerBox)
|
||||||
|
|
||||||
self.rejected.connect(self._doClose)
|
# Load Content
|
||||||
|
self._loadContent(sHandle, itemList)
|
||||||
self._populateList()
|
|
||||||
|
|
||||||
logger.debug("GuiDocMerge initialisation complete")
|
logger.debug("GuiDocMerge initialisation complete")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
def getData(self):
|
||||||
# Buttons
|
"""Return the user's choices.
|
||||||
##
|
|
||||||
|
|
||||||
def _doMerge(self):
|
|
||||||
"""Perform the merge of the files in the selected folder, and
|
|
||||||
create a new file in the same parent folder. The old files are
|
|
||||||
not removed in the merge process, and must be deleted manually.
|
|
||||||
"""
|
"""
|
||||||
logger.verbose("GuiDocMerge merge button clicked")
|
finalItems = []
|
||||||
|
|
||||||
finalOrder = []
|
|
||||||
for i in range(self.listBox.count()):
|
for i in range(self.listBox.count()):
|
||||||
finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
|
item = self.listBox.item(i)
|
||||||
|
if item is not None and item.checkState() == Qt.Checked:
|
||||||
|
finalItems.append(item.data(Qt.UserRole))
|
||||||
|
|
||||||
if len(finalOrder) == 0:
|
self._data["moveToTrash"] = self.trashSwitch.isChecked()
|
||||||
self.mainGui.makeAlert(self.tr(
|
self._data["finalItems"] = finalItems
|
||||||
"No source documents found. Nothing to do."
|
|
||||||
), nwAlert.ERROR)
|
|
||||||
return False
|
|
||||||
|
|
||||||
theText = ""
|
return self._data
|
||||||
for tHandle in finalOrder:
|
|
||||||
inDoc = NWDoc(self.theProject, tHandle)
|
|
||||||
docText = inDoc.readDocument()
|
|
||||||
docErr = inDoc.getError()
|
|
||||||
if docText is None and docErr:
|
|
||||||
self.mainGui.makeAlert([
|
|
||||||
self.tr("Failed to open document file."), docErr
|
|
||||||
], nwAlert.ERROR)
|
|
||||||
if docText:
|
|
||||||
theText += docText.rstrip("\n")+"\n\n"
|
|
||||||
|
|
||||||
if self.sourceItem is None:
|
##
|
||||||
self.mainGui.makeAlert(self.tr(
|
# Slots
|
||||||
"No source folder selected. Nothing to do."
|
##
|
||||||
), nwAlert.ERROR)
|
|
||||||
return False
|
|
||||||
|
|
||||||
srcItem = self.theProject.tree[self.sourceItem]
|
def _resetList(self):
|
||||||
if srcItem is None:
|
"""Reset the content of the list box to its original state.
|
||||||
self.mainGui.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
|
|
||||||
return False
|
|
||||||
|
|
||||||
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent)
|
|
||||||
newItem = self.theProject.tree[nHandle]
|
|
||||||
newItem.setStatus(srcItem.itemStatus)
|
|
||||||
newItem.setImport(srcItem.itemImport)
|
|
||||||
|
|
||||||
outDoc = NWDoc(self.theProject, nHandle)
|
|
||||||
if not outDoc.writeDocument(theText):
|
|
||||||
self.mainGui.makeAlert([
|
|
||||||
self.tr("Could not save document."), outDoc.getError()
|
|
||||||
], nwAlert.ERROR)
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.mainGui.projView.revealNewTreeItem(nHandle)
|
|
||||||
self.mainGui.openDocument(nHandle, doScroll=True)
|
|
||||||
|
|
||||||
self._doClose()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _doClose(self):
|
|
||||||
"""Close the dialog window without doing anything.
|
|
||||||
"""
|
"""
|
||||||
self.close()
|
logger.debug("Resetting list box content")
|
||||||
|
sHandle = self._data.get("sHandle", None)
|
||||||
|
itemList = self._data.get("origItems", [])
|
||||||
|
self._loadContent(sHandle, itemList)
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
def _populateList(self):
|
def _loadContent(self, sHandle, itemList):
|
||||||
"""Get the item selected in the tree, check that it is a folder,
|
"""Load content from a given list of items.
|
||||||
and try to find all files associated with it. The valid files
|
|
||||||
are then added to the list view in order. The list itself can be
|
|
||||||
reordered by the user.
|
|
||||||
"""
|
"""
|
||||||
tHandle = self.mainGui.projView.getSelectedHandle()
|
self._data = {}
|
||||||
self.sourceItem = tHandle
|
self._data["sHandle"] = sHandle
|
||||||
if tHandle is None:
|
self._data["origItems"] = itemList
|
||||||
return False
|
|
||||||
|
|
||||||
nwItem = self.theProject.tree[tHandle]
|
self.listBox.clear()
|
||||||
if nwItem is None:
|
for tHandle in itemList:
|
||||||
return False
|
nwItem = self.theProject.tree[tHandle]
|
||||||
|
if nwItem is None or not nwItem.isFileType():
|
||||||
if nwItem.itemType is not nwItemType.FOLDER:
|
|
||||||
self.mainGui.makeAlert(self.tr(
|
|
||||||
"Element selected in the project tree must be a folder."
|
|
||||||
), nwAlert.ERROR)
|
|
||||||
return False
|
|
||||||
|
|
||||||
for sHandle in self.mainGui.projView.getTreeFromHandle(tHandle):
|
|
||||||
newItem = QListWidgetItem()
|
|
||||||
nwItem = self.theProject.tree[sHandle]
|
|
||||||
if nwItem.itemType is not nwItemType.FILE:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
itemIcon = self.mainTheme.getItemIcon(
|
||||||
|
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading
|
||||||
|
)
|
||||||
|
|
||||||
|
newItem = QListWidgetItem()
|
||||||
|
newItem.setIcon(itemIcon)
|
||||||
newItem.setText(nwItem.itemName)
|
newItem.setText(nwItem.itemName)
|
||||||
newItem.setData(Qt.UserRole, sHandle)
|
newItem.setData(Qt.UserRole, tHandle)
|
||||||
|
newItem.setCheckState(Qt.Checked)
|
||||||
|
|
||||||
self.listBox.addItem(newItem)
|
self.listBox.addItem(newItem)
|
||||||
|
|
||||||
return True
|
return
|
||||||
|
|
||||||
# END Class GuiDocMerge
|
# END Class GuiDocMerge
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
novelWriter – GUI Doc Split Tool
|
novelWriter – GUI Doc Split Dialog
|
||||||
================================
|
==================================
|
||||||
GUI class for splitting a single document into multiple documents
|
Custom dialog class for splitting documents.
|
||||||
|
|
||||||
File History:
|
File History:
|
||||||
Created: 2020-02-01 [0.4.3]
|
Created: 2020-02-01 [0.4.3]
|
||||||
|
Rewritten: 2022-10-12 [2.0b1]
|
||||||
|
|
||||||
This file is a part of novelWriter
|
This file is a part of novelWriter
|
||||||
Copyright 2018–2022, Veronica Berglyd Olsen
|
Copyright 2018–2022, Veronica Berglyd Olsen
|
||||||
@@ -29,32 +30,34 @@ import novelwriter
|
|||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QComboBox, QListWidget, QAbstractItemView,
|
QDialog, QVBoxLayout, QComboBox, QListWidget, QAbstractItemView,
|
||||||
QListWidgetItem, QDialogButtonBox, QLabel
|
QListWidgetItem, QDialogButtonBox, QLabel, QGridLayout
|
||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter.core import NWDoc
|
from novelwriter.custom import QHelpLabel, QSwitch
|
||||||
from novelwriter.enum import nwAlert, nwItemType
|
|
||||||
from novelwriter.gui.custom import QHelpLabel
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class GuiDocSplit(QDialog):
|
class GuiDocSplit(QDialog):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
LINE_ROLE = Qt.UserRole
|
||||||
QDialog.__init__(self, mainGui)
|
LEVEL_ROLE = Qt.UserRole + 1
|
||||||
|
LABEL_ROLE = Qt.UserRole + 2
|
||||||
|
|
||||||
|
def __init__(self, mainGui, sHandle):
|
||||||
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
logger.debug("Initialising GuiDocSplit ...")
|
logger.debug("Initialising GuiDocSplit ...")
|
||||||
self.setObjectName("GuiDocSplit")
|
self.setObjectName("GuiDocSplit")
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.mainGui = mainGui
|
||||||
|
self.mainTheme = mainGui.mainTheme
|
||||||
self.theProject = mainGui.theProject
|
self.theProject = mainGui.theProject
|
||||||
|
|
||||||
self.sourceItem = None
|
self._data = {}
|
||||||
self.sourceText = []
|
self._text = []
|
||||||
|
|
||||||
self.outerBox = QVBoxLayout()
|
|
||||||
self.setWindowTitle(self.tr("Split Document"))
|
self.setWindowTitle(self.tr("Split Document"))
|
||||||
|
|
||||||
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Document Headers")))
|
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Document Headers")))
|
||||||
@@ -63,6 +66,18 @@ class GuiDocSplit(QDialog):
|
|||||||
self.mainGui.mainTheme.helpText
|
self.mainGui.mainTheme.helpText
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Values
|
||||||
|
iPx = self.mainTheme.baseIconSize
|
||||||
|
hSp = self.mainConf.pxInt(12)
|
||||||
|
vSp = self.mainConf.pxInt(8)
|
||||||
|
bSp = self.mainConf.pxInt(12)
|
||||||
|
|
||||||
|
pOptions = self.theProject.options
|
||||||
|
spLevel = pOptions.getInt("GuiDocSplit", "spLevel", 3)
|
||||||
|
intoFolder = pOptions.getBool("GuiDocSplit", "intoFolder", True)
|
||||||
|
docHierarchy = pOptions.getBool("GuiDocSplit", "docHierarchy", True)
|
||||||
|
|
||||||
|
# Header Selection
|
||||||
self.listBox = QListWidget()
|
self.listBox = QListWidget()
|
||||||
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
|
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
|
||||||
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
|
self.listBox.setMinimumWidth(self.mainConf.pxInt(400))
|
||||||
@@ -73,206 +88,162 @@ class GuiDocSplit(QDialog):
|
|||||||
self.splitLevel.addItem(self.tr("Split up to Header Level 2 (Chapter)"), 2)
|
self.splitLevel.addItem(self.tr("Split up to Header Level 2 (Chapter)"), 2)
|
||||||
self.splitLevel.addItem(self.tr("Split up to Header Level 3 (Scene)"), 3)
|
self.splitLevel.addItem(self.tr("Split up to Header Level 3 (Scene)"), 3)
|
||||||
self.splitLevel.addItem(self.tr("Split up to Header Level 4 (Section)"), 4)
|
self.splitLevel.addItem(self.tr("Split up to Header Level 4 (Section)"), 4)
|
||||||
spIndex = self.splitLevel.findData(
|
spIndex = self.splitLevel.findData(spLevel)
|
||||||
self.theProject.options.getInt("GuiDocSplit", "spLevel", 3)
|
|
||||||
)
|
|
||||||
if spIndex != -1:
|
if spIndex != -1:
|
||||||
self.splitLevel.setCurrentIndex(spIndex)
|
self.splitLevel.setCurrentIndex(spIndex)
|
||||||
self.splitLevel.currentIndexChanged.connect(self._populateList)
|
self.splitLevel.currentIndexChanged.connect(self._reloadList)
|
||||||
|
|
||||||
|
# Split Options
|
||||||
|
self.folderLabel = QLabel(self.tr("Split into a new folder"))
|
||||||
|
self.folderSwitch = QSwitch(width=2*iPx, height=iPx)
|
||||||
|
self.folderSwitch.setChecked(intoFolder)
|
||||||
|
|
||||||
|
self.hierarchyLabel = QLabel(self.tr("Create document hierarchy"))
|
||||||
|
self.hierarchySwitch = QSwitch(width=2*iPx, height=iPx)
|
||||||
|
self.hierarchySwitch.setChecked(docHierarchy)
|
||||||
|
|
||||||
|
self.trashLabel = QLabel(self.tr("Move split document to Trash"))
|
||||||
|
self.trashSwitch = QSwitch(width=2*iPx, height=iPx)
|
||||||
|
|
||||||
|
self.optBox = QGridLayout()
|
||||||
|
self.optBox.addWidget(self.folderLabel, 0, 0)
|
||||||
|
self.optBox.addWidget(self.folderSwitch, 0, 1)
|
||||||
|
self.optBox.addWidget(self.hierarchyLabel, 1, 0)
|
||||||
|
self.optBox.addWidget(self.hierarchySwitch, 1, 1)
|
||||||
|
self.optBox.addWidget(self.trashLabel, 2, 0)
|
||||||
|
self.optBox.addWidget(self.trashSwitch, 2, 1)
|
||||||
|
self.optBox.setVerticalSpacing(vSp)
|
||||||
|
self.optBox.setHorizontalSpacing(hSp)
|
||||||
|
self.optBox.setColumnStretch(3, 1)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||||
self.buttonBox.accepted.connect(self._doSplit)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self._doClose)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
|
|
||||||
|
# Assemble
|
||||||
|
self.outerBox = QVBoxLayout()
|
||||||
self.outerBox.setSpacing(0)
|
self.outerBox.setSpacing(0)
|
||||||
self.outerBox.addWidget(self.headLabel)
|
self.outerBox.addWidget(self.headLabel)
|
||||||
self.outerBox.addWidget(self.helpLabel)
|
self.outerBox.addWidget(self.helpLabel)
|
||||||
self.outerBox.addSpacing(self.mainConf.pxInt(8))
|
self.outerBox.addSpacing(vSp)
|
||||||
self.outerBox.addWidget(self.listBox)
|
self.outerBox.addWidget(self.listBox)
|
||||||
self.outerBox.addWidget(self.splitLevel)
|
self.outerBox.addWidget(self.splitLevel)
|
||||||
self.outerBox.addSpacing(self.mainConf.pxInt(12))
|
self.outerBox.addSpacing(vSp)
|
||||||
|
self.outerBox.addLayout(self.optBox)
|
||||||
|
self.outerBox.addSpacing(bSp)
|
||||||
self.outerBox.addWidget(self.buttonBox)
|
self.outerBox.addWidget(self.buttonBox)
|
||||||
self.setLayout(self.outerBox)
|
self.setLayout(self.outerBox)
|
||||||
|
|
||||||
self.rejected.connect(self._doClose)
|
# Load Content
|
||||||
|
self._loadContent(sHandle)
|
||||||
self._populateList()
|
|
||||||
|
|
||||||
logger.debug("GuiDocSplit initialisation complete")
|
logger.debug("GuiDocSplit initialisation complete")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
def getData(self):
|
||||||
# Buttons
|
"""Return the user's choices. Also save the users options for
|
||||||
##
|
the next time the dialog is used.
|
||||||
|
|
||||||
def _doSplit(self):
|
|
||||||
"""Perform the split of the file, create a new folder in the
|
|
||||||
same parent folder, and multiple files depending on split level
|
|
||||||
settings. The old file is not removed in the split process, and
|
|
||||||
must be deleted manually.
|
|
||||||
"""
|
"""
|
||||||
logger.verbose("GuiDocSplit split button clicked")
|
headerList = []
|
||||||
|
|
||||||
if self.sourceItem is None:
|
|
||||||
self.mainGui.makeAlert(self.tr(
|
|
||||||
"No source document selected. Nothing to do."
|
|
||||||
), nwAlert.ERROR)
|
|
||||||
return False
|
|
||||||
|
|
||||||
srcItem = self.theProject.tree[self.sourceItem]
|
|
||||||
if srcItem is None:
|
|
||||||
self.mainGui.makeAlert(self.tr(
|
|
||||||
"Could not parse source document."
|
|
||||||
), nwAlert.ERROR)
|
|
||||||
return False
|
|
||||||
|
|
||||||
inDoc = NWDoc(self.theProject, self.sourceItem)
|
|
||||||
theText = inDoc.readDocument()
|
|
||||||
|
|
||||||
docErr = inDoc.getError()
|
|
||||||
if theText is None and docErr:
|
|
||||||
self.mainGui.makeAlert([
|
|
||||||
self.tr("Failed to open document file."), docErr
|
|
||||||
], nwAlert.ERROR)
|
|
||||||
|
|
||||||
if theText is None:
|
|
||||||
theText = ""
|
|
||||||
|
|
||||||
nLines = len(self.sourceText)
|
|
||||||
logger.debug("Splitting document %s with %d lines", self.sourceItem, nLines)
|
|
||||||
|
|
||||||
finalOrder = []
|
|
||||||
for i in range(self.listBox.count()):
|
for i in range(self.listBox.count()):
|
||||||
listItem = self.listBox.item(i)
|
item = self.listBox.item(i)
|
||||||
wTitle = listItem.text()
|
if item is not None:
|
||||||
lineNo = listItem.data(Qt.UserRole)
|
headerList.append((
|
||||||
finalOrder.append([wTitle, lineNo, nLines])
|
item.data(self.LINE_ROLE),
|
||||||
if i > 0:
|
item.data(self.LEVEL_ROLE),
|
||||||
finalOrder[i-1][2] = lineNo
|
item.data(self.LABEL_ROLE),
|
||||||
|
))
|
||||||
|
|
||||||
nFiles = len(finalOrder)
|
spLevel = self.splitLevel.currentData()
|
||||||
if nFiles == 0:
|
intoFolder = self.folderSwitch.isChecked()
|
||||||
self.mainGui.makeAlert(self.tr(
|
docHierarchy = self.hierarchySwitch.isChecked()
|
||||||
"No headers found. Nothing to do."
|
moveToTrash = self.trashSwitch.isChecked()
|
||||||
), nwAlert.ERROR)
|
|
||||||
return False
|
|
||||||
|
|
||||||
msgYes = self.mainGui.askQuestion(
|
self._data["spLevel"] = spLevel
|
||||||
self.tr("Split Document"),
|
self._data["headerList"] = headerList
|
||||||
"{0}<br><br>{1}".format(
|
self._data["intoFolder"] = intoFolder
|
||||||
self.tr(
|
self._data["docHierarchy"] = docHierarchy
|
||||||
"The document will be split into {0} file(s) in a new folder. "
|
self._data["moveToTrash"] = moveToTrash
|
||||||
"The original document will remain intact."
|
|
||||||
).format(nFiles),
|
|
||||||
self.tr(
|
|
||||||
"Continue with the splitting process?"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if not msgYes:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Create the folder
|
pOptions = self.theProject.options
|
||||||
fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent)
|
pOptions.setValue("GuiDocSplit", "spLevel", spLevel)
|
||||||
self.mainGui.projView.revealNewTreeItem(fHandle)
|
pOptions.setValue("GuiDocSplit", "intoFolder", intoFolder)
|
||||||
logger.verbose("Creating folder '%s'", fHandle)
|
pOptions.setValue("GuiDocSplit", "docHierarchy", docHierarchy)
|
||||||
|
|
||||||
# Loop through, and create the files
|
return self._data, self._text
|
||||||
for wTitle, iStart, iEnd in finalOrder:
|
|
||||||
|
|
||||||
wTitle = wTitle.lstrip("#").strip()
|
##
|
||||||
nHandle = self.theProject.newFile(wTitle, fHandle)
|
# Slots
|
||||||
newItem = self.theProject.tree[nHandle]
|
##
|
||||||
newItem.setStatus(srcItem.itemStatus)
|
|
||||||
newItem.setImport(srcItem.itemImport)
|
|
||||||
logger.verbose(
|
|
||||||
"Creating new document '%s' with text from line %d to %d",
|
|
||||||
nHandle, iStart+1, iEnd
|
|
||||||
)
|
|
||||||
|
|
||||||
theText = "\n".join(self.sourceText[iStart:iEnd])
|
def _reloadList(self):
|
||||||
theText = theText.rstrip("\n") + "\n\n"
|
"""Reload the content of the list box.
|
||||||
|
|
||||||
outDoc = NWDoc(self.theProject, nHandle)
|
|
||||||
if not outDoc.writeDocument(theText):
|
|
||||||
self.mainGui.makeAlert([
|
|
||||||
self.tr("Could not save document."), outDoc.getError()
|
|
||||||
], nwAlert.ERROR)
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.mainGui.projView.revealNewTreeItem(nHandle)
|
|
||||||
|
|
||||||
self._doClose()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _doClose(self):
|
|
||||||
"""Close the dialog window without doing anything.
|
|
||||||
"""
|
"""
|
||||||
self.theProject.options.saveSettings()
|
sHandle = self._data.get("sHandle", None)
|
||||||
self.close()
|
self._loadContent(sHandle)
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
def _populateList(self):
|
def _loadContent(self, sHandle):
|
||||||
"""Get the item selected in the tree, check that it is a folder,
|
"""Load content from a given source item.
|
||||||
and try to find all files associated with it. The valid files
|
|
||||||
are then added to the list view in order. The list itself can be
|
|
||||||
reordered by the user.
|
|
||||||
"""
|
"""
|
||||||
|
self._data = {}
|
||||||
|
self._data["sHandle"] = sHandle
|
||||||
|
|
||||||
self.listBox.clear()
|
self.listBox.clear()
|
||||||
if self.sourceItem is None:
|
|
||||||
self.sourceItem = self.mainGui.projView.getSelectedHandle()
|
|
||||||
|
|
||||||
if self.sourceItem is None:
|
nwItem = self.theProject.tree[sHandle]
|
||||||
return False
|
if nwItem is None or not nwItem.isFileType():
|
||||||
|
return
|
||||||
nwItem = self.theProject.tree[self.sourceItem]
|
|
||||||
if nwItem is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if nwItem.itemType is not nwItemType.FILE:
|
|
||||||
self.mainGui.makeAlert(self.tr(
|
|
||||||
"Element selected in the project tree must be a file."
|
|
||||||
), nwAlert.ERROR)
|
|
||||||
return False
|
|
||||||
|
|
||||||
inDoc = NWDoc(self.theProject, self.sourceItem)
|
|
||||||
theText = inDoc.readDocument()
|
|
||||||
if theText is None:
|
|
||||||
theText = ""
|
|
||||||
return False
|
|
||||||
|
|
||||||
spLevel = self.splitLevel.currentData()
|
spLevel = self.splitLevel.currentData()
|
||||||
self.theProject.options.setValue("GuiDocSplit", "spLevel", spLevel)
|
if not self._text:
|
||||||
logger.debug(
|
inDoc = self.theProject.storage.getDocument(sHandle)
|
||||||
"Scanning document '%s' for headings level <= %d",
|
self._text = (inDoc.readDocument() or "").splitlines()
|
||||||
self.sourceItem, spLevel
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sourceText = theText.splitlines()
|
for lineNo, aLine in enumerate(self._text):
|
||||||
for lineNo, aLine in enumerate(self.sourceText):
|
|
||||||
|
|
||||||
onLine = -1
|
onLine = -1
|
||||||
|
hLevel = 0
|
||||||
|
hLabel = aLine.strip()
|
||||||
if aLine.startswith("# ") and spLevel >= 1:
|
if aLine.startswith("# ") and spLevel >= 1:
|
||||||
onLine = lineNo
|
onLine = lineNo
|
||||||
|
hLevel = 1
|
||||||
|
hLabel = aLine[2:].strip()
|
||||||
elif aLine.startswith("## ") and spLevel >= 2:
|
elif aLine.startswith("## ") and spLevel >= 2:
|
||||||
onLine = lineNo
|
onLine = lineNo
|
||||||
|
hLevel = 2
|
||||||
|
hLabel = aLine[3:].strip()
|
||||||
elif aLine.startswith("### ") and spLevel >= 3:
|
elif aLine.startswith("### ") and spLevel >= 3:
|
||||||
onLine = lineNo
|
onLine = lineNo
|
||||||
|
hLevel = 3
|
||||||
|
hLabel = aLine[4:].strip()
|
||||||
elif aLine.startswith("#### ") and spLevel >= 4:
|
elif aLine.startswith("#### ") and spLevel >= 4:
|
||||||
onLine = lineNo
|
onLine = lineNo
|
||||||
|
hLevel = 4
|
||||||
|
hLabel = aLine[5:].strip()
|
||||||
|
elif aLine.startswith("#! ") and spLevel >= 1:
|
||||||
|
onLine = lineNo
|
||||||
|
hLevel = 1
|
||||||
|
hLabel = aLine[3:].strip()
|
||||||
|
elif aLine.startswith("##! ") and spLevel >= 2:
|
||||||
|
onLine = lineNo
|
||||||
|
hLevel = 2
|
||||||
|
hLabel = aLine[4:].strip()
|
||||||
|
|
||||||
if onLine >= 0:
|
if onLine >= 0 and hLevel > 0:
|
||||||
newItem = QListWidgetItem()
|
newItem = QListWidgetItem()
|
||||||
newItem.setText(aLine.strip())
|
newItem.setText(aLine.strip())
|
||||||
newItem.setData(Qt.UserRole, onLine)
|
newItem.setData(self.LINE_ROLE, onLine)
|
||||||
|
newItem.setData(self.LEVEL_ROLE, hLevel)
|
||||||
|
newItem.setData(self.LABEL_ROLE, hLabel)
|
||||||
self.listBox.addItem(newItem)
|
self.listBox.addItem(newItem)
|
||||||
|
|
||||||
return True
|
return
|
||||||
|
|
||||||
# END Class GuiDocSplit
|
# END Class GuiDocSplit
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class GuiEditLabel(QDialog):
|
class GuiEditLabel(QDialog):
|
||||||
|
|
||||||
def __init__(self, parent, text=""):
|
def __init__(self, parent, text=""):
|
||||||
QDialog.__init__(self, parent=parent)
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
self.setObjectName("GuiEditLabel")
|
self.setObjectName("GuiEditLabel")
|
||||||
self.setWindowTitle(self.tr("Item Label"))
|
self.setWindowTitle(self.tr("Item Label"))
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
import novelwriter
|
import novelwriter
|
||||||
|
|
||||||
@@ -34,8 +33,7 @@ from PyQt5.QtWidgets import (
|
|||||||
QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox
|
QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox
|
||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter.enum import nwAlert
|
from novelwriter.custom import QSwitch, QConfigLayout, PagedDialog
|
||||||
from novelwriter.gui.custom import QSwitch, QConfigLayout, PagedDialog
|
|
||||||
from novelwriter.dialogs.quotes import GuiQuoteSelect
|
from novelwriter.dialogs.quotes import GuiQuoteSelect
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -44,7 +42,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class GuiPreferences(PagedDialog):
|
class GuiPreferences(PagedDialog):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, mainGui):
|
||||||
PagedDialog.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
logger.debug("Initialising GuiPreferences ...")
|
logger.debug("Initialising GuiPreferences ...")
|
||||||
self.setObjectName("GuiPreferences")
|
self.setObjectName("GuiPreferences")
|
||||||
@@ -55,13 +53,13 @@ class GuiPreferences(PagedDialog):
|
|||||||
|
|
||||||
self.setWindowTitle(self.tr("Preferences"))
|
self.setWindowTitle(self.tr("Preferences"))
|
||||||
|
|
||||||
self.tabGeneral = GuiPreferencesGeneral(self.mainGui)
|
self.tabGeneral = GuiPreferencesGeneral(self)
|
||||||
self.tabProjects = GuiPreferencesProjects(self.mainGui)
|
self.tabProjects = GuiPreferencesProjects(self)
|
||||||
self.tabDocs = GuiPreferencesDocuments(self.mainGui)
|
self.tabDocs = GuiPreferencesDocuments(self)
|
||||||
self.tabEditor = GuiPreferencesEditor(self.mainGui)
|
self.tabEditor = GuiPreferencesEditor(self)
|
||||||
self.tabSyntax = GuiPreferencesSyntax(self.mainGui)
|
self.tabSyntax = GuiPreferencesSyntax(self)
|
||||||
self.tabAuto = GuiPreferencesAutomation(self.mainGui)
|
self.tabAuto = GuiPreferencesAutomation(self)
|
||||||
self.tabQuote = GuiPreferencesQuotes(self.mainGui)
|
self.tabQuote = GuiPreferencesQuotes(self)
|
||||||
|
|
||||||
self.addTab(self.tabGeneral, self.tr("General"))
|
self.addTab(self.tabGeneral, self.tr("General"))
|
||||||
self.addTab(self.tabProjects, self.tr("Projects"))
|
self.addTab(self.tabProjects, self.tr("Projects"))
|
||||||
@@ -76,12 +74,38 @@ class GuiPreferences(PagedDialog):
|
|||||||
self.buttonBox.rejected.connect(self._doClose)
|
self.buttonBox.rejected.connect(self._doClose)
|
||||||
self.addControls(self.buttonBox)
|
self.addControls(self.buttonBox)
|
||||||
|
|
||||||
self.resize(*self.mainConf.getPreferencesSize())
|
self.resize(*self.mainConf.preferencesWinSize)
|
||||||
|
|
||||||
|
# Settings
|
||||||
|
self._updateTheme = False
|
||||||
|
self._updateSyntax = False
|
||||||
|
self._needsRestart = False
|
||||||
|
self._refreshTree = False
|
||||||
|
|
||||||
logger.debug("GuiPreferences initialisation complete")
|
logger.debug("GuiPreferences initialisation complete")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Properties
|
||||||
|
##
|
||||||
|
|
||||||
|
@property
|
||||||
|
def updateTheme(self):
|
||||||
|
return self._updateTheme
|
||||||
|
|
||||||
|
@property
|
||||||
|
def updateSyntax(self):
|
||||||
|
return self._updateSyntax
|
||||||
|
|
||||||
|
@property
|
||||||
|
def needsRestart(self):
|
||||||
|
return self._needsRestart
|
||||||
|
|
||||||
|
@property
|
||||||
|
def refreshTree(self):
|
||||||
|
return self._refreshTree
|
||||||
|
|
||||||
##
|
##
|
||||||
# Slots
|
# Slots
|
||||||
##
|
##
|
||||||
@@ -92,8 +116,7 @@ class GuiPreferences(PagedDialog):
|
|||||||
"""
|
"""
|
||||||
logger.debug("Saving new preferences")
|
logger.debug("Saving new preferences")
|
||||||
|
|
||||||
needsRestart, refreshTree = self.tabGeneral.saveValues()
|
self.tabGeneral.saveValues()
|
||||||
|
|
||||||
self.tabProjects.saveValues()
|
self.tabProjects.saveValues()
|
||||||
self.tabDocs.saveValues()
|
self.tabDocs.saveValues()
|
||||||
self.tabEditor.saveValues()
|
self.tabEditor.saveValues()
|
||||||
@@ -101,15 +124,8 @@ class GuiPreferences(PagedDialog):
|
|||||||
self.tabAuto.saveValues()
|
self.tabAuto.saveValues()
|
||||||
self.tabQuote.saveValues()
|
self.tabQuote.saveValues()
|
||||||
|
|
||||||
if needsRestart:
|
|
||||||
self.mainGui.makeAlert(self.tr(
|
|
||||||
"Some changes will not be applied until novelWriter has been restarted."
|
|
||||||
), nwAlert.INFO)
|
|
||||||
|
|
||||||
if refreshTree:
|
|
||||||
self.mainGui.projView.populateTree()
|
|
||||||
|
|
||||||
self._saveWindowSize()
|
self._saveWindowSize()
|
||||||
|
self.mainConf.saveConfig()
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -128,9 +144,7 @@ class GuiPreferences(PagedDialog):
|
|||||||
def _saveWindowSize(self):
|
def _saveWindowSize(self):
|
||||||
"""Save the dialog window size.
|
"""Save the dialog window size.
|
||||||
"""
|
"""
|
||||||
winWidth = self.mainConf.rpxInt(self.width())
|
self.mainConf.setPreferencesWinSize(self.width(), self.height())
|
||||||
winHeight = self.mainConf.rpxInt(self.height())
|
|
||||||
self.mainConf.setPreferencesSize(winWidth, winHeight)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# END Class GuiPreferences
|
# END Class GuiPreferences
|
||||||
@@ -138,12 +152,13 @@ class GuiPreferences(PagedDialog):
|
|||||||
|
|
||||||
class GuiPreferencesGeneral(QWidget):
|
class GuiPreferencesGeneral(QWidget):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, prefsGui):
|
||||||
QWidget.__init__(self, mainGui)
|
super().__init__(parent=prefsGui)
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.prefsGui = prefsGui
|
||||||
self.mainTheme = mainGui.mainTheme
|
self.mainGui = prefsGui.mainGui
|
||||||
|
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||||
|
|
||||||
# The Form
|
# The Form
|
||||||
self.mainForm = QConfigLayout()
|
self.mainForm = QConfigLayout()
|
||||||
@@ -156,19 +171,19 @@ class GuiPreferencesGeneral(QWidget):
|
|||||||
minWidth = self.mainConf.pxInt(200)
|
minWidth = self.mainConf.pxInt(200)
|
||||||
|
|
||||||
# Select Locale
|
# Select Locale
|
||||||
self.guiLang = QComboBox()
|
self.guiLocale = QComboBox()
|
||||||
self.guiLang.setMinimumWidth(minWidth)
|
self.guiLocale.setMinimumWidth(minWidth)
|
||||||
theLangs = self.mainConf.listLanguages(self.mainConf.LANG_NW)
|
theLangs = self.mainConf.listLanguages(self.mainConf.LANG_NW)
|
||||||
for lang, langName in theLangs:
|
for lang, langName in theLangs:
|
||||||
self.guiLang.addItem(langName, lang)
|
self.guiLocale.addItem(langName, lang)
|
||||||
langIdx = self.guiLang.findData(self.mainConf.guiLang)
|
langIdx = self.guiLocale.findData(self.mainConf.guiLocale)
|
||||||
if langIdx != -1:
|
if langIdx != -1:
|
||||||
self.guiLang.setCurrentIndex(langIdx)
|
self.guiLocale.setCurrentIndex(langIdx)
|
||||||
|
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
self.tr("Main GUI language"),
|
self.tr("Main GUI language"),
|
||||||
self.guiLang,
|
self.guiLocale,
|
||||||
self.tr("Requires restart.")
|
self.tr("Requires restart to take effect.")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Select Theme
|
# Select Theme
|
||||||
@@ -184,23 +199,7 @@ class GuiPreferencesGeneral(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
self.tr("Main GUI theme"),
|
self.tr("Main GUI theme"),
|
||||||
self.guiTheme,
|
self.guiTheme,
|
||||||
self.tr("Requires restart.")
|
self.tr("General colour theme and icons.")
|
||||||
)
|
|
||||||
|
|
||||||
# Select Icon Theme
|
|
||||||
self.guiIcons = QComboBox()
|
|
||||||
self.guiIcons.setMinimumWidth(minWidth)
|
|
||||||
self.iconCache = self.mainTheme.iconCache.listThemes()
|
|
||||||
for iconDir, iconName in self.iconCache:
|
|
||||||
self.guiIcons.addItem(iconName, iconDir)
|
|
||||||
iconIdx = self.guiIcons.findData(self.mainConf.guiIcons)
|
|
||||||
if iconIdx != -1:
|
|
||||||
self.guiIcons.setCurrentIndex(iconIdx)
|
|
||||||
|
|
||||||
self.mainForm.addRow(
|
|
||||||
self.tr("Main icon theme"),
|
|
||||||
self.guiIcons,
|
|
||||||
self.tr("Requires restart.")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Editor Theme
|
# Editor Theme
|
||||||
@@ -230,7 +229,7 @@ class GuiPreferencesGeneral(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
self.tr("Font family"),
|
self.tr("Font family"),
|
||||||
self.guiFont,
|
self.guiFont,
|
||||||
self.tr("Requires restart."),
|
self.tr("Requires restart to take effect."),
|
||||||
theButton=self.fontButton
|
theButton=self.fontButton
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -243,7 +242,7 @@ class GuiPreferencesGeneral(QWidget):
|
|||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
self.tr("Font size"),
|
self.tr("Font size"),
|
||||||
self.guiFontSize,
|
self.guiFontSize,
|
||||||
self.tr("Requires restart."),
|
self.tr("Requires restart to take effect."),
|
||||||
theUnit=self.tr("pt")
|
theUnit=self.tr("pt")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -288,29 +287,23 @@ class GuiPreferencesGeneral(QWidget):
|
|||||||
def saveValues(self):
|
def saveValues(self):
|
||||||
"""Save the values set for this tab.
|
"""Save the values set for this tab.
|
||||||
"""
|
"""
|
||||||
guiLang = self.guiLang.currentData()
|
guiLocale = self.guiLocale.currentData()
|
||||||
guiTheme = self.guiTheme.currentData()
|
guiTheme = self.guiTheme.currentData()
|
||||||
guiIcons = self.guiIcons.currentData()
|
|
||||||
guiSyntax = self.guiSyntax.currentData()
|
guiSyntax = self.guiSyntax.currentData()
|
||||||
guiFont = self.guiFont.text()
|
guiFont = self.guiFont.text()
|
||||||
guiFontSize = self.guiFontSize.value()
|
guiFontSize = self.guiFontSize.value()
|
||||||
emphLabels = self.emphLabels.isChecked()
|
emphLabels = self.emphLabels.isChecked()
|
||||||
|
|
||||||
# Check if restart is needed
|
# Update Flags
|
||||||
needsRestart = False
|
self.prefsGui._updateTheme |= self.mainConf.guiTheme != guiTheme
|
||||||
needsRestart |= self.mainConf.guiLang != guiLang
|
self.prefsGui._updateSyntax |= self.mainConf.guiSyntax != guiSyntax
|
||||||
needsRestart |= self.mainConf.guiTheme != guiTheme
|
self.prefsGui._needsRestart |= self.mainConf.guiLocale != guiLocale
|
||||||
needsRestart |= self.mainConf.guiIcons != guiIcons
|
self.prefsGui._needsRestart |= self.mainConf.guiFont != guiFont
|
||||||
needsRestart |= self.mainConf.guiFont != guiFont
|
self.prefsGui._needsRestart |= self.mainConf.guiFontSize != guiFontSize
|
||||||
needsRestart |= self.mainConf.guiFontSize != guiFontSize
|
self.prefsGui._refreshTree |= self.mainConf.emphLabels != emphLabels
|
||||||
|
|
||||||
# Check if refreshing project tree is needed
|
self.mainConf.guiLocale = guiLocale
|
||||||
refreshTree = False
|
|
||||||
refreshTree |= self.mainConf.emphLabels != emphLabels
|
|
||||||
|
|
||||||
self.mainConf.guiLang = guiLang
|
|
||||||
self.mainConf.guiTheme = guiTheme
|
self.mainConf.guiTheme = guiTheme
|
||||||
self.mainConf.guiIcons = guiIcons
|
|
||||||
self.mainConf.guiSyntax = guiSyntax
|
self.mainConf.guiSyntax = guiSyntax
|
||||||
self.mainConf.guiFont = guiFont
|
self.mainConf.guiFont = guiFont
|
||||||
self.mainConf.guiFontSize = guiFontSize
|
self.mainConf.guiFontSize = guiFontSize
|
||||||
@@ -319,9 +312,7 @@ class GuiPreferencesGeneral(QWidget):
|
|||||||
self.mainConf.hideVScroll = self.hideVScroll.isChecked()
|
self.mainConf.hideVScroll = self.hideVScroll.isChecked()
|
||||||
self.mainConf.hideHScroll = self.hideHScroll.isChecked()
|
self.mainConf.hideHScroll = self.hideHScroll.isChecked()
|
||||||
|
|
||||||
self.mainConf.confChanged = True
|
return
|
||||||
|
|
||||||
return needsRestart, refreshTree
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Slots
|
# Slots
|
||||||
@@ -344,12 +335,12 @@ class GuiPreferencesGeneral(QWidget):
|
|||||||
|
|
||||||
class GuiPreferencesProjects(QWidget):
|
class GuiPreferencesProjects(QWidget):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, prefsGui):
|
||||||
QWidget.__init__(self, mainGui)
|
super().__init__(parent=prefsGui)
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.mainGui = prefsGui.mainGui
|
||||||
self.mainTheme = mainGui.mainTheme
|
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||||
|
|
||||||
# The Form
|
# The Form
|
||||||
self.mainForm = QConfigLayout()
|
self.mainForm = QConfigLayout()
|
||||||
@@ -391,7 +382,7 @@ class GuiPreferencesProjects(QWidget):
|
|||||||
self.mainForm.addGroupLabel(self.tr("Project Backup"))
|
self.mainForm.addGroupLabel(self.tr("Project Backup"))
|
||||||
|
|
||||||
# Backup Path
|
# Backup Path
|
||||||
self.backupPath = self.mainConf.backupPath
|
self.backupPath = self.mainConf.backupPath()
|
||||||
self.backupGetPath = QPushButton(self.tr("Browse"))
|
self.backupGetPath = QPushButton(self.tr("Browse"))
|
||||||
self.backupGetPath.clicked.connect(self._backupFolder)
|
self.backupGetPath.clicked.connect(self._backupFolder)
|
||||||
self.backupPathRow = self.mainForm.addRow(
|
self.backupPathRow = self.mainForm.addRow(
|
||||||
@@ -458,7 +449,7 @@ class GuiPreferencesProjects(QWidget):
|
|||||||
self.mainConf.autoSaveProj = self.autoSaveProj.value()
|
self.mainConf.autoSaveProj = self.autoSaveProj.value()
|
||||||
|
|
||||||
# Project Backup
|
# Project Backup
|
||||||
self.mainConf.backupPath = self.backupPath
|
self.mainConf.setBackupPath(self.backupPath)
|
||||||
self.mainConf.backupOnClose = self.backupOnClose.isChecked()
|
self.mainConf.backupOnClose = self.backupOnClose.isChecked()
|
||||||
self.mainConf.askBeforeBackup = self.askBeforeBackup.isChecked()
|
self.mainConf.askBeforeBackup = self.askBeforeBackup.isChecked()
|
||||||
|
|
||||||
@@ -466,8 +457,6 @@ class GuiPreferencesProjects(QWidget):
|
|||||||
self.mainConf.stopWhenIdle = self.stopWhenIdle.isChecked()
|
self.mainConf.stopWhenIdle = self.stopWhenIdle.isChecked()
|
||||||
self.mainConf.userIdleTime = round(self.userIdleTime.value() * 60)
|
self.mainConf.userIdleTime = round(self.userIdleTime.value() * 60)
|
||||||
|
|
||||||
self.mainConf.confChanged = True
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -477,12 +466,9 @@ class GuiPreferencesProjects(QWidget):
|
|||||||
def _backupFolder(self):
|
def _backupFolder(self):
|
||||||
"""Open a dialog to select the backup folder.
|
"""Open a dialog to select the backup folder.
|
||||||
"""
|
"""
|
||||||
currDir = self.backupPath
|
currDir = self.backupPath or ""
|
||||||
if not os.path.isdir(currDir):
|
|
||||||
currDir = ""
|
|
||||||
|
|
||||||
newDir = QFileDialog.getExistingDirectory(
|
newDir = QFileDialog.getExistingDirectory(
|
||||||
self, self.tr("Backup Directory"), currDir, options=QFileDialog.ShowDirsOnly
|
self, self.tr("Backup Directory"), str(currDir), options=QFileDialog.ShowDirsOnly
|
||||||
)
|
)
|
||||||
if newDir:
|
if newDir:
|
||||||
self.backupPath = newDir
|
self.backupPath = newDir
|
||||||
@@ -505,12 +491,12 @@ class GuiPreferencesProjects(QWidget):
|
|||||||
|
|
||||||
class GuiPreferencesDocuments(QWidget):
|
class GuiPreferencesDocuments(QWidget):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, prefsGui):
|
||||||
QWidget.__init__(self, mainGui)
|
super().__init__(parent=prefsGui)
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.mainGui = prefsGui.mainGui
|
||||||
self.mainTheme = mainGui.mainTheme
|
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||||
|
|
||||||
# The Form
|
# The Form
|
||||||
self.mainForm = QConfigLayout()
|
self.mainForm = QConfigLayout()
|
||||||
@@ -640,8 +626,6 @@ class GuiPreferencesDocuments(QWidget):
|
|||||||
self.mainConf.textMargin = self.textMargin.value()
|
self.mainConf.textMargin = self.textMargin.value()
|
||||||
self.mainConf.tabWidth = self.tabWidth.value()
|
self.mainConf.tabWidth = self.tabWidth.value()
|
||||||
|
|
||||||
self.mainConf.confChanged = True
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -666,12 +650,12 @@ class GuiPreferencesDocuments(QWidget):
|
|||||||
|
|
||||||
class GuiPreferencesEditor(QWidget):
|
class GuiPreferencesEditor(QWidget):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, prefsGui):
|
||||||
QWidget.__init__(self, mainGui)
|
super().__init__(parent=prefsGui)
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.mainGui = prefsGui.mainGui
|
||||||
self.mainTheme = mainGui.mainTheme
|
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||||
|
|
||||||
# The Form
|
# The Form
|
||||||
self.mainForm = QConfigLayout()
|
self.mainForm = QConfigLayout()
|
||||||
@@ -831,8 +815,6 @@ class GuiPreferencesEditor(QWidget):
|
|||||||
self.mainConf.autoScroll = self.autoScroll.isChecked()
|
self.mainConf.autoScroll = self.autoScroll.isChecked()
|
||||||
self.mainConf.autoScrollPos = self.autoScrollPos.value()
|
self.mainConf.autoScrollPos = self.autoScrollPos.value()
|
||||||
|
|
||||||
self.mainConf.confChanged = True
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# END Class GuiPreferencesEditor
|
# END Class GuiPreferencesEditor
|
||||||
@@ -840,12 +822,12 @@ class GuiPreferencesEditor(QWidget):
|
|||||||
|
|
||||||
class GuiPreferencesSyntax(QWidget):
|
class GuiPreferencesSyntax(QWidget):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, prefsGui):
|
||||||
QWidget.__init__(self, mainGui)
|
super().__init__(parent=prefsGui)
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.mainGui = prefsGui.mainGui
|
||||||
self.mainTheme = mainGui.mainTheme
|
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||||
|
|
||||||
# The Form
|
# The Form
|
||||||
self.mainForm = QConfigLayout()
|
self.mainForm = QConfigLayout()
|
||||||
@@ -922,8 +904,6 @@ class GuiPreferencesSyntax(QWidget):
|
|||||||
# Text Errors
|
# Text Errors
|
||||||
self.mainConf.showMultiSpaces = self.showMultiSpaces.isChecked()
|
self.mainConf.showMultiSpaces = self.showMultiSpaces.isChecked()
|
||||||
|
|
||||||
self.mainConf.confChanged = True
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -943,12 +923,12 @@ class GuiPreferencesSyntax(QWidget):
|
|||||||
|
|
||||||
class GuiPreferencesAutomation(QWidget):
|
class GuiPreferencesAutomation(QWidget):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, prefsGui):
|
||||||
QWidget.__init__(self, mainGui)
|
super().__init__(parent=prefsGui)
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.mainGui = prefsGui.mainGui
|
||||||
self.mainTheme = mainGui.mainTheme
|
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||||
|
|
||||||
# The Form
|
# The Form
|
||||||
self.mainForm = QConfigLayout()
|
self.mainForm = QConfigLayout()
|
||||||
@@ -1076,8 +1056,6 @@ class GuiPreferencesAutomation(QWidget):
|
|||||||
self.mainConf.fmtPadAfter = self.fmtPadAfter.text().strip()
|
self.mainConf.fmtPadAfter = self.fmtPadAfter.text().strip()
|
||||||
self.mainConf.fmtPadThin = self.fmtPadThin.isChecked()
|
self.mainConf.fmtPadThin = self.fmtPadThin.isChecked()
|
||||||
|
|
||||||
self.mainConf.confChanged = True
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -1100,12 +1078,12 @@ class GuiPreferencesAutomation(QWidget):
|
|||||||
|
|
||||||
class GuiPreferencesQuotes(QWidget):
|
class GuiPreferencesQuotes(QWidget):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, prefsGui):
|
||||||
QWidget.__init__(self, mainGui)
|
super().__init__(parent=prefsGui)
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.mainGui = prefsGui.mainGui
|
||||||
self.mainTheme = mainGui.mainTheme
|
self.mainTheme = prefsGui.mainGui.mainTheme
|
||||||
|
|
||||||
# The Form
|
# The Form
|
||||||
self.mainForm = QConfigLayout()
|
self.mainForm = QConfigLayout()
|
||||||
@@ -1126,7 +1104,7 @@ class GuiPreferencesQuotes(QWidget):
|
|||||||
self.quoteSym["SO"].setReadOnly(True)
|
self.quoteSym["SO"].setReadOnly(True)
|
||||||
self.quoteSym["SO"].setFixedWidth(qWidth)
|
self.quoteSym["SO"].setFixedWidth(qWidth)
|
||||||
self.quoteSym["SO"].setAlignment(Qt.AlignCenter)
|
self.quoteSym["SO"].setAlignment(Qt.AlignCenter)
|
||||||
self.quoteSym["SO"].setText(self.mainConf.fmtSingleQuotes[0])
|
self.quoteSym["SO"].setText(self.mainConf.fmtSQuoteOpen)
|
||||||
self.btnSingleStyleO = QPushButton("...")
|
self.btnSingleStyleO = QPushButton("...")
|
||||||
self.btnSingleStyleO.setMaximumWidth(bWidth)
|
self.btnSingleStyleO.setMaximumWidth(bWidth)
|
||||||
self.btnSingleStyleO.clicked.connect(lambda: self._getQuote("SO"))
|
self.btnSingleStyleO.clicked.connect(lambda: self._getQuote("SO"))
|
||||||
@@ -1142,7 +1120,7 @@ class GuiPreferencesQuotes(QWidget):
|
|||||||
self.quoteSym["SC"].setReadOnly(True)
|
self.quoteSym["SC"].setReadOnly(True)
|
||||||
self.quoteSym["SC"].setFixedWidth(qWidth)
|
self.quoteSym["SC"].setFixedWidth(qWidth)
|
||||||
self.quoteSym["SC"].setAlignment(Qt.AlignCenter)
|
self.quoteSym["SC"].setAlignment(Qt.AlignCenter)
|
||||||
self.quoteSym["SC"].setText(self.mainConf.fmtSingleQuotes[1])
|
self.quoteSym["SC"].setText(self.mainConf.fmtSQuoteClose)
|
||||||
self.btnSingleStyleC = QPushButton("...")
|
self.btnSingleStyleC = QPushButton("...")
|
||||||
self.btnSingleStyleC.setMaximumWidth(bWidth)
|
self.btnSingleStyleC.setMaximumWidth(bWidth)
|
||||||
self.btnSingleStyleC.clicked.connect(lambda: self._getQuote("SC"))
|
self.btnSingleStyleC.clicked.connect(lambda: self._getQuote("SC"))
|
||||||
@@ -1159,7 +1137,7 @@ class GuiPreferencesQuotes(QWidget):
|
|||||||
self.quoteSym["DO"].setReadOnly(True)
|
self.quoteSym["DO"].setReadOnly(True)
|
||||||
self.quoteSym["DO"].setFixedWidth(qWidth)
|
self.quoteSym["DO"].setFixedWidth(qWidth)
|
||||||
self.quoteSym["DO"].setAlignment(Qt.AlignCenter)
|
self.quoteSym["DO"].setAlignment(Qt.AlignCenter)
|
||||||
self.quoteSym["DO"].setText(self.mainConf.fmtDoubleQuotes[0])
|
self.quoteSym["DO"].setText(self.mainConf.fmtDQuoteOpen)
|
||||||
self.btnDoubleStyleO = QPushButton("...")
|
self.btnDoubleStyleO = QPushButton("...")
|
||||||
self.btnDoubleStyleO.setMaximumWidth(bWidth)
|
self.btnDoubleStyleO.setMaximumWidth(bWidth)
|
||||||
self.btnDoubleStyleO.clicked.connect(lambda: self._getQuote("DO"))
|
self.btnDoubleStyleO.clicked.connect(lambda: self._getQuote("DO"))
|
||||||
@@ -1175,7 +1153,7 @@ class GuiPreferencesQuotes(QWidget):
|
|||||||
self.quoteSym["DC"].setReadOnly(True)
|
self.quoteSym["DC"].setReadOnly(True)
|
||||||
self.quoteSym["DC"].setFixedWidth(qWidth)
|
self.quoteSym["DC"].setFixedWidth(qWidth)
|
||||||
self.quoteSym["DC"].setAlignment(Qt.AlignCenter)
|
self.quoteSym["DC"].setAlignment(Qt.AlignCenter)
|
||||||
self.quoteSym["DC"].setText(self.mainConf.fmtDoubleQuotes[1])
|
self.quoteSym["DC"].setText(self.mainConf.fmtDQuoteClose)
|
||||||
self.btnDoubleStyleC = QPushButton("...")
|
self.btnDoubleStyleC = QPushButton("...")
|
||||||
self.btnDoubleStyleC.setMaximumWidth(bWidth)
|
self.btnDoubleStyleC.setMaximumWidth(bWidth)
|
||||||
self.btnDoubleStyleC.clicked.connect(lambda: self._getQuote("DC"))
|
self.btnDoubleStyleC.clicked.connect(lambda: self._getQuote("DC"))
|
||||||
@@ -1192,13 +1170,10 @@ class GuiPreferencesQuotes(QWidget):
|
|||||||
"""Save the values set for this tab.
|
"""Save the values set for this tab.
|
||||||
"""
|
"""
|
||||||
# Quotation Style
|
# Quotation Style
|
||||||
self.mainConf.fmtSingleQuotes[0] = self.quoteSym["SO"].text()
|
self.mainConf.fmtSQuoteOpen = self.quoteSym["SO"].text()
|
||||||
self.mainConf.fmtSingleQuotes[1] = self.quoteSym["SC"].text()
|
self.mainConf.fmtSQuoteClose = self.quoteSym["SC"].text()
|
||||||
self.mainConf.fmtDoubleQuotes[0] = self.quoteSym["DO"].text()
|
self.mainConf.fmtDQuoteOpen = self.quoteSym["DO"].text()
|
||||||
self.mainConf.fmtDoubleQuotes[1] = self.quoteSym["DC"].text()
|
self.mainConf.fmtDQuoteClose = self.quoteSym["DC"].text()
|
||||||
|
|
||||||
self.mainConf.confChanged = True
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -27,16 +27,18 @@ import math
|
|||||||
import logging
|
import logging
|
||||||
import novelwriter
|
import novelwriter
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, QSize
|
from PyQt5.QtCore import Qt, QSize, pyqtSlot
|
||||||
from PyQt5.QtGui import QFont
|
from PyQt5.QtGui import QFont
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QWidget, QDialogButtonBox, QVBoxLayout, QTreeWidget, QTreeWidgetItem,
|
QAbstractItemView, QComboBox, QDialogButtonBox, QGridLayout, QHBoxLayout,
|
||||||
QLabel, QSpinBox, QGridLayout, QHBoxLayout, QLineEdit, QAbstractItemView
|
QLabel, QLineEdit, QSpinBox, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
||||||
|
QWidget
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from novelwriter.enum import nwItemClass
|
||||||
from novelwriter.common import numberToRoman
|
from novelwriter.common import numberToRoman
|
||||||
from novelwriter.constants import nwUnicode
|
from novelwriter.custom import PagedDialog, QSwitch
|
||||||
from novelwriter.gui.custom import PagedDialog, QSwitch
|
from novelwriter.constants import nwLabels, nwUnicode
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class GuiProjectDetails(PagedDialog):
|
class GuiProjectDetails(PagedDialog):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, mainGui):
|
||||||
PagedDialog.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
logger.debug("Initialising GuiProjectDetails ...")
|
logger.debug("Initialising GuiProjectDetails ...")
|
||||||
self.setObjectName("GuiProjectDetails")
|
self.setObjectName("GuiProjectDetails")
|
||||||
@@ -140,7 +142,7 @@ class GuiProjectDetails(PagedDialog):
|
|||||||
class GuiProjectDetailsMain(QWidget):
|
class GuiProjectDetailsMain(QWidget):
|
||||||
|
|
||||||
def __init__(self, mainGui, theProject):
|
def __init__(self, mainGui, theProject):
|
||||||
QWidget.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.theProject = theProject
|
self.theProject = theProject
|
||||||
@@ -155,7 +157,7 @@ class GuiProjectDetailsMain(QWidget):
|
|||||||
# Header
|
# Header
|
||||||
# ======
|
# ======
|
||||||
|
|
||||||
self.bookTitle = QLabel(self.theProject.bookTitle)
|
self.bookTitle = QLabel(self.theProject.data.title)
|
||||||
bookFont = self.bookTitle.font()
|
bookFont = self.bookTitle.font()
|
||||||
bookFont.setPointSizeF(2.2*fPt)
|
bookFont.setPointSizeF(2.2*fPt)
|
||||||
bookFont.setWeight(QFont.Bold)
|
bookFont.setWeight(QFont.Bold)
|
||||||
@@ -164,7 +166,7 @@ class GuiProjectDetailsMain(QWidget):
|
|||||||
self.bookTitle.setWordWrap(True)
|
self.bookTitle.setWordWrap(True)
|
||||||
|
|
||||||
self.projName = QLabel(
|
self.projName = QLabel(
|
||||||
self.tr("Working Title: {0}").format(self.theProject.projName)
|
self.tr("Working Title: {0}").format(self.theProject.data.name)
|
||||||
)
|
)
|
||||||
workFont = self.projName.font()
|
workFont = self.projName.font()
|
||||||
workFont.setPointSizeF(0.8*fPt)
|
workFont.setPointSizeF(0.8*fPt)
|
||||||
@@ -173,7 +175,9 @@ class GuiProjectDetailsMain(QWidget):
|
|||||||
self.projName.setAlignment(Qt.AlignHCenter)
|
self.projName.setAlignment(Qt.AlignHCenter)
|
||||||
self.projName.setWordWrap(True)
|
self.projName.setWordWrap(True)
|
||||||
|
|
||||||
self.bookAuthors = QLabel(self.tr("By {0}").format(self.theProject.getAuthors()))
|
self.bookAuthors = QLabel(self.tr("By {0}").format(
|
||||||
|
self.theProject.getFormattedAuthors()
|
||||||
|
))
|
||||||
authFont = self.bookAuthors.font()
|
authFont = self.bookAuthors.font()
|
||||||
authFont.setPointSizeF(1.2*fPt)
|
authFont.setPointSizeF(1.2*fPt)
|
||||||
self.bookAuthors.setFont(authFont)
|
self.bookAuthors.setFont(authFont)
|
||||||
@@ -253,10 +257,10 @@ class GuiProjectDetailsMain(QWidget):
|
|||||||
self.wordCountVal.setText(f"{nwCount:n}")
|
self.wordCountVal.setText(f"{nwCount:n}")
|
||||||
self.chapCountVal.setText(f"{hCounts[2]:n}")
|
self.chapCountVal.setText(f"{hCounts[2]:n}")
|
||||||
self.sceneCountVal.setText(f"{hCounts[3]:n}")
|
self.sceneCountVal.setText(f"{hCounts[3]:n}")
|
||||||
self.revCountVal.setText(f"{self.theProject.saveCount:n}")
|
self.revCountVal.setText(f"{self.theProject.data.saveCount:n}")
|
||||||
self.editTimeVal.setText(f"{edTime//3600:02d}:{edTime%3600//60:02d}")
|
self.editTimeVal.setText(f"{edTime//3600:02d}:{edTime%3600//60:02d}")
|
||||||
|
|
||||||
self.projPathVal.setText(self.theProject.projPath)
|
self.projPathVal.setText(str(self.theProject.storage.storagePath))
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -272,7 +276,7 @@ class GuiProjectDetailsContents(QWidget):
|
|||||||
C_PROG = 4
|
C_PROG = 4
|
||||||
|
|
||||||
def __init__(self, mainGui, theProject):
|
def __init__(self, mainGui, theProject):
|
||||||
QWidget.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.theProject = theProject
|
self.theProject = theProject
|
||||||
@@ -281,12 +285,26 @@ class GuiProjectDetailsContents(QWidget):
|
|||||||
|
|
||||||
# Internal
|
# Internal
|
||||||
self._theToC = []
|
self._theToC = []
|
||||||
|
self._currentRoot = None
|
||||||
|
|
||||||
iPx = self.mainTheme.baseIconSize
|
iPx = self.mainTheme.baseIconSize
|
||||||
hPx = self.mainConf.pxInt(12)
|
hPx = self.mainConf.pxInt(12)
|
||||||
vPx = self.mainConf.pxInt(4)
|
vPx = self.mainConf.pxInt(4)
|
||||||
pOptions = self.theProject.options
|
pOptions = self.theProject.options
|
||||||
|
|
||||||
|
# Header
|
||||||
|
# ======
|
||||||
|
|
||||||
|
self.tocLabel = QLabel("<b>%s</b>" % self.tr("Table of Contents"))
|
||||||
|
|
||||||
|
self.novelValue = QComboBox(self)
|
||||||
|
self.novelValue.setMinimumWidth(self.mainConf.pxInt(200))
|
||||||
|
self.novelValue.currentIndexChanged.connect(self._novelValueChanged)
|
||||||
|
|
||||||
|
self.headBox = QHBoxLayout()
|
||||||
|
self.headBox.addWidget(self.tocLabel)
|
||||||
|
self.headBox.addWidget(self.novelValue)
|
||||||
|
|
||||||
# Contents Tree
|
# Contents Tree
|
||||||
# =============
|
# =============
|
||||||
|
|
||||||
@@ -389,7 +407,7 @@ class GuiProjectDetailsContents(QWidget):
|
|||||||
# ========
|
# ========
|
||||||
|
|
||||||
self.outerBox = QVBoxLayout()
|
self.outerBox = QVBoxLayout()
|
||||||
self.outerBox.addWidget(QLabel("<b>%s</b>" % self.tr("Table of Contents")))
|
self.outerBox.addLayout(self.headBox)
|
||||||
self.outerBox.addWidget(self.tocTree)
|
self.outerBox.addWidget(self.tocTree)
|
||||||
self.outerBox.addLayout(self.optionsBox)
|
self.outerBox.addLayout(self.optionsBox)
|
||||||
|
|
||||||
@@ -412,19 +430,35 @@ class GuiProjectDetailsContents(QWidget):
|
|||||||
def updateValues(self):
|
def updateValues(self):
|
||||||
"""Populate the tree.
|
"""Populate the tree.
|
||||||
"""
|
"""
|
||||||
self._prepareData()
|
self._currentRoot = None
|
||||||
|
self._populateNovelList()
|
||||||
|
|
||||||
|
rootHandle = self.novelValue.currentData()
|
||||||
|
self._prepareData(rootHandle)
|
||||||
self._populateTree()
|
self._populateTree()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
def _prepareData(self):
|
def _populateNovelList(self):
|
||||||
"""Extract the data for the tree.
|
"""Fill the novel combo box with a list of all novel folders.
|
||||||
"""
|
"""
|
||||||
self._theToC = []
|
self.novelValue.clear()
|
||||||
self._theToC = self.theProject.index.getTableOfContents(2)
|
|
||||||
|
tIcon = self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])
|
||||||
|
for tHandle, nwItem in self.theProject.tree.iterRoots(nwItemClass.NOVEL):
|
||||||
|
self.novelValue.addItem(tIcon, nwItem.itemName, tHandle)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def _prepareData(self, rootHandle):
|
||||||
|
"""Extract the information from the project index.
|
||||||
|
"""
|
||||||
|
logger.debug("Populating ToC from handle '%s'", rootHandle)
|
||||||
|
self._theToC = self.theProject.index.getTableOfContents(rootHandle, 2)
|
||||||
self._theToC.append(("", 0, self.tr("END"), 0))
|
self._theToC.append(("", 0, self.tr("END"), 0))
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -432,6 +466,18 @@ class GuiProjectDetailsContents(QWidget):
|
|||||||
# Slots
|
# Slots
|
||||||
##
|
##
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def _novelValueChanged(self):
|
||||||
|
"""Refresh the tree with another root item.
|
||||||
|
"""
|
||||||
|
rootHandle = self.novelValue.currentData()
|
||||||
|
if rootHandle != self._currentRoot:
|
||||||
|
self._prepareData(rootHandle)
|
||||||
|
self._populateTree()
|
||||||
|
self._currentRoot = rootHandle
|
||||||
|
return
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
def _populateTree(self):
|
def _populateTree(self):
|
||||||
"""Set the content of the chapter/page tree.
|
"""Set the content of the chapter/page tree.
|
||||||
"""
|
"""
|
||||||
@@ -466,10 +512,11 @@ class GuiProjectDetailsContents(QWidget):
|
|||||||
progPage = f"{cPage:n}"
|
progPage = f"{cPage:n}"
|
||||||
progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%"
|
progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%"
|
||||||
|
|
||||||
|
hDec = self.mainTheme.getHeaderDecoration(tLevel)
|
||||||
if tTitle.strip() == "":
|
if tTitle.strip() == "":
|
||||||
tTitle = self.tr("Untitled")
|
tTitle = self.tr("Untitled")
|
||||||
|
|
||||||
newItem.setIcon(self.C_TITLE, self.mainTheme.getIcon("doc_h%d" % tLevel))
|
newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
|
||||||
newItem.setText(self.C_TITLE, tTitle)
|
newItem.setText(self.C_TITLE, tTitle)
|
||||||
newItem.setText(self.C_WORDS, f"{wCount:n}")
|
newItem.setText(self.C_WORDS, f"{wCount:n}")
|
||||||
newItem.setText(self.C_PAGES, f"{pCount:n}")
|
newItem.setText(self.C_PAGES, f"{pCount:n}")
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
import novelwriter
|
import novelwriter
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from PyQt5.QtGui import QKeySequence
|
from PyQt5.QtGui import QKeySequence
|
||||||
@@ -54,7 +54,7 @@ class GuiProjectLoad(QDialog):
|
|||||||
C_TIME = 2
|
C_TIME = 2
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, mainGui):
|
||||||
QDialog.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
logger.debug("Initialising GuiProjectLoad ...")
|
logger.debug("Initialising GuiProjectLoad ...")
|
||||||
self.setObjectName("GuiProjectLoad")
|
self.setObjectName("GuiProjectLoad")
|
||||||
@@ -77,7 +77,6 @@ class GuiProjectLoad(QDialog):
|
|||||||
self.setWindowTitle(self.tr("Open Project"))
|
self.setWindowTitle(self.tr("Open Project"))
|
||||||
self.setMinimumWidth(self.mainConf.pxInt(650))
|
self.setMinimumWidth(self.mainConf.pxInt(650))
|
||||||
self.setMinimumHeight(self.mainConf.pxInt(400))
|
self.setMinimumHeight(self.mainConf.pxInt(400))
|
||||||
self.setModal(True)
|
|
||||||
|
|
||||||
self.nwIcon = QLabel()
|
self.nwIcon = QLabel()
|
||||||
self.nwIcon.setPixmap(self.mainGui.mainTheme.getPixmap("novelwriter", (nPx, nPx)))
|
self.nwIcon.setPixmap(self.mainGui.mainTheme.getPixmap("novelwriter", (nPx, nPx)))
|
||||||
@@ -158,7 +157,6 @@ class GuiProjectLoad(QDialog):
|
|||||||
def _doOpenRecent(self):
|
def _doOpenRecent(self):
|
||||||
"""Close the dialog window with a recent project selected.
|
"""Close the dialog window with a recent project selected.
|
||||||
"""
|
"""
|
||||||
logger.verbose("GuiProjectLoad open button clicked")
|
|
||||||
self._saveSettings()
|
self._saveSettings()
|
||||||
|
|
||||||
self.openPath = None
|
self.openPath = None
|
||||||
@@ -183,7 +181,6 @@ class GuiProjectLoad(QDialog):
|
|||||||
def _doBrowse(self):
|
def _doBrowse(self):
|
||||||
"""Browse for a folder path.
|
"""Browse for a folder path.
|
||||||
"""
|
"""
|
||||||
logger.verbose("GuiProjectLoad browse button clicked")
|
|
||||||
extFilter = [
|
extFilter = [
|
||||||
self.tr("novelWriter Project File ({0})").format(nwFiles.PROJ_FILE),
|
self.tr("novelWriter Project File ({0})").format(nwFiles.PROJ_FILE),
|
||||||
self.tr("All files ({0})").format("*"),
|
self.tr("All files ({0})").format("*"),
|
||||||
@@ -192,8 +189,8 @@ class GuiProjectLoad(QDialog):
|
|||||||
self, self.tr("Open Project"), "", filter=";;".join(extFilter)
|
self, self.tr("Open Project"), "", filter=";;".join(extFilter)
|
||||||
)
|
)
|
||||||
if projFile:
|
if projFile:
|
||||||
thePath = os.path.abspath(os.path.dirname(projFile))
|
thePath = Path(projFile).absolute()
|
||||||
self.selPath.setText(thePath)
|
self.selPath.setText(str(thePath))
|
||||||
self.openPath = thePath
|
self.openPath = thePath
|
||||||
self.openState = self.OPEN_STATE
|
self.openState = self.OPEN_STATE
|
||||||
self.accept()
|
self.accept()
|
||||||
@@ -203,7 +200,6 @@ class GuiProjectLoad(QDialog):
|
|||||||
def _doCancel(self):
|
def _doCancel(self):
|
||||||
"""Close the dialog window without doing anything.
|
"""Close the dialog window without doing anything.
|
||||||
"""
|
"""
|
||||||
logger.verbose("GuiProjectLoad close button clicked")
|
|
||||||
self.openPath = None
|
self.openPath = None
|
||||||
self.openState = self.NONE_STATE
|
self.openState = self.NONE_STATE
|
||||||
self.close()
|
self.close()
|
||||||
@@ -212,7 +208,6 @@ class GuiProjectLoad(QDialog):
|
|||||||
def _doNewProject(self):
|
def _doNewProject(self):
|
||||||
"""Create a new project.
|
"""Create a new project.
|
||||||
"""
|
"""
|
||||||
logger.verbose("GuiProjectLoad new project button clicked")
|
|
||||||
self._saveSettings()
|
self._saveSettings()
|
||||||
self.openPath = None
|
self.openPath = None
|
||||||
self.openState = self.NEW_STATE
|
self.openState = self.NEW_STATE
|
||||||
@@ -233,7 +228,7 @@ class GuiProjectLoad(QDialog):
|
|||||||
).format(projName)
|
).format(projName)
|
||||||
)
|
)
|
||||||
if msgYes:
|
if msgYes:
|
||||||
self.mainConf.removeFromRecentCache(
|
self.mainConf.recentProjects.remove(
|
||||||
selList[0].data(self.C_NAME, Qt.UserRole)
|
selList[0].data(self.C_NAME, Qt.UserRole)
|
||||||
)
|
)
|
||||||
self._populateList()
|
self._populateList()
|
||||||
@@ -262,29 +257,23 @@ class GuiProjectLoad(QDialog):
|
|||||||
colWidths[self.C_NAME] = self.listBox.columnWidth(self.C_NAME)
|
colWidths[self.C_NAME] = self.listBox.columnWidth(self.C_NAME)
|
||||||
colWidths[self.C_COUNT] = self.listBox.columnWidth(self.C_COUNT)
|
colWidths[self.C_COUNT] = self.listBox.columnWidth(self.C_COUNT)
|
||||||
colWidths[self.C_TIME] = self.listBox.columnWidth(self.C_TIME)
|
colWidths[self.C_TIME] = self.listBox.columnWidth(self.C_TIME)
|
||||||
self.mainConf.setProjColWidths(colWidths)
|
self.mainConf.setProjLoadColWidths(colWidths)
|
||||||
return
|
return
|
||||||
|
|
||||||
def _populateList(self):
|
def _populateList(self):
|
||||||
"""Populate the list box with recent project data.
|
"""Populate the list box with recent project data.
|
||||||
"""
|
"""
|
||||||
dataList = []
|
|
||||||
for projPath in self.mainConf.recentProj:
|
|
||||||
theEntry = self.mainConf.recentProj[projPath]
|
|
||||||
theTitle = theEntry.get("title", "")
|
|
||||||
theTime = theEntry.get("time", 0)
|
|
||||||
theWords = theEntry.get("words", 0)
|
|
||||||
dataList.append([theTitle, theTime, theWords, projPath])
|
|
||||||
|
|
||||||
self.listBox.clear()
|
self.listBox.clear()
|
||||||
sortList = sorted(dataList, key=lambda x: x[1], reverse=True)
|
dataList = self.mainConf.recentProjects.listEntries()
|
||||||
for theTitle, theTime, theWords, projPath in sortList:
|
sortList = sorted(dataList, key=lambda x: x[3], reverse=True)
|
||||||
|
nwxIcon = self.mainGui.mainTheme.getIcon("proj_nwx")
|
||||||
|
for path, title, words, time in sortList:
|
||||||
newItem = QTreeWidgetItem([""]*4)
|
newItem = QTreeWidgetItem([""]*4)
|
||||||
newItem.setIcon(self.C_NAME, self.mainGui.mainTheme.getIcon("proj_nwx"))
|
newItem.setIcon(self.C_NAME, nwxIcon)
|
||||||
newItem.setText(self.C_NAME, theTitle)
|
newItem.setText(self.C_NAME, title)
|
||||||
newItem.setData(self.C_NAME, Qt.UserRole, projPath)
|
newItem.setData(self.C_NAME, Qt.UserRole, path)
|
||||||
newItem.setText(self.C_COUNT, formatInt(theWords))
|
newItem.setText(self.C_COUNT, formatInt(words))
|
||||||
newItem.setText(self.C_TIME, datetime.fromtimestamp(theTime).strftime("%x %X"))
|
newItem.setText(self.C_TIME, datetime.fromtimestamp(time).strftime("%x %X"))
|
||||||
newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter)
|
newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter)
|
||||||
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter)
|
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter)
|
||||||
newItem.setTextAlignment(self.C_TIME, Qt.AlignRight | Qt.AlignVCenter)
|
newItem.setTextAlignment(self.C_TIME, Qt.AlignRight | Qt.AlignVCenter)
|
||||||
@@ -294,7 +283,7 @@ class GuiProjectLoad(QDialog):
|
|||||||
if self.listBox.topLevelItemCount() > 0:
|
if self.listBox.topLevelItemCount() > 0:
|
||||||
self.listBox.topLevelItem(0).setSelected(True)
|
self.listBox.topLevelItem(0).setSelected(True)
|
||||||
|
|
||||||
projColWidth = self.mainConf.getProjColWidths()
|
projColWidth = self.mainConf.projLoadColWidths
|
||||||
if len(projColWidth) == 3:
|
if len(projColWidth) == 3:
|
||||||
self.listBox.setColumnWidth(self.C_NAME, projColWidth[self.C_NAME])
|
self.listBox.setColumnWidth(self.C_NAME, projColWidth[self.C_NAME])
|
||||||
self.listBox.setColumnWidth(self.C_COUNT, projColWidth[self.C_COUNT])
|
self.listBox.setColumnWidth(self.C_COUNT, projColWidth[self.C_COUNT])
|
||||||
|
|||||||
@@ -36,15 +36,20 @@ from PyQt5.QtWidgets import (
|
|||||||
|
|
||||||
from novelwriter.enum import nwAlert
|
from novelwriter.enum import nwAlert
|
||||||
from novelwriter.common import simplified
|
from novelwriter.common import simplified
|
||||||
from novelwriter.gui.custom import QSwitch, PagedDialog, QConfigLayout
|
from novelwriter.custom import QSwitch, PagedDialog, QConfigLayout
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class GuiProjectSettings(PagedDialog):
|
class GuiProjectSettings(PagedDialog):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
TAB_MAIN = 0
|
||||||
PagedDialog.__init__(self, mainGui)
|
TAB_STATUS = 1
|
||||||
|
TAB_IMPORT = 2
|
||||||
|
TAB_REPLACE = 3
|
||||||
|
|
||||||
|
def __init__(self, mainGui, focusTab=TAB_MAIN):
|
||||||
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
logger.debug("Initialising GuiProjectSettings ...")
|
logger.debug("Initialising GuiProjectSettings ...")
|
||||||
self.setObjectName("GuiProjectSettings")
|
self.setObjectName("GuiProjectSettings")
|
||||||
@@ -67,10 +72,10 @@ class GuiProjectSettings(PagedDialog):
|
|||||||
self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH))
|
self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH))
|
||||||
)
|
)
|
||||||
|
|
||||||
self.tabMain = GuiProjectEditMain(self.mainGui, self.theProject)
|
self.tabMain = GuiProjectEditMain(self)
|
||||||
self.tabStatus = GuiProjectEditStatus(self.mainGui, self.theProject, True)
|
self.tabStatus = GuiProjectEditStatus(self, True)
|
||||||
self.tabImport = GuiProjectEditStatus(self.mainGui, self.theProject, False)
|
self.tabImport = GuiProjectEditStatus(self, False)
|
||||||
self.tabReplace = GuiProjectEditReplace(self.mainGui, self.theProject)
|
self.tabReplace = GuiProjectEditReplace(self)
|
||||||
|
|
||||||
self.addTab(self.tabMain, self.tr("Settings"))
|
self.addTab(self.tabMain, self.tr("Settings"))
|
||||||
self.addTab(self.tabStatus, self.tr("Status"))
|
self.addTab(self.tabStatus, self.tr("Status"))
|
||||||
@@ -83,12 +88,19 @@ class GuiProjectSettings(PagedDialog):
|
|||||||
self.addControls(self.buttonBox)
|
self.addControls(self.buttonBox)
|
||||||
|
|
||||||
# Flags
|
# Flags
|
||||||
self.spellChanged = False
|
self._spellChanged = False
|
||||||
|
|
||||||
|
# Focus Tab
|
||||||
|
self._focusTab(focusTab)
|
||||||
|
|
||||||
logger.debug("GuiProjectSettings initialisation complete")
|
logger.debug("GuiProjectSettings initialisation complete")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def spellChanged(self):
|
||||||
|
return self._spellChanged
|
||||||
|
|
||||||
##
|
##
|
||||||
# Slots
|
# Slots
|
||||||
##
|
##
|
||||||
@@ -96,21 +108,19 @@ class GuiProjectSettings(PagedDialog):
|
|||||||
def _doSave(self):
|
def _doSave(self):
|
||||||
"""Save settings and close dialog.
|
"""Save settings and close dialog.
|
||||||
"""
|
"""
|
||||||
logger.verbose("GuiProjectSettings save button clicked")
|
|
||||||
|
|
||||||
projName = self.tabMain.editName.text()
|
projName = self.tabMain.editName.text()
|
||||||
bookTitle = self.tabMain.editTitle.text()
|
bookTitle = self.tabMain.editTitle.text()
|
||||||
bookAuthors = self.tabMain.editAuthors.toPlainText()
|
bookAuthors = self.tabMain.editAuthors.toPlainText()
|
||||||
spellLang = self.tabMain.spellLang.currentData()
|
spellLang = self.tabMain.spellLang.currentData()
|
||||||
doBackup = not self.tabMain.doBackup.isChecked()
|
doBackup = not self.tabMain.doBackup.isChecked()
|
||||||
|
|
||||||
self.theProject.setProjectName(projName)
|
self.theProject.data.setName(projName)
|
||||||
self.theProject.setBookTitle(bookTitle)
|
self.theProject.data.setTitle(bookTitle)
|
||||||
self.theProject.setBookAuthors(bookAuthors)
|
self.theProject.data.setAuthors(bookAuthors)
|
||||||
self.theProject.setProjBackup(doBackup)
|
self.theProject.data.setDoBackup(doBackup)
|
||||||
|
|
||||||
# Remember this as updating spell dictionary can be expensive
|
# Remember this as updating spell dictionary can be expensive
|
||||||
self.spellChanged = self.theProject.setSpellLang(spellLang)
|
self._spellChanged = self.theProject.data.setSpellLang(spellLang)
|
||||||
|
|
||||||
if self.tabStatus.colChanged:
|
if self.tabStatus.colChanged:
|
||||||
newList, delList = self.tabStatus.getNewList()
|
newList, delList = self.tabStatus.getNewList()
|
||||||
@@ -125,7 +135,7 @@ class GuiProjectSettings(PagedDialog):
|
|||||||
|
|
||||||
if self.tabReplace.arChanged:
|
if self.tabReplace.arChanged:
|
||||||
newList = self.tabReplace.getNewList()
|
newList = self.tabReplace.getNewList()
|
||||||
self.theProject.setAutoReplace(newList)
|
self.theProject.data.setAutoReplace(newList)
|
||||||
|
|
||||||
self._saveGuiSettings()
|
self._saveGuiSettings()
|
||||||
self.accept()
|
self.accept()
|
||||||
@@ -143,6 +153,19 @@ class GuiProjectSettings(PagedDialog):
|
|||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
|
def _focusTab(self, tab):
|
||||||
|
"""Change which is the focused tab.
|
||||||
|
"""
|
||||||
|
if tab == self.TAB_MAIN:
|
||||||
|
self.setCurrentWidget(self.tabMain)
|
||||||
|
elif tab == self.TAB_STATUS:
|
||||||
|
self.setCurrentWidget(self.tabStatus)
|
||||||
|
elif tab == self.TAB_IMPORT:
|
||||||
|
self.setCurrentWidget(self.tabImport)
|
||||||
|
elif tab == self.TAB_REPLACE:
|
||||||
|
self.setCurrentWidget(self.tabReplace)
|
||||||
|
return
|
||||||
|
|
||||||
def _saveGuiSettings(self):
|
def _saveGuiSettings(self):
|
||||||
"""Save GUI settings.
|
"""Save GUI settings.
|
||||||
"""
|
"""
|
||||||
@@ -166,12 +189,12 @@ class GuiProjectSettings(PagedDialog):
|
|||||||
|
|
||||||
class GuiProjectEditMain(QWidget):
|
class GuiProjectEditMain(QWidget):
|
||||||
|
|
||||||
def __init__(self, mainGui, theProject):
|
def __init__(self, projGui):
|
||||||
QWidget.__init__(self, mainGui)
|
super().__init__(parent=projGui)
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.mainGui = projGui.mainGui
|
||||||
self.theProject = theProject
|
self.theProject = projGui.theProject
|
||||||
|
|
||||||
# The Form
|
# The Form
|
||||||
self.mainForm = QConfigLayout()
|
self.mainForm = QConfigLayout()
|
||||||
@@ -186,7 +209,7 @@ class GuiProjectEditMain(QWidget):
|
|||||||
self.editName = QLineEdit()
|
self.editName = QLineEdit()
|
||||||
self.editName.setMaxLength(200)
|
self.editName.setMaxLength(200)
|
||||||
self.editName.setMaximumWidth(xW)
|
self.editName.setMaximumWidth(xW)
|
||||||
self.editName.setText(self.theProject.projName)
|
self.editName.setText(self.theProject.data.name)
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
self.tr("Project name"),
|
self.tr("Project name"),
|
||||||
self.editName,
|
self.editName,
|
||||||
@@ -196,7 +219,7 @@ class GuiProjectEditMain(QWidget):
|
|||||||
self.editTitle = QLineEdit()
|
self.editTitle = QLineEdit()
|
||||||
self.editTitle.setMaxLength(200)
|
self.editTitle.setMaxLength(200)
|
||||||
self.editTitle.setMaximumWidth(xW)
|
self.editTitle.setMaximumWidth(xW)
|
||||||
self.editTitle.setText(self.theProject.bookTitle)
|
self.editTitle.setText(self.theProject.data.title)
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
self.tr("Novel title"),
|
self.tr("Novel title"),
|
||||||
self.editTitle,
|
self.editTitle,
|
||||||
@@ -206,7 +229,7 @@ class GuiProjectEditMain(QWidget):
|
|||||||
self.editAuthors = QPlainTextEdit()
|
self.editAuthors = QPlainTextEdit()
|
||||||
self.editAuthors.setMaximumHeight(xH)
|
self.editAuthors.setMaximumHeight(xH)
|
||||||
self.editAuthors.setMaximumWidth(xW)
|
self.editAuthors.setMaximumWidth(xW)
|
||||||
self.editAuthors.setPlainText("\n".join(self.theProject.bookAuthors))
|
self.editAuthors.setPlainText("\n".join(self.theProject.data.authors))
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
self.tr("Author(s)"),
|
self.tr("Author(s)"),
|
||||||
self.editAuthors,
|
self.editAuthors,
|
||||||
@@ -229,13 +252,13 @@ class GuiProjectEditMain(QWidget):
|
|||||||
)
|
)
|
||||||
|
|
||||||
spellIdx = 0
|
spellIdx = 0
|
||||||
if self.theProject.projSpell is not None:
|
if self.theProject.data.spellLang is not None:
|
||||||
spellIdx = self.spellLang.findData(self.theProject.projSpell)
|
spellIdx = self.spellLang.findData(self.theProject.data.spellLang)
|
||||||
if spellIdx != -1:
|
if spellIdx != -1:
|
||||||
self.spellLang.setCurrentIndex(spellIdx)
|
self.spellLang.setCurrentIndex(spellIdx)
|
||||||
|
|
||||||
self.doBackup = QSwitch(self)
|
self.doBackup = QSwitch(self)
|
||||||
self.doBackup.setChecked(not self.theProject.doBackup)
|
self.doBackup.setChecked(not self.theProject.data.doBackup)
|
||||||
self.mainForm.addRow(
|
self.mainForm.addRow(
|
||||||
self.tr("No backup on close"),
|
self.tr("No backup on close"),
|
||||||
self.doBackup,
|
self.doBackup,
|
||||||
@@ -256,20 +279,20 @@ class GuiProjectEditStatus(QWidget):
|
|||||||
COL_ROLE = Qt.UserRole + 1
|
COL_ROLE = Qt.UserRole + 1
|
||||||
NUM_ROLE = Qt.UserRole + 2
|
NUM_ROLE = Qt.UserRole + 2
|
||||||
|
|
||||||
def __init__(self, mainGui, theProject, isStatus):
|
def __init__(self, projGui, isStatus):
|
||||||
QWidget.__init__(self, mainGui)
|
super().__init__(parent=projGui)
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.mainGui = projGui.mainGui
|
||||||
self.theProject = theProject
|
self.theProject = projGui.theProject
|
||||||
self.mainTheme = mainGui.mainTheme
|
self.mainTheme = projGui.mainGui.mainTheme
|
||||||
|
|
||||||
if isStatus:
|
if isStatus:
|
||||||
self.theStatus = self.theProject.statusItems
|
self.theStatus = self.theProject.data.itemStatus
|
||||||
pageLabel = self.tr("Novel File Status Levels")
|
pageLabel = self.tr("Novel File Status Levels")
|
||||||
colSetting = "statusColW"
|
colSetting = "statusColW"
|
||||||
else:
|
else:
|
||||||
self.theStatus = self.theProject.importItems
|
self.theStatus = self.theProject.data.itemImport
|
||||||
pageLabel = self.tr("Note File Importance Levels")
|
pageLabel = self.tr("Note File Importance Levels")
|
||||||
colSetting = "importColW"
|
colSetting = "importColW"
|
||||||
|
|
||||||
@@ -367,11 +390,12 @@ class GuiProjectEditStatus(QWidget):
|
|||||||
newList = []
|
newList = []
|
||||||
for n in range(self.listBox.topLevelItemCount()):
|
for n in range(self.listBox.topLevelItemCount()):
|
||||||
item = self.listBox.topLevelItem(n)
|
item = self.listBox.topLevelItem(n)
|
||||||
newList.append({
|
if item is not None:
|
||||||
"key": item.data(self.COL_LABEL, self.KEY_ROLE),
|
newList.append({
|
||||||
"name": item.text(self.COL_LABEL),
|
"key": item.data(self.COL_LABEL, self.KEY_ROLE),
|
||||||
"cols": item.data(self.COL_LABEL, self.COL_ROLE),
|
"name": item.text(self.COL_LABEL),
|
||||||
})
|
"cols": item.data(self.COL_LABEL, self.COL_ROLE),
|
||||||
|
})
|
||||||
return newList, self.colDeleted
|
return newList, self.colDeleted
|
||||||
|
|
||||||
return [], []
|
return [], []
|
||||||
@@ -470,7 +494,8 @@ class GuiProjectEditStatus(QWidget):
|
|||||||
self.listBox.insertTopLevelItem(nIndex, cItem)
|
self.listBox.insertTopLevelItem(nIndex, cItem)
|
||||||
self.listBox.clearSelection()
|
self.listBox.clearSelection()
|
||||||
|
|
||||||
cItem.setSelected(True)
|
if cItem is not None:
|
||||||
|
cItem.setSelected(True)
|
||||||
self.colChanged = True
|
self.colChanged = True
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -527,13 +552,13 @@ class GuiProjectEditReplace(QWidget):
|
|||||||
COL_KEY = 0
|
COL_KEY = 0
|
||||||
COL_REPL = 1
|
COL_REPL = 1
|
||||||
|
|
||||||
def __init__(self, mainGui, theProject):
|
def __init__(self, projGui):
|
||||||
QWidget.__init__(self, mainGui)
|
super().__init__(parent=projGui)
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.mainGui = projGui.mainGui
|
||||||
self.mainTheme = mainGui.mainTheme
|
self.mainTheme = projGui.mainGui.mainTheme
|
||||||
self.theProject = theProject
|
self.theProject = projGui.theProject
|
||||||
self.arChanged = False
|
self.arChanged = False
|
||||||
|
|
||||||
wCol0 = self.mainConf.pxInt(
|
wCol0 = self.mainConf.pxInt(
|
||||||
@@ -553,7 +578,7 @@ class GuiProjectEditReplace(QWidget):
|
|||||||
self.listBox.setColumnWidth(self.COL_KEY, wCol0)
|
self.listBox.setColumnWidth(self.COL_KEY, wCol0)
|
||||||
self.listBox.setIndentation(0)
|
self.listBox.setIndentation(0)
|
||||||
|
|
||||||
for aKey, aVal in self.theProject.autoReplace.items():
|
for aKey, aVal in self.theProject.data.autoReplace.items():
|
||||||
newItem = QTreeWidgetItem(["<%s>" % aKey, aVal])
|
newItem = QTreeWidgetItem(["<%s>" % aKey, aVal])
|
||||||
self.listBox.addTopLevelItem(newItem)
|
self.listBox.addTopLevelItem(newItem)
|
||||||
|
|
||||||
@@ -619,10 +644,11 @@ class GuiProjectEditReplace(QWidget):
|
|||||||
newList = {}
|
newList = {}
|
||||||
for n in range(self.listBox.topLevelItemCount()):
|
for n in range(self.listBox.topLevelItemCount()):
|
||||||
tItem = self.listBox.topLevelItem(n)
|
tItem = self.listBox.topLevelItem(n)
|
||||||
aKey = self._stripNotAllowed(tItem.text(0))
|
if tItem is not None:
|
||||||
aVal = tItem.text(1)
|
aKey = self._stripNotAllowed(tItem.text(0))
|
||||||
if len(aKey) > 0:
|
aVal = tItem.text(1)
|
||||||
newList[aKey] = aVal
|
if len(aKey) > 0:
|
||||||
|
newList[aKey] = aVal
|
||||||
|
|
||||||
return newList
|
return newList
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class GuiQuoteSelect(QDialog):
|
|||||||
selectedQuote = ""
|
selectedQuote = ""
|
||||||
|
|
||||||
def __init__(self, parent=None, currentQuote='"'):
|
def __init__(self, parent=None, currentQuote='"'):
|
||||||
QDialog.__init__(self, parent=parent)
|
super().__init__(parent=parent)
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class GuiUpdates(QDialog):
|
class GuiUpdates(QDialog):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, mainGui):
|
||||||
QDialog.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
logger.debug("Initialising GuiUpdates ...")
|
logger.debug("Initialising GuiUpdates ...")
|
||||||
self.setObjectName("GuiUpdates")
|
self.setObjectName("GuiUpdates")
|
||||||
@@ -135,7 +135,7 @@ class GuiUpdates(QDialog):
|
|||||||
logException()
|
logException()
|
||||||
|
|
||||||
relVersion = rawData.get("tag_name", "Unknown")
|
relVersion = rawData.get("tag_name", "Unknown")
|
||||||
relDate = rawData.get("created_at", None)
|
relDate = rawData.get("created_at", "")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
relDate = datetime.strptime(relDate[:10], "%Y-%m-%d").strftime("%x")
|
relDate = datetime.strptime(relDate[:10], "%Y-%m-%d").strftime("%x")
|
||||||
|
|||||||
@@ -23,10 +23,11 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
import novelwriter
|
import novelwriter
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget,
|
QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget,
|
||||||
@@ -43,7 +44,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class GuiWordList(QDialog):
|
class GuiWordList(QDialog):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, mainGui):
|
||||||
QDialog.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
logger.debug("Initialising GuiWordList ...")
|
logger.debug("Initialising GuiWordList ...")
|
||||||
self.setObjectName("GuiWordList")
|
self.setObjectName("GuiWordList")
|
||||||
@@ -150,13 +151,19 @@ class GuiWordList(QDialog):
|
|||||||
"""
|
"""
|
||||||
self._saveGuiSettings()
|
self._saveGuiSettings()
|
||||||
|
|
||||||
dctFile = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT)
|
dctFile = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT)
|
||||||
tmpFile = dctFile + "~"
|
if not isinstance(dctFile, Path):
|
||||||
|
return False
|
||||||
|
|
||||||
|
tmpFile = dctFile.with_suffix(".tmp")
|
||||||
try:
|
try:
|
||||||
with open(tmpFile, mode="w", encoding="utf-8") as outFile:
|
with open(tmpFile, mode="w", encoding="utf-8") as outFile:
|
||||||
for i in range(self.listBox.count()):
|
for i in range(self.listBox.count()):
|
||||||
outFile.write(self.listBox.item(i).text() + "\n")
|
item = self.listBox.item(i)
|
||||||
|
if item is not None:
|
||||||
|
outFile.write(item.text() + "\n")
|
||||||
|
|
||||||
|
tmpFile.replace(dctFile)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Could not save new word list")
|
logger.error("Could not save new word list")
|
||||||
@@ -164,9 +171,6 @@ class GuiWordList(QDialog):
|
|||||||
self.reject()
|
self.reject()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if os.path.isfile(dctFile):
|
|
||||||
os.unlink(dctFile)
|
|
||||||
os.rename(tmpFile, dctFile)
|
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -185,10 +189,12 @@ class GuiWordList(QDialog):
|
|||||||
def _loadWordList(self):
|
def _loadWordList(self):
|
||||||
"""Load the project's word list, if it exists.
|
"""Load the project's word list, if it exists.
|
||||||
"""
|
"""
|
||||||
self.listBox.clear()
|
wordList = self.theProject.storage.getMetaFile(nwFiles.PROJ_DICT)
|
||||||
|
if not isinstance(wordList, Path):
|
||||||
|
return False
|
||||||
|
|
||||||
wordList = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT)
|
self.listBox.clear()
|
||||||
if not os.path.isfile(wordList):
|
if not wordList.exists():
|
||||||
logger.debug("No project dictionary file found")
|
logger.debug("No project dictionary file found")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -112,9 +112,10 @@ class nwDocInsert(Enum):
|
|||||||
QUOTE_RS = 2
|
QUOTE_RS = 2
|
||||||
QUOTE_LD = 3
|
QUOTE_LD = 3
|
||||||
QUOTE_RD = 4
|
QUOTE_RD = 4
|
||||||
NEW_PAGE = 5
|
SYNOPSIS = 5
|
||||||
VSPACE_S = 6
|
NEW_PAGE = 6
|
||||||
VSPACE_M = 7
|
VSPACE_S = 7
|
||||||
|
VSPACE_M = 8
|
||||||
|
|
||||||
# END Enum nwDocInsert
|
# END Enum nwDocInsert
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,8 @@ def logException():
|
|||||||
"""Log the content of an exception message.
|
"""Log the content of an exception message.
|
||||||
"""
|
"""
|
||||||
exType, exValue, _ = sys.exc_info()
|
exType, exValue, _ = sys.exc_info()
|
||||||
logger.error("%s: %s", exType.__name__, str(exValue))
|
if exType is not None:
|
||||||
|
logger.error("%s: %s", exType.__name__, str(exValue))
|
||||||
|
|
||||||
|
|
||||||
def formatException(exc):
|
def formatException(exc):
|
||||||
@@ -61,7 +62,7 @@ def formatException(exc):
|
|||||||
class NWErrorMessage(QDialog):
|
class NWErrorMessage(QDialog):
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
QDialog.__init__(self, parent=parent)
|
super().__init__(parent=parent)
|
||||||
self.setObjectName("NWErrorMessage")
|
self.setObjectName("NWErrorMessage")
|
||||||
|
|
||||||
# Widgets
|
# Widgets
|
||||||
@@ -131,7 +132,7 @@ class NWErrorMessage(QDialog):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
import lxml
|
import lxml
|
||||||
lxmlVersion = lxml.__version__
|
lxmlVersion = lxml.__version__ # type: ignore
|
||||||
except Exception:
|
except Exception:
|
||||||
lxmlVersion = "Unknown"
|
lxmlVersion = "Unknown"
|
||||||
|
|
||||||
@@ -198,8 +199,8 @@ def exceptionHandler(exType, exValue, exTrace):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Try a controlled shutdown
|
# Try a controlled shutdown
|
||||||
nwGUI.closeProject(isYes=True)
|
nwGUI.closeProject(isYes=True) # type: ignore
|
||||||
nwGUI.closeMain()
|
nwGUI.closeMain() # type: ignore
|
||||||
logger.info("Emergency shutdown successful")
|
logger.info("Emergency shutdown successful")
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
BLOCK_TITLE = 4
|
BLOCK_TITLE = 4
|
||||||
|
|
||||||
def __init__(self, theDoc, mainGui, spEnchant):
|
def __init__(self, theDoc, mainGui, spEnchant):
|
||||||
QSyntaxHighlighter.__init__(self, theDoc)
|
super().__init__(theDoc)
|
||||||
|
|
||||||
logger.debug("Initialising GuiDocHighlighter ...")
|
logger.debug("Initialising GuiDocHighlighter ...")
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
@@ -151,11 +151,13 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
|
|
||||||
# Quoted Strings
|
# Quoted Strings
|
||||||
if self.mainConf.highlightQuotes:
|
if self.mainConf.highlightQuotes:
|
||||||
fmtDbl = self.mainConf.fmtDoubleQuotes
|
fmtDblO = self.mainConf.fmtDQuoteOpen
|
||||||
fmtSng = self.mainConf.fmtSingleQuotes
|
fmtDblC = self.mainConf.fmtDQuoteClose
|
||||||
|
fmtSngO = self.mainConf.fmtSQuoteOpen
|
||||||
|
fmtSngC = self.mainConf.fmtSQuoteClose
|
||||||
|
|
||||||
# Straight Quotes
|
# Straight Quotes
|
||||||
if fmtDbl != ["\"", "\""]:
|
if not (fmtDblO == fmtDblC == "\""):
|
||||||
self.hRules.append((
|
self.hRules.append((
|
||||||
"(\\B\")(.*?)(\"\\B)", {
|
"(\\B\")(.*?)(\"\\B)", {
|
||||||
0: self.hStyles["dialogue1"],
|
0: self.hStyles["dialogue1"],
|
||||||
@@ -165,7 +167,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
# Double Quotes
|
# Double Quotes
|
||||||
dblEnd = "|$" if self.mainConf.allowOpenDQuote else ""
|
dblEnd = "|$" if self.mainConf.allowOpenDQuote else ""
|
||||||
self.hRules.append((
|
self.hRules.append((
|
||||||
f"(\\B{fmtDbl[0]})(.*?)({fmtDbl[1]}\\B{dblEnd})", {
|
f"(\\B{fmtDblO})(.*?)({fmtDblC}\\B{dblEnd})", {
|
||||||
0: self.hStyles["dialogue2"],
|
0: self.hStyles["dialogue2"],
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
@@ -173,7 +175,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
# Single Quotes
|
# Single Quotes
|
||||||
sngEnd = "|$" if self.mainConf.allowOpenSQuote else ""
|
sngEnd = "|$" if self.mainConf.allowOpenSQuote else ""
|
||||||
self.hRules.append((
|
self.hRules.append((
|
||||||
f"(\\B{fmtSng[0]})(.*?)({fmtSng[1]}\\B{sngEnd})", {
|
f"(\\B{fmtSngO})(.*?)({fmtSngC}\\B{sngEnd})", {
|
||||||
0: self.hStyles["dialogue3"],
|
0: self.hStyles["dialogue3"],
|
||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, mainGui):
|
||||||
QTextBrowser.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
logger.debug("Initialising GuiDocViewer ...")
|
logger.debug("Initialising GuiDocViewer ...")
|
||||||
|
|
||||||
@@ -102,6 +102,13 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
self.docHeader.setTitleFromHandle(self._docHandle)
|
self.docHeader.setTitleFromHandle(self._docHandle)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def updateTheme(self):
|
||||||
|
"""Update theme elements.
|
||||||
|
"""
|
||||||
|
self.docHeader.updateTheme()
|
||||||
|
self.docFooter.updateTheme()
|
||||||
|
return
|
||||||
|
|
||||||
def initViewer(self):
|
def initViewer(self):
|
||||||
"""Set editor settings from main config.
|
"""Set editor settings from main config.
|
||||||
"""
|
"""
|
||||||
@@ -150,10 +157,7 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||||
|
|
||||||
# Refresh the tab stops
|
# Refresh the tab stops
|
||||||
if self.mainConf.verQtValue >= 51000:
|
self.setTabStopDistance(self.mainConf.getTabWidth())
|
||||||
self.setTabStopDistance(self.mainConf.getTabWidth())
|
|
||||||
else:
|
|
||||||
self.setTabStopWidth(self.mainConf.getTabWidth())
|
|
||||||
|
|
||||||
# If we have a document open, we should reload it in case the font changed
|
# If we have a document open, we should reload it in case the font changed
|
||||||
if self._docHandle is not None:
|
if self._docHandle is not None:
|
||||||
@@ -193,10 +197,7 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Refresh the tab stops
|
# Refresh the tab stops
|
||||||
if self.mainConf.verQtValue >= 51000:
|
self.setTabStopDistance(self.mainConf.getTabWidth())
|
||||||
self.setTabStopDistance(self.mainConf.getTabWidth())
|
|
||||||
else:
|
|
||||||
self.setTabStopWidth(self.mainConf.getTabWidth())
|
|
||||||
|
|
||||||
# Must be before setHtml
|
# Must be before setHtml
|
||||||
if updateHistory:
|
if updateHistory:
|
||||||
@@ -216,7 +217,7 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
self.verticalScrollBar().setValue(sPos)
|
self.verticalScrollBar().setValue(sPos)
|
||||||
|
|
||||||
self._docHandle = tHandle
|
self._docHandle = tHandle
|
||||||
self.theProject.setLastViewed(tHandle)
|
self.theProject._data.setLastHandle(tHandle, "viewer")
|
||||||
self.docHeader.setTitleFromHandle(self._docHandle)
|
self.docHeader.setTitleFromHandle(self._docHandle)
|
||||||
self.updateDocMargins()
|
self.updateDocMargins()
|
||||||
|
|
||||||
@@ -247,7 +248,7 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
"""Wrapper function for various document actions on the current
|
"""Wrapper function for various document actions on the current
|
||||||
document.
|
document.
|
||||||
"""
|
"""
|
||||||
logger.verbose("Requesting action: '%s'", theAction.name)
|
logger.debug("Requesting action: '%s'", theAction.name)
|
||||||
if self._docHandle is None:
|
if self._docHandle is None:
|
||||||
logger.error("No document open")
|
logger.error("No document open")
|
||||||
return False
|
return False
|
||||||
@@ -270,7 +271,7 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
if not isinstance(tAnchor, str):
|
if not isinstance(tAnchor, str):
|
||||||
return False
|
return False
|
||||||
if tAnchor.startswith("#"):
|
if tAnchor.startswith("#"):
|
||||||
logger.verbose("Moving to anchor '%s'", tAnchor)
|
logger.debug("Moving to anchor '%s'", tAnchor)
|
||||||
self.setSource(QUrl(tAnchor))
|
self.setSource(QUrl(tAnchor))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -356,7 +357,7 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
theBlock = self.document().findBlockByLineNumber(theLine)
|
theBlock = self.document().findBlockByLineNumber(theLine)
|
||||||
if theBlock:
|
if theBlock:
|
||||||
self.setCursorPosition(theBlock.position())
|
self.setCursorPosition(theBlock.position())
|
||||||
logger.verbose("Cursor moved to line %d", theLine)
|
logger.debug("Cursor moved to line %d", theLine)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def setScrollPosition(self, thePos):
|
def setScrollPosition(self, thePos):
|
||||||
@@ -402,7 +403,7 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
"""Process a clicked link internally in the document.
|
"""Process a clicked link internally in the document.
|
||||||
"""
|
"""
|
||||||
theLink = theURL.url()
|
theLink = theURL.url()
|
||||||
logger.verbose("Clicked link: '%s'", theLink)
|
logger.debug("Clicked link: '%s'", theLink)
|
||||||
if len(theLink) > 0:
|
if len(theLink) > 0:
|
||||||
theBits = theLink.split("=")
|
theBits = theLink.split("=")
|
||||||
if len(theBits) == 2:
|
if len(theBits) == 2:
|
||||||
@@ -461,7 +462,7 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
has its margins adjusted according to user preferences.
|
has its margins adjusted according to user preferences.
|
||||||
"""
|
"""
|
||||||
self.updateDocMargins()
|
self.updateDocMargins()
|
||||||
QTextBrowser.resizeEvent(self, theEvent)
|
super().resizeEvent(theEvent)
|
||||||
return
|
return
|
||||||
|
|
||||||
def mouseReleaseEvent(self, theEvent):
|
def mouseReleaseEvent(self, theEvent):
|
||||||
@@ -472,7 +473,7 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
elif theEvent.button() == Qt.ForwardButton:
|
elif theEvent.button() == Qt.ForwardButton:
|
||||||
self.navForward()
|
self.navForward()
|
||||||
else:
|
else:
|
||||||
QTextBrowser.mouseReleaseEvent(self, theEvent)
|
super().mouseReleaseEvent(theEvent)
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -568,7 +569,7 @@ class GuiDocViewer(QTextBrowser):
|
|||||||
# END Class GuiDocViewer
|
# END Class GuiDocViewer
|
||||||
|
|
||||||
|
|
||||||
class GuiDocViewHistory():
|
class GuiDocViewHistory:
|
||||||
|
|
||||||
def __init__(self, docViewer):
|
def __init__(self, docViewer):
|
||||||
|
|
||||||
@@ -584,7 +585,7 @@ class GuiDocViewHistory():
|
|||||||
def clear(self):
|
def clear(self):
|
||||||
"""Clear the view history.
|
"""Clear the view history.
|
||||||
"""
|
"""
|
||||||
logger.verbose("View history cleared")
|
logger.debug("View history cleared")
|
||||||
self._navHistory = []
|
self._navHistory = []
|
||||||
self._posHistory = []
|
self._posHistory = []
|
||||||
self._currPos = -1
|
self._currPos = -1
|
||||||
@@ -598,7 +599,7 @@ class GuiDocViewHistory():
|
|||||||
"""
|
"""
|
||||||
if self._currPos >= 0 and self._currPos < len(self._navHistory):
|
if self._currPos >= 0 and self._currPos < len(self._navHistory):
|
||||||
if tHandle == self._navHistory[self._currPos]:
|
if tHandle == self._navHistory[self._currPos]:
|
||||||
logger.verbose("Not updating view hsitory")
|
logger.debug("Not updating view hsitory")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self._truncateHistory(self._currPos)
|
self._truncateHistory(self._currPos)
|
||||||
@@ -613,7 +614,7 @@ class GuiDocViewHistory():
|
|||||||
|
|
||||||
self._dumpHistory()
|
self._dumpHistory()
|
||||||
|
|
||||||
logger.verbose("Added '%s' to view history", tHandle)
|
logger.debug("Added '%s' to view history", tHandle)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -622,7 +623,7 @@ class GuiDocViewHistory():
|
|||||||
"""
|
"""
|
||||||
newPos = self._currPos + 1
|
newPos = self._currPos + 1
|
||||||
if newPos < len(self._navHistory):
|
if newPos < len(self._navHistory):
|
||||||
logger.verbose("Move forward in view history")
|
logger.debug("Move forward in view history")
|
||||||
self._prevPos = self._currPos
|
self._prevPos = self._currPos
|
||||||
self._updateScrollBar()
|
self._updateScrollBar()
|
||||||
|
|
||||||
@@ -640,7 +641,7 @@ class GuiDocViewHistory():
|
|||||||
"""
|
"""
|
||||||
newPos = self._currPos - 1
|
newPos = self._currPos - 1
|
||||||
if newPos >= 0:
|
if newPos >= 0:
|
||||||
logger.verbose("Move backward in view history")
|
logger.debug("Move backward in view history")
|
||||||
self._prevPos = self._currPos
|
self._prevPos = self._currPos
|
||||||
self._updateScrollBar()
|
self._updateScrollBar()
|
||||||
|
|
||||||
@@ -686,11 +687,11 @@ class GuiDocViewHistory():
|
|||||||
|
|
||||||
def _dumpHistory(self):
|
def _dumpHistory(self):
|
||||||
"""Debug function to dump history to the logger. Since it is a
|
"""Debug function to dump history to the logger. Since it is a
|
||||||
for loop, it is skipped entirely if log level isn't VERBOSE.
|
for loop, it is skipped entirely if log level isn't DEBUG.
|
||||||
"""
|
"""
|
||||||
if logger.getEffectiveLevel() < logging.DEBUG:
|
if logger.getEffectiveLevel() == logging.DEBUG:
|
||||||
for i, (h, p) in enumerate(zip(self._navHistory, self._posHistory)):
|
for i, (h, p) in enumerate(zip(self._navHistory, self._posHistory)):
|
||||||
logger.verbose(
|
logger.debug(
|
||||||
"History %02d: %s %13s [x:%d]" % (
|
"History %02d: %s %13s [x:%d]" % (
|
||||||
i + 1, ">" if i == self._currPos else " ", h, p
|
i + 1, ">" if i == self._currPos else " ", h, p
|
||||||
)
|
)
|
||||||
@@ -708,7 +709,7 @@ class GuiDocViewHistory():
|
|||||||
class GuiDocViewHeader(QWidget):
|
class GuiDocViewHeader(QWidget):
|
||||||
|
|
||||||
def __init__(self, docViewer):
|
def __init__(self, docViewer):
|
||||||
QWidget.__init__(self, docViewer)
|
super().__init__(parent=docViewer)
|
||||||
|
|
||||||
logger.debug("Initialising GuiDocViewHeader ...")
|
logger.debug("Initialising GuiDocViewHeader ...")
|
||||||
|
|
||||||
@@ -741,51 +742,38 @@ class GuiDocViewHeader(QWidget):
|
|||||||
lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize)
|
lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize)
|
||||||
self.theTitle.setFont(lblFont)
|
self.theTitle.setFont(lblFont)
|
||||||
|
|
||||||
buttonStyle = (
|
|
||||||
"QToolButton {{border: none; background: transparent;}} "
|
|
||||||
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
|
|
||||||
).format(*self.mainTheme.colText)
|
|
||||||
|
|
||||||
# Buttons
|
# Buttons
|
||||||
self.backButton = QToolButton(self)
|
self.backButton = QToolButton(self)
|
||||||
self.backButton.setIcon(self.mainTheme.getIcon("backward"))
|
|
||||||
self.backButton.setContentsMargins(0, 0, 0, 0)
|
self.backButton.setContentsMargins(0, 0, 0, 0)
|
||||||
self.backButton.setIconSize(QSize(fPx, fPx))
|
self.backButton.setIconSize(QSize(fPx, fPx))
|
||||||
self.backButton.setFixedSize(fPx, fPx)
|
self.backButton.setFixedSize(fPx, fPx)
|
||||||
self.backButton.setStyleSheet(buttonStyle)
|
|
||||||
self.backButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
self.backButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||||
self.backButton.setVisible(False)
|
self.backButton.setVisible(False)
|
||||||
self.backButton.setToolTip(self.tr("Go backward"))
|
self.backButton.setToolTip(self.tr("Go backward"))
|
||||||
self.backButton.clicked.connect(self.docViewer.navBackward)
|
self.backButton.clicked.connect(self.docViewer.navBackward)
|
||||||
|
|
||||||
self.forwardButton = QToolButton(self)
|
self.forwardButton = QToolButton(self)
|
||||||
self.forwardButton.setIcon(self.mainTheme.getIcon("forward"))
|
|
||||||
self.forwardButton.setContentsMargins(0, 0, 0, 0)
|
self.forwardButton.setContentsMargins(0, 0, 0, 0)
|
||||||
self.forwardButton.setIconSize(QSize(fPx, fPx))
|
self.forwardButton.setIconSize(QSize(fPx, fPx))
|
||||||
self.forwardButton.setFixedSize(fPx, fPx)
|
self.forwardButton.setFixedSize(fPx, fPx)
|
||||||
self.forwardButton.setStyleSheet(buttonStyle)
|
|
||||||
self.forwardButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
self.forwardButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||||
self.forwardButton.setVisible(False)
|
self.forwardButton.setVisible(False)
|
||||||
self.forwardButton.setToolTip(self.tr("Go forward"))
|
self.forwardButton.setToolTip(self.tr("Go forward"))
|
||||||
self.forwardButton.clicked.connect(self.docViewer.navForward)
|
self.forwardButton.clicked.connect(self.docViewer.navForward)
|
||||||
|
|
||||||
self.refreshButton = QToolButton(self)
|
self.refreshButton = QToolButton(self)
|
||||||
self.refreshButton.setIcon(self.mainTheme.getIcon("refresh"))
|
|
||||||
self.refreshButton.setContentsMargins(0, 0, 0, 0)
|
self.refreshButton.setContentsMargins(0, 0, 0, 0)
|
||||||
self.refreshButton.setIconSize(QSize(fPx, fPx))
|
self.refreshButton.setIconSize(QSize(fPx, fPx))
|
||||||
self.refreshButton.setFixedSize(fPx, fPx)
|
self.refreshButton.setFixedSize(fPx, fPx)
|
||||||
self.refreshButton.setStyleSheet(buttonStyle)
|
|
||||||
self.refreshButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
self.refreshButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||||
self.refreshButton.setVisible(False)
|
self.refreshButton.setVisible(False)
|
||||||
self.refreshButton.setToolTip(self.tr("Reload the document"))
|
self.refreshButton.setToolTip(self.tr("Reload the document"))
|
||||||
self.refreshButton.clicked.connect(self._refreshDocument)
|
self.refreshButton.clicked.connect(self._refreshDocument)
|
||||||
|
|
||||||
self.closeButton = QToolButton(self)
|
self.closeButton = QToolButton(self)
|
||||||
self.closeButton.setIcon(self.mainTheme.getIcon("close"))
|
|
||||||
self.closeButton.setContentsMargins(0, 0, 0, 0)
|
self.closeButton.setContentsMargins(0, 0, 0, 0)
|
||||||
self.closeButton.setIconSize(QSize(fPx, fPx))
|
self.closeButton.setIconSize(QSize(fPx, fPx))
|
||||||
self.closeButton.setFixedSize(fPx, fPx)
|
self.closeButton.setFixedSize(fPx, fPx)
|
||||||
self.closeButton.setStyleSheet(buttonStyle)
|
|
||||||
self.closeButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
self.closeButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||||
self.closeButton.setVisible(False)
|
self.closeButton.setVisible(False)
|
||||||
self.closeButton.setToolTip(self.tr("Close the document"))
|
self.closeButton.setToolTip(self.tr("Close the document"))
|
||||||
@@ -809,7 +797,7 @@ class GuiDocViewHeader(QWidget):
|
|||||||
self.setMinimumHeight(fPx + 2*cM)
|
self.setMinimumHeight(fPx + 2*cM)
|
||||||
|
|
||||||
# Fix the Colours
|
# Fix the Colours
|
||||||
self.matchColours()
|
self.updateTheme()
|
||||||
|
|
||||||
logger.debug("GuiDocViewHeader initialisation complete")
|
logger.debug("GuiDocViewHeader initialisation complete")
|
||||||
|
|
||||||
@@ -819,6 +807,28 @@ class GuiDocViewHeader(QWidget):
|
|||||||
# Methods
|
# Methods
|
||||||
##
|
##
|
||||||
|
|
||||||
|
def updateTheme(self):
|
||||||
|
"""Update theme elements.
|
||||||
|
"""
|
||||||
|
self.backButton.setIcon(self.mainTheme.getIcon("backward"))
|
||||||
|
self.forwardButton.setIcon(self.mainTheme.getIcon("forward"))
|
||||||
|
self.refreshButton.setIcon(self.mainTheme.getIcon("refresh"))
|
||||||
|
self.closeButton.setIcon(self.mainTheme.getIcon("close"))
|
||||||
|
|
||||||
|
buttonStyle = (
|
||||||
|
"QToolButton {{border: none; background: transparent;}} "
|
||||||
|
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
|
||||||
|
).format(*self.mainTheme.colText)
|
||||||
|
|
||||||
|
self.backButton.setStyleSheet(buttonStyle)
|
||||||
|
self.forwardButton.setStyleSheet(buttonStyle)
|
||||||
|
self.refreshButton.setStyleSheet(buttonStyle)
|
||||||
|
self.closeButton.setStyleSheet(buttonStyle)
|
||||||
|
|
||||||
|
self.matchColours()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
def matchColours(self):
|
def matchColours(self):
|
||||||
"""Update the colours of the widget to match those of the syntax
|
"""Update the colours of the widget to match those of the syntax
|
||||||
theme rather than the main GUI.
|
theme rather than the main GUI.
|
||||||
@@ -879,12 +889,14 @@ class GuiDocViewHeader(QWidget):
|
|||||||
# Slots
|
# Slots
|
||||||
##
|
##
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
def _closeDocument(self):
|
def _closeDocument(self):
|
||||||
"""Trigger the close editor/viewer on the main window.
|
"""Trigger the close editor/viewer on the main window.
|
||||||
"""
|
"""
|
||||||
self.mainGui.closeDocViewer()
|
self.mainGui.closeDocViewer()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
def _refreshDocument(self):
|
def _refreshDocument(self):
|
||||||
"""Reload the content of the document.
|
"""Reload the content of the document.
|
||||||
"""
|
"""
|
||||||
@@ -915,7 +927,7 @@ class GuiDocViewHeader(QWidget):
|
|||||||
class GuiDocViewFooter(QWidget):
|
class GuiDocViewFooter(QWidget):
|
||||||
|
|
||||||
def __init__(self, docViewer):
|
def __init__(self, docViewer):
|
||||||
QWidget.__init__(self, docViewer)
|
super().__init__(parent=docViewer)
|
||||||
|
|
||||||
logger.debug("Initialising GuiDocViewFooter ...")
|
logger.debug("Initialising GuiDocViewFooter ...")
|
||||||
|
|
||||||
@@ -932,33 +944,13 @@ class GuiDocViewFooter(QWidget):
|
|||||||
bSp = self.mainConf.pxInt(2)
|
bSp = self.mainConf.pxInt(2)
|
||||||
hSp = self.mainConf.pxInt(8)
|
hSp = self.mainConf.pxInt(8)
|
||||||
|
|
||||||
# Icons
|
|
||||||
stickyOn = self.mainTheme.getPixmap("sticky-on", (fPx, fPx))
|
|
||||||
stickyOff = self.mainTheme.getPixmap("sticky-off", (fPx, fPx))
|
|
||||||
stickyIcon = QIcon()
|
|
||||||
stickyIcon.addPixmap(stickyOn, QIcon.Normal, QIcon.On)
|
|
||||||
stickyIcon.addPixmap(stickyOff, QIcon.Normal, QIcon.Off)
|
|
||||||
|
|
||||||
bulletOn = self.mainTheme.getPixmap("bullet-on", (fPx, fPx))
|
|
||||||
bulletOff = self.mainTheme.getPixmap("bullet-off", (fPx, fPx))
|
|
||||||
bulletIcon = QIcon()
|
|
||||||
bulletIcon.addPixmap(bulletOn, QIcon.Normal, QIcon.On)
|
|
||||||
bulletIcon.addPixmap(bulletOff, QIcon.Normal, QIcon.Off)
|
|
||||||
|
|
||||||
# Main Widget Settings
|
# Main Widget Settings
|
||||||
self.setContentsMargins(0, 0, 0, 0)
|
self.setContentsMargins(0, 0, 0, 0)
|
||||||
self.setAutoFillBackground(True)
|
self.setAutoFillBackground(True)
|
||||||
|
|
||||||
buttonStyle = (
|
|
||||||
"QToolButton {{border: none; background: transparent;}} "
|
|
||||||
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
|
|
||||||
).format(*self.mainTheme.colText)
|
|
||||||
|
|
||||||
# Show/Hide Details
|
# Show/Hide Details
|
||||||
self.showHide = QToolButton(self)
|
self.showHide = QToolButton(self)
|
||||||
self.showHide.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
self.showHide.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||||
self.showHide.setStyleSheet(buttonStyle)
|
|
||||||
self.showHide.setIcon(self.mainTheme.getIcon("reference"))
|
|
||||||
self.showHide.setIconSize(QSize(fPx, fPx))
|
self.showHide.setIconSize(QSize(fPx, fPx))
|
||||||
self.showHide.setFixedSize(QSize(fPx, fPx))
|
self.showHide.setFixedSize(QSize(fPx, fPx))
|
||||||
self.showHide.clicked.connect(self._doShowHide)
|
self.showHide.clicked.connect(self._doShowHide)
|
||||||
@@ -968,8 +960,6 @@ class GuiDocViewFooter(QWidget):
|
|||||||
self.stickyRefs = QToolButton(self)
|
self.stickyRefs = QToolButton(self)
|
||||||
self.stickyRefs.setCheckable(True)
|
self.stickyRefs.setCheckable(True)
|
||||||
self.stickyRefs.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
self.stickyRefs.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||||
self.stickyRefs.setStyleSheet(buttonStyle)
|
|
||||||
self.stickyRefs.setIcon(stickyIcon)
|
|
||||||
self.stickyRefs.setIconSize(QSize(fPx, fPx))
|
self.stickyRefs.setIconSize(QSize(fPx, fPx))
|
||||||
self.stickyRefs.setFixedSize(QSize(fPx, fPx))
|
self.stickyRefs.setFixedSize(QSize(fPx, fPx))
|
||||||
self.stickyRefs.toggled.connect(self._doToggleSticky)
|
self.stickyRefs.toggled.connect(self._doToggleSticky)
|
||||||
@@ -982,8 +972,6 @@ class GuiDocViewFooter(QWidget):
|
|||||||
self.showComments.setCheckable(True)
|
self.showComments.setCheckable(True)
|
||||||
self.showComments.setChecked(self.mainConf.viewComments)
|
self.showComments.setChecked(self.mainConf.viewComments)
|
||||||
self.showComments.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
self.showComments.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||||
self.showComments.setStyleSheet(buttonStyle)
|
|
||||||
self.showComments.setIcon(bulletIcon)
|
|
||||||
self.showComments.setIconSize(QSize(fPx, fPx))
|
self.showComments.setIconSize(QSize(fPx, fPx))
|
||||||
self.showComments.setFixedSize(QSize(fPx, fPx))
|
self.showComments.setFixedSize(QSize(fPx, fPx))
|
||||||
self.showComments.toggled.connect(self._doToggleComments)
|
self.showComments.toggled.connect(self._doToggleComments)
|
||||||
@@ -994,8 +982,6 @@ class GuiDocViewFooter(QWidget):
|
|||||||
self.showSynopsis.setCheckable(True)
|
self.showSynopsis.setCheckable(True)
|
||||||
self.showSynopsis.setChecked(self.mainConf.viewSynopsis)
|
self.showSynopsis.setChecked(self.mainConf.viewSynopsis)
|
||||||
self.showSynopsis.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
self.showSynopsis.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
||||||
self.showSynopsis.setStyleSheet(buttonStyle)
|
|
||||||
self.showSynopsis.setIcon(bulletIcon)
|
|
||||||
self.showSynopsis.setIconSize(QSize(fPx, fPx))
|
self.showSynopsis.setIconSize(QSize(fPx, fPx))
|
||||||
self.showSynopsis.setFixedSize(QSize(fPx, fPx))
|
self.showSynopsis.setFixedSize(QSize(fPx, fPx))
|
||||||
self.showSynopsis.toggled.connect(self._doToggleSynopsis)
|
self.showSynopsis.toggled.connect(self._doToggleSynopsis)
|
||||||
@@ -1069,7 +1055,7 @@ class GuiDocViewFooter(QWidget):
|
|||||||
self.setMinimumHeight(fPx + 2*cM)
|
self.setMinimumHeight(fPx + 2*cM)
|
||||||
|
|
||||||
# Fix the Colours
|
# Fix the Colours
|
||||||
self.matchColours()
|
self.updateTheme()
|
||||||
|
|
||||||
logger.debug("GuiDocViewFooter initialisation complete")
|
logger.debug("GuiDocViewFooter initialisation complete")
|
||||||
|
|
||||||
@@ -1079,6 +1065,46 @@ class GuiDocViewFooter(QWidget):
|
|||||||
# Methods
|
# Methods
|
||||||
##
|
##
|
||||||
|
|
||||||
|
def updateTheme(self):
|
||||||
|
"""Update theme elements.
|
||||||
|
"""
|
||||||
|
# Icons
|
||||||
|
|
||||||
|
fPx = int(0.9*self.mainTheme.fontPixelSize)
|
||||||
|
|
||||||
|
stickyOn = self.mainTheme.getPixmap("sticky-on", (fPx, fPx))
|
||||||
|
stickyOff = self.mainTheme.getPixmap("sticky-off", (fPx, fPx))
|
||||||
|
stickyIcon = QIcon()
|
||||||
|
stickyIcon.addPixmap(stickyOn, QIcon.Normal, QIcon.On)
|
||||||
|
stickyIcon.addPixmap(stickyOff, QIcon.Normal, QIcon.Off)
|
||||||
|
|
||||||
|
bulletOn = self.mainTheme.getPixmap("bullet-on", (fPx, fPx))
|
||||||
|
bulletOff = self.mainTheme.getPixmap("bullet-off", (fPx, fPx))
|
||||||
|
bulletIcon = QIcon()
|
||||||
|
bulletIcon.addPixmap(bulletOn, QIcon.Normal, QIcon.On)
|
||||||
|
bulletIcon.addPixmap(bulletOff, QIcon.Normal, QIcon.Off)
|
||||||
|
|
||||||
|
self.showHide.setIcon(self.mainTheme.getIcon("reference"))
|
||||||
|
self.stickyRefs.setIcon(stickyIcon)
|
||||||
|
self.showComments.setIcon(bulletIcon)
|
||||||
|
self.showSynopsis.setIcon(bulletIcon)
|
||||||
|
|
||||||
|
# StyleSheets
|
||||||
|
|
||||||
|
buttonStyle = (
|
||||||
|
"QToolButton {{border: none; background: transparent;}} "
|
||||||
|
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
|
||||||
|
).format(*self.mainTheme.colText)
|
||||||
|
|
||||||
|
self.showHide.setStyleSheet(buttonStyle)
|
||||||
|
self.stickyRefs.setStyleSheet(buttonStyle)
|
||||||
|
self.showComments.setStyleSheet(buttonStyle)
|
||||||
|
self.showSynopsis.setStyleSheet(buttonStyle)
|
||||||
|
|
||||||
|
self.matchColours()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
def matchColours(self):
|
def matchColours(self):
|
||||||
"""Update the colours of the widget to match those of the syntax
|
"""Update the colours of the widget to match those of the syntax
|
||||||
theme rather than the main GUI.
|
theme rather than the main GUI.
|
||||||
@@ -1100,6 +1126,7 @@ class GuiDocViewFooter(QWidget):
|
|||||||
# Slots
|
# Slots
|
||||||
##
|
##
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
def _doShowHide(self):
|
def _doShowHide(self):
|
||||||
"""Toggle the expand/collapse of the panel.
|
"""Toggle the expand/collapse of the panel.
|
||||||
"""
|
"""
|
||||||
@@ -1107,26 +1134,29 @@ class GuiDocViewFooter(QWidget):
|
|||||||
self.viewMeta.setVisible(not isVisible)
|
self.viewMeta.setVisible(not isVisible)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@pyqtSlot(bool)
|
||||||
def _doToggleSticky(self, theState):
|
def _doToggleSticky(self, theState):
|
||||||
"""Toggle the sticky flag for the reference panel.
|
"""Toggle the sticky flag for the reference panel.
|
||||||
"""
|
"""
|
||||||
logger.verbose("Reference sticky is %s", str(theState))
|
logger.debug("Reference sticky is %s", str(theState))
|
||||||
self.docViewer.stickyRef = theState
|
self.docViewer.stickyRef = theState
|
||||||
if not theState and self.docViewer.docHandle() is not None:
|
if not theState and self.docViewer.docHandle() is not None:
|
||||||
self.viewMeta.refreshReferences(self.docViewer.docHandle())
|
self.viewMeta.refreshReferences(self.docViewer.docHandle())
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@pyqtSlot(bool)
|
||||||
def _doToggleComments(self, theState):
|
def _doToggleComments(self, theState):
|
||||||
"""Toggle the view comment button and reload the document.
|
"""Toggle the view comment button and reload the document.
|
||||||
"""
|
"""
|
||||||
self.mainConf.setViewComments(theState)
|
self.mainConf.viewComments = theState
|
||||||
self.docViewer.reloadText()
|
self.docViewer.reloadText()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@pyqtSlot(bool)
|
||||||
def _doToggleSynopsis(self, theState):
|
def _doToggleSynopsis(self, theState):
|
||||||
"""Toggle the view synopsis button and reload the document.
|
"""Toggle the view synopsis button and reload the document.
|
||||||
"""
|
"""
|
||||||
self.mainConf.setViewSynopsis(theState)
|
self.mainConf.viewSynopsis = theState
|
||||||
self.docViewer.reloadText()
|
self.docViewer.reloadText()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1141,7 +1171,7 @@ class GuiDocViewFooter(QWidget):
|
|||||||
class GuiDocViewDetails(QScrollArea):
|
class GuiDocViewDetails(QScrollArea):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, mainGui):
|
||||||
QScrollArea.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
logger.debug("Initialising GuiDocViewDetails ...")
|
logger.debug("Initialising GuiDocViewDetails ...")
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
@@ -1205,7 +1235,7 @@ class GuiDocViewDetails(QScrollArea):
|
|||||||
"""Capture the link-click and forward it to the document viewer
|
"""Capture the link-click and forward it to the document viewer
|
||||||
class for handling.
|
class for handling.
|
||||||
"""
|
"""
|
||||||
logger.verbose("Clicked link: '%s'", theLink)
|
logger.debug("Clicked link: '%s'", theLink)
|
||||||
if len(theLink) == 21:
|
if len(theLink) == 21:
|
||||||
tHandle = theLink[:13]
|
tHandle = theLink[:13]
|
||||||
tAnchor = theLink[13:]
|
tAnchor = theLink[13:]
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ from PyQt5.QtCore import Qt, pyqtSlot
|
|||||||
from PyQt5.QtGui import QFont, QPixmap
|
from PyQt5.QtGui import QFont, QPixmap
|
||||||
from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel
|
from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel
|
||||||
|
|
||||||
from novelwriter.enum import nwItemType
|
|
||||||
from novelwriter.constants import trConst, nwLabels
|
from novelwriter.constants import trConst, nwLabels
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -39,7 +38,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class GuiItemDetails(QWidget):
|
class GuiItemDetails(QWidget):
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, mainGui):
|
||||||
QWidget.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
logger.debug("Initialising GuiItemDetails ...")
|
logger.debug("Initialising GuiItemDetails ...")
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
@@ -54,12 +53,8 @@ class GuiItemDetails(QWidget):
|
|||||||
hSp = self.mainConf.pxInt(6)
|
hSp = self.mainConf.pxInt(6)
|
||||||
vSp = self.mainConf.pxInt(1)
|
vSp = self.mainConf.pxInt(1)
|
||||||
mPx = self.mainConf.pxInt(6)
|
mPx = self.mainConf.pxInt(6)
|
||||||
iPx = self.mainTheme.baseIconSize
|
|
||||||
fPt = self.mainTheme.fontPointSize
|
fPt = self.mainTheme.fontPointSize
|
||||||
|
|
||||||
self._expCheck = self.mainTheme.getPixmap("check", (iPx, iPx))
|
|
||||||
self._expCross = self.mainTheme.getPixmap("cross", (iPx, iPx))
|
|
||||||
|
|
||||||
fntLabel = QFont()
|
fntLabel = QFont()
|
||||||
fntLabel.setBold(True)
|
fntLabel.setBold(True)
|
||||||
fntLabel.setPointSizeF(0.9*fPt)
|
fntLabel.setPointSizeF(0.9*fPt)
|
||||||
@@ -180,6 +175,8 @@ class GuiItemDetails(QWidget):
|
|||||||
|
|
||||||
self.setLayout(self.mainBox)
|
self.setLayout(self.mainBox)
|
||||||
|
|
||||||
|
self.updateTheme()
|
||||||
|
|
||||||
# Make sure the columns for flags and counts don't resize too often
|
# Make sure the columns for flags and counts don't resize too often
|
||||||
flagWidth = self.mainTheme.getTextWidth("Mm", fntValue)
|
flagWidth = self.mainTheme.getTextWidth("Mm", fntValue)
|
||||||
countWidth = self.mainTheme.getTextWidth("99,999", fntValue)
|
countWidth = self.mainTheme.getTextWidth("99,999", fntValue)
|
||||||
@@ -220,6 +217,12 @@ class GuiItemDetails(QWidget):
|
|||||||
"""
|
"""
|
||||||
self.updateViewBox(self._itemHandle)
|
self.updateViewBox(self._itemHandle)
|
||||||
|
|
||||||
|
def updateTheme(self):
|
||||||
|
"""Update theme elements.
|
||||||
|
"""
|
||||||
|
self.updateViewBox(self._itemHandle)
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Public Slots
|
# Public Slots
|
||||||
##
|
##
|
||||||
@@ -247,13 +250,13 @@ class GuiItemDetails(QWidget):
|
|||||||
if len(theLabel) > 100:
|
if len(theLabel) > 100:
|
||||||
theLabel = theLabel[:96].rstrip()+" ..."
|
theLabel = theLabel[:96].rstrip()+" ..."
|
||||||
|
|
||||||
if nwItem.itemType == nwItemType.FILE:
|
if nwItem.isFileType():
|
||||||
if nwItem.isExported:
|
if nwItem.isActive:
|
||||||
self.labelIcon.setPixmap(self._expCheck)
|
self.labelIcon.setPixmap(self.mainTheme.getPixmap("checked", (iPx, iPx)))
|
||||||
else:
|
else:
|
||||||
self.labelIcon.setPixmap(self._expCross)
|
self.labelIcon.setPixmap(self.mainTheme.getPixmap("unchecked", (iPx, iPx)))
|
||||||
else:
|
else:
|
||||||
self.labelIcon.setPixmap(QPixmap(1, 1))
|
self.labelIcon.setPixmap(self.mainTheme.getPixmap("noncheckable", (iPx, iPx)))
|
||||||
|
|
||||||
self.labelData.setText(theLabel)
|
self.labelData.setText(theLabel)
|
||||||
|
|
||||||
@@ -274,17 +277,16 @@ class GuiItemDetails(QWidget):
|
|||||||
# Layout
|
# Layout
|
||||||
# ======
|
# ======
|
||||||
|
|
||||||
hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
|
|
||||||
usageIcon = self.mainTheme.getItemIcon(
|
usageIcon = self.mainTheme.getItemIcon(
|
||||||
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
|
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading
|
||||||
)
|
)
|
||||||
self.usageIcon.setPixmap(usageIcon.pixmap(iPx, iPx))
|
self.usageIcon.setPixmap(usageIcon.pixmap(iPx, iPx))
|
||||||
self.usageData.setText(nwItem.describeMe(hLevel))
|
self.usageData.setText(nwItem.describeMe())
|
||||||
|
|
||||||
# Counts
|
# Counts
|
||||||
# ======
|
# ======
|
||||||
|
|
||||||
if nwItem.itemType == nwItemType.FILE:
|
if nwItem.isFileType():
|
||||||
self.cCountData.setText(f"{nwItem.charCount:n}")
|
self.cCountData.setText(f"{nwItem.charCount:n}")
|
||||||
self.wCountData.setText(f"{nwItem.wordCount:n}")
|
self.wCountData.setText(f"{nwItem.wordCount:n}")
|
||||||
self.pCountData.setText(f"{nwItem.paraCount:n}")
|
self.pCountData.setText(f"{nwItem.paraCount:n}")
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
import logging
|
import logging
|
||||||
import novelwriter
|
import novelwriter
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
from urllib.request import pathname2url
|
from urllib.request import pathname2url
|
||||||
|
|
||||||
@@ -40,9 +41,13 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class GuiMainMenu(QMenuBar):
|
class GuiMainMenu(QMenuBar):
|
||||||
|
"""The GUI main menu. All menu actions are defined here with the
|
||||||
|
main menu as the owner. Each widget that need them elsewhere need to
|
||||||
|
add them from this class.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, mainGui):
|
||||||
QMenuBar.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
logger.debug("Initialising GuiMainMenu ...")
|
logger.debug("Initialising GuiMainMenu ...")
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
@@ -79,12 +84,6 @@ class GuiMainMenu(QMenuBar):
|
|||||||
self.aSpellCheck.setChecked(theMode)
|
self.aSpellCheck.setChecked(theMode)
|
||||||
return
|
return
|
||||||
|
|
||||||
def setFocusMode(self, theMode):
|
|
||||||
"""Forward focus mode check state to its action.
|
|
||||||
"""
|
|
||||||
self.aFocusMode.setChecked(theMode)
|
|
||||||
return
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Slots
|
# Slots
|
||||||
##
|
##
|
||||||
@@ -106,10 +105,11 @@ class GuiMainMenu(QMenuBar):
|
|||||||
def _openUserManualFile(self):
|
def _openUserManualFile(self):
|
||||||
"""Open the documentation in PDF format.
|
"""Open the documentation in PDF format.
|
||||||
"""
|
"""
|
||||||
if self.mainConf.pdfDocs is None:
|
if isinstance(self.mainConf.pdfDocs, Path):
|
||||||
return False
|
QDesktopServices.openUrl(
|
||||||
QDesktopServices.openUrl(QUrl(urljoin("file:", pathname2url(self.mainConf.pdfDocs))))
|
QUrl(urljoin("file:", pathname2url(str(self.mainConf.pdfDocs))))
|
||||||
return True
|
)
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Menu Builders
|
# Menu Builders
|
||||||
@@ -164,14 +164,14 @@ class GuiMainMenu(QMenuBar):
|
|||||||
|
|
||||||
# Project > Edit
|
# Project > Edit
|
||||||
self.aEditItem = QAction(self.tr("Rename Item"), self)
|
self.aEditItem = QAction(self.tr("Rename Item"), self)
|
||||||
self.aEditItem.setShortcuts(["F2"])
|
self.aEditItem.setShortcut("F2")
|
||||||
self.aEditItem.triggered.connect(lambda: self.mainGui.editItemLabel(None))
|
self.aEditItem.triggered.connect(lambda: self.mainGui.editItemLabel(None))
|
||||||
self.projMenu.addAction(self.aEditItem)
|
self.projMenu.addAction(self.aEditItem)
|
||||||
|
|
||||||
# Project > Delete
|
# Project > Delete
|
||||||
self.aDeleteItem = QAction(self.tr("Delete Item"), self)
|
self.aDeleteItem = QAction(self.tr("Delete Item"), self)
|
||||||
self.aDeleteItem.setShortcut("Ctrl+Shift+Del")
|
self.aDeleteItem.setShortcut("Ctrl+Shift+Del")
|
||||||
self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.deleteItem(None))
|
self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.requestDeleteItem(None))
|
||||||
self.projMenu.addAction(self.aDeleteItem)
|
self.projMenu.addAction(self.aDeleteItem)
|
||||||
|
|
||||||
# Project > Empty Trash
|
# Project > Empty Trash
|
||||||
@@ -244,16 +244,6 @@ class GuiMainMenu(QMenuBar):
|
|||||||
self.aImportFile.triggered.connect(lambda: self.mainGui.importDocument())
|
self.aImportFile.triggered.connect(lambda: self.mainGui.importDocument())
|
||||||
self.docuMenu.addAction(self.aImportFile)
|
self.docuMenu.addAction(self.aImportFile)
|
||||||
|
|
||||||
# Document > Merge Documents
|
|
||||||
self.aMergeDocs = QAction(self.tr("Merge Folder to Document"), self)
|
|
||||||
self.aMergeDocs.triggered.connect(lambda: self.mainGui.mergeDocuments())
|
|
||||||
self.docuMenu.addAction(self.aMergeDocs)
|
|
||||||
|
|
||||||
# Document > Split Document
|
|
||||||
self.aSplitDoc = QAction(self.tr("Split Document to Folder"), self)
|
|
||||||
self.aSplitDoc.triggered.connect(lambda: self.mainGui.splitDocument())
|
|
||||||
self.docuMenu.addAction(self.aSplitDoc)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def _buildEditMenu(self):
|
def _buildEditMenu(self):
|
||||||
@@ -375,8 +365,6 @@ class GuiMainMenu(QMenuBar):
|
|||||||
# View > Focus Mode
|
# View > Focus Mode
|
||||||
self.aFocusMode = QAction(self.tr("Focus Mode"), self)
|
self.aFocusMode = QAction(self.tr("Focus Mode"), self)
|
||||||
self.aFocusMode.setShortcut("F8")
|
self.aFocusMode.setShortcut("F8")
|
||||||
self.aFocusMode.setCheckable(True)
|
|
||||||
self.aFocusMode.setChecked(self.mainGui.isFocusMode)
|
|
||||||
self.aFocusMode.triggered.connect(lambda: self.mainGui.toggleFocusMode())
|
self.aFocusMode.triggered.connect(lambda: self.mainGui.toggleFocusMode())
|
||||||
self.viewMenu.addAction(self.aFocusMode)
|
self.viewMenu.addAction(self.aFocusMode)
|
||||||
|
|
||||||
@@ -568,6 +556,15 @@ class GuiMainMenu(QMenuBar):
|
|||||||
)
|
)
|
||||||
self.mInsKeywords.addAction(self.mInsKWItems[keyWord][0])
|
self.mInsKeywords.addAction(self.mInsKWItems[keyWord][0])
|
||||||
|
|
||||||
|
# Insert > Special Comments
|
||||||
|
self.mInsComments = self.insertMenu.addMenu(self.tr("Special Comments"))
|
||||||
|
|
||||||
|
# Insert > Synopsis Comment
|
||||||
|
self.aInsSynopsis = QAction(self.tr("Synopsis Comment"), self)
|
||||||
|
self.aInsSynopsis.setShortcut("Ctrl+K, S")
|
||||||
|
self.aInsSynopsis.triggered.connect(lambda: self._docInsert(nwDocInsert.SYNOPSIS))
|
||||||
|
self.mInsComments.addAction(self.aInsSynopsis)
|
||||||
|
|
||||||
# Insert > Symbols
|
# Insert > Symbols
|
||||||
self.mInsBreaks = self.insertMenu.addMenu(self.tr("Page Break and Space"))
|
self.mInsBreaks = self.insertMenu.addMenu(self.tr("Page Break and Space"))
|
||||||
|
|
||||||
@@ -799,7 +796,7 @@ class GuiMainMenu(QMenuBar):
|
|||||||
# Tools > Check Spelling
|
# Tools > Check Spelling
|
||||||
self.aSpellCheck = QAction(self.tr("Check Spelling"), self)
|
self.aSpellCheck = QAction(self.tr("Check Spelling"), self)
|
||||||
self.aSpellCheck.setCheckable(True)
|
self.aSpellCheck.setCheckable(True)
|
||||||
self.aSpellCheck.setChecked(self.theProject.spellCheck)
|
self.aSpellCheck.setChecked(self.theProject.data.spellCheck)
|
||||||
self.aSpellCheck.triggered.connect(self._toggleSpellCheck) # triggered, not toggled!
|
self.aSpellCheck.triggered.connect(self._toggleSpellCheck) # triggered, not toggled!
|
||||||
self.aSpellCheck.setShortcut("Ctrl+F7")
|
self.aSpellCheck.setShortcut("Ctrl+F7")
|
||||||
self.toolsMenu.addAction(self.aSpellCheck)
|
self.toolsMenu.addAction(self.aSpellCheck)
|
||||||
@@ -829,7 +826,7 @@ class GuiMainMenu(QMenuBar):
|
|||||||
|
|
||||||
# Tools > Backup
|
# Tools > Backup
|
||||||
self.aBackupProject = QAction(self.tr("Backup Project"), self)
|
self.aBackupProject = QAction(self.tr("Backup Project"), self)
|
||||||
self.aBackupProject.triggered.connect(lambda: self.theProject.zipIt(True))
|
self.aBackupProject.triggered.connect(lambda: self.theProject.backupProject(True))
|
||||||
self.toolsMenu.addAction(self.aBackupProject)
|
self.toolsMenu.addAction(self.aBackupProject)
|
||||||
|
|
||||||
# Tools > Export Project
|
# Tools > Export Project
|
||||||
@@ -886,7 +883,7 @@ class GuiMainMenu(QMenuBar):
|
|||||||
self.helpMenu.addAction(self.aHelpDocs)
|
self.helpMenu.addAction(self.aHelpDocs)
|
||||||
|
|
||||||
# Help > User Manual (PDF)
|
# Help > User Manual (PDF)
|
||||||
if self.mainConf.pdfDocs is not None:
|
if isinstance(self.mainConf.pdfDocs, Path):
|
||||||
self.aPdfDocs = QAction(self.tr("User Manual (PDF)"), self)
|
self.aPdfDocs = QAction(self.tr("User Manual (PDF)"), self)
|
||||||
self.aPdfDocs.setShortcut("Shift+F1")
|
self.aPdfDocs.setShortcut("Shift+F1")
|
||||||
self.aPdfDocs.triggered.connect(self._openUserManualFile)
|
self.aPdfDocs.triggered.connect(self._openUserManualFile)
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ from PyQt5.QtGui import QPalette
|
|||||||
from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal
|
from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel,
|
QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel,
|
||||||
QMenu, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
QMenu, QSizePolicy, QToolButton, QToolTip, QTreeWidget, QTreeWidgetItem,
|
||||||
QWidget
|
QVBoxLayout, QWidget
|
||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter.enum import nwDocMode, nwItemClass
|
from novelwriter.enum import nwDocMode, nwItemClass, nwOutline
|
||||||
from novelwriter.common import checkInt
|
from novelwriter.common import checkInt
|
||||||
from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst
|
from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ class GuiNovelView(QWidget):
|
|||||||
openDocumentRequest = pyqtSignal(str, Enum, int, str)
|
openDocumentRequest = pyqtSignal(str, Enum, int, str)
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, mainGui):
|
||||||
QWidget.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
self.mainGui = mainGui
|
self.mainGui = mainGui
|
||||||
self.theProject = mainGui.theProject
|
self.theProject = mainGui.theProject
|
||||||
@@ -71,6 +71,7 @@ class GuiNovelView(QWidget):
|
|||||||
# Build GUI
|
# Build GUI
|
||||||
self.novelTree = GuiNovelTree(self)
|
self.novelTree = GuiNovelTree(self)
|
||||||
self.novelBar = GuiNovelToolBar(self)
|
self.novelBar = GuiNovelToolBar(self)
|
||||||
|
self.novelBar.setEnabled(False)
|
||||||
|
|
||||||
# Assemble
|
# Assemble
|
||||||
self.outerBox = QVBoxLayout()
|
self.outerBox = QVBoxLayout()
|
||||||
@@ -84,6 +85,7 @@ class GuiNovelView(QWidget):
|
|||||||
# Function Mappings
|
# Function Mappings
|
||||||
self.updateWordCounts = self.novelTree.updateWordCounts
|
self.updateWordCounts = self.novelTree.updateWordCounts
|
||||||
self.getSelectedHandle = self.novelTree.getSelectedHandle
|
self.getSelectedHandle = self.novelTree.getSelectedHandle
|
||||||
|
self.setActiveHandle = self.novelTree.setActiveHandle
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -91,14 +93,24 @@ class GuiNovelView(QWidget):
|
|||||||
# Methods
|
# Methods
|
||||||
##
|
##
|
||||||
|
|
||||||
|
def updateTheme(self):
|
||||||
|
"""Update theme elements.
|
||||||
|
"""
|
||||||
|
self.novelBar.updateTheme()
|
||||||
|
self.novelTree.updateTheme()
|
||||||
|
self.refreshTree()
|
||||||
|
return
|
||||||
|
|
||||||
def initSettings(self):
|
def initSettings(self):
|
||||||
|
"""Initialise GUI elements that depend on specific settings.
|
||||||
|
"""
|
||||||
self.novelTree.initSettings()
|
self.novelTree.initSettings()
|
||||||
return
|
return
|
||||||
|
|
||||||
def refreshTree(self):
|
def refreshTree(self):
|
||||||
"""Refresh the current tree.
|
"""Refresh the current tree.
|
||||||
"""
|
"""
|
||||||
self.novelTree.refreshTree(rootHandle=self.theProject.lastNovel)
|
self.novelTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("novelTree"))
|
||||||
return
|
return
|
||||||
|
|
||||||
def clearProject(self):
|
def clearProject(self):
|
||||||
@@ -106,12 +118,13 @@ class GuiNovelView(QWidget):
|
|||||||
"""
|
"""
|
||||||
self.novelTree.clearContent()
|
self.novelTree.clearContent()
|
||||||
self.novelBar.clearContent()
|
self.novelBar.clearContent()
|
||||||
|
self.novelBar.setEnabled(False)
|
||||||
return
|
return
|
||||||
|
|
||||||
def openProjectTasks(self):
|
def openProjectTasks(self):
|
||||||
"""Run opening project tasks.
|
"""Run open project tasks.
|
||||||
"""
|
"""
|
||||||
lastNovel = self.theProject.lastNovel
|
lastNovel = self.theProject.data.getLastHandle("novelTree")
|
||||||
if lastNovel not in self.theProject.tree:
|
if lastNovel not in self.theProject.tree:
|
||||||
lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL)
|
lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL)
|
||||||
|
|
||||||
@@ -125,6 +138,7 @@ class GuiNovelView(QWidget):
|
|||||||
self.novelBar.buildNovelRootMenu()
|
self.novelBar.buildNovelRootMenu()
|
||||||
self.novelBar.setLastColType(lastCol, doRefresh=False)
|
self.novelBar.setLastColType(lastCol, doRefresh=False)
|
||||||
self.novelBar.setCurrentRoot(lastNovel)
|
self.novelBar.setCurrentRoot(lastNovel)
|
||||||
|
self.novelBar.setEnabled(True)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -135,8 +149,8 @@ class GuiNovelView(QWidget):
|
|||||||
self.theProject.options.setValue("GuiNovelView", "lastCol", lastColType)
|
self.theProject.options.setValue("GuiNovelView", "lastCol", lastColType)
|
||||||
return
|
return
|
||||||
|
|
||||||
def setFocus(self):
|
def setTreeFocus(self):
|
||||||
"""Forward the set focus call to the tree widget.
|
"""Set the focus to the tree widget.
|
||||||
"""
|
"""
|
||||||
self.novelTree.setFocus()
|
self.novelTree.setFocus()
|
||||||
return
|
return
|
||||||
@@ -163,7 +177,7 @@ class GuiNovelView(QWidget):
|
|||||||
class GuiNovelToolBar(QWidget):
|
class GuiNovelToolBar(QWidget):
|
||||||
|
|
||||||
def __init__(self, novelView):
|
def __init__(self, novelView):
|
||||||
QTreeWidget.__init__(self, novelView)
|
super().__init__(parent=novelView)
|
||||||
|
|
||||||
logger.debug("Initialising GuiNovelToolBar ...")
|
logger.debug("Initialising GuiNovelToolBar ...")
|
||||||
|
|
||||||
@@ -173,21 +187,11 @@ class GuiNovelToolBar(QWidget):
|
|||||||
self.mainTheme = novelView.mainGui.mainTheme
|
self.mainTheme = novelView.mainGui.mainTheme
|
||||||
|
|
||||||
iPx = self.mainTheme.baseIconSize
|
iPx = self.mainTheme.baseIconSize
|
||||||
mPx = self.mainConf.pxInt(3)
|
mPx = self.mainConf.pxInt(2)
|
||||||
|
|
||||||
self.setContentsMargins(0, 0, 0, 0)
|
self.setContentsMargins(0, 0, 0, 0)
|
||||||
self.setAutoFillBackground(True)
|
self.setAutoFillBackground(True)
|
||||||
|
|
||||||
qPalette = self.palette()
|
|
||||||
qPalette.setBrush(QPalette.Window, qPalette.base())
|
|
||||||
self.setPalette(qPalette)
|
|
||||||
|
|
||||||
fadeCol = qPalette.text().color()
|
|
||||||
buttonStyle = (
|
|
||||||
"QToolButton {{padding: {0}px; border: none; background: transparent;}} "
|
|
||||||
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
|
|
||||||
).format(mPx, fadeCol.red(), fadeCol.green(), fadeCol.blue())
|
|
||||||
|
|
||||||
# Widget Label
|
# Widget Label
|
||||||
self.viewLabel = QLabel("<b>%s</b>" % self.tr("Novel Outline"))
|
self.viewLabel = QLabel("<b>%s</b>" % self.tr("Novel Outline"))
|
||||||
self.viewLabel.setContentsMargins(0, 0, 0, 0)
|
self.viewLabel.setContentsMargins(0, 0, 0, 0)
|
||||||
@@ -196,9 +200,7 @@ class GuiNovelToolBar(QWidget):
|
|||||||
# Refresh Button
|
# Refresh Button
|
||||||
self.tbRefresh = QToolButton(self)
|
self.tbRefresh = QToolButton(self)
|
||||||
self.tbRefresh.setToolTip(self.tr("Refresh"))
|
self.tbRefresh.setToolTip(self.tr("Refresh"))
|
||||||
self.tbRefresh.setIcon(self.mainTheme.getIcon("refresh"))
|
|
||||||
self.tbRefresh.setIconSize(QSize(iPx, iPx))
|
self.tbRefresh.setIconSize(QSize(iPx, iPx))
|
||||||
self.tbRefresh.setStyleSheet(buttonStyle)
|
|
||||||
self.tbRefresh.clicked.connect(self._refreshNovelTree)
|
self.tbRefresh.clicked.connect(self._refreshNovelTree)
|
||||||
|
|
||||||
# Novel Root Menu
|
# Novel Root Menu
|
||||||
@@ -208,9 +210,7 @@ class GuiNovelToolBar(QWidget):
|
|||||||
|
|
||||||
self.tbRoot = QToolButton(self)
|
self.tbRoot = QToolButton(self)
|
||||||
self.tbRoot.setToolTip(self.tr("Novel Root"))
|
self.tbRoot.setToolTip(self.tr("Novel Root"))
|
||||||
self.tbRoot.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]))
|
|
||||||
self.tbRoot.setIconSize(QSize(iPx, iPx))
|
self.tbRoot.setIconSize(QSize(iPx, iPx))
|
||||||
self.tbRoot.setStyleSheet(buttonStyle)
|
|
||||||
self.tbRoot.setMenu(self.mRoot)
|
self.tbRoot.setMenu(self.mRoot)
|
||||||
self.tbRoot.setPopupMode(QToolButton.InstantPopup)
|
self.tbRoot.setPopupMode(QToolButton.InstantPopup)
|
||||||
|
|
||||||
@@ -227,9 +227,7 @@ class GuiNovelToolBar(QWidget):
|
|||||||
|
|
||||||
self.tbMore = QToolButton(self)
|
self.tbMore = QToolButton(self)
|
||||||
self.tbMore.setToolTip(self.tr("More Options"))
|
self.tbMore.setToolTip(self.tr("More Options"))
|
||||||
self.tbMore.setIcon(self.mainTheme.getIcon("menu"))
|
|
||||||
self.tbMore.setIconSize(QSize(iPx, iPx))
|
self.tbMore.setIconSize(QSize(iPx, iPx))
|
||||||
self.tbMore.setStyleSheet(buttonStyle)
|
|
||||||
self.tbMore.setMenu(self.mMore)
|
self.tbMore.setMenu(self.mMore)
|
||||||
self.tbMore.setPopupMode(QToolButton.InstantPopup)
|
self.tbMore.setPopupMode(QToolButton.InstantPopup)
|
||||||
|
|
||||||
@@ -244,6 +242,8 @@ class GuiNovelToolBar(QWidget):
|
|||||||
|
|
||||||
self.setLayout(self.outerBox)
|
self.setLayout(self.outerBox)
|
||||||
|
|
||||||
|
self.updateTheme()
|
||||||
|
|
||||||
logger.debug("GuiNovelToolBar initialisation complete")
|
logger.debug("GuiNovelToolBar initialisation complete")
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -252,6 +252,31 @@ class GuiNovelToolBar(QWidget):
|
|||||||
# Methods
|
# Methods
|
||||||
##
|
##
|
||||||
|
|
||||||
|
def updateTheme(self):
|
||||||
|
"""Update theme elements.
|
||||||
|
"""
|
||||||
|
# Icons
|
||||||
|
self.tbRefresh.setIcon(self.mainTheme.getIcon("refresh"))
|
||||||
|
self.tbRoot.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]))
|
||||||
|
self.tbMore.setIcon(self.mainTheme.getIcon("menu"))
|
||||||
|
|
||||||
|
qPalette = self.palette()
|
||||||
|
qPalette.setBrush(QPalette.Window, qPalette.base())
|
||||||
|
self.setPalette(qPalette)
|
||||||
|
|
||||||
|
# StyleSheets
|
||||||
|
fadeCol = qPalette.text().color()
|
||||||
|
buttonStyle = (
|
||||||
|
"QToolButton {{padding: {0}px; border: none; background: transparent;}} "
|
||||||
|
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
|
||||||
|
).format(self.mainConf.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue())
|
||||||
|
|
||||||
|
self.tbRefresh.setStyleSheet(buttonStyle)
|
||||||
|
self.tbRoot.setStyleSheet(buttonStyle)
|
||||||
|
self.tbMore.setStyleSheet(buttonStyle)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
def clearContent(self):
|
def clearContent(self):
|
||||||
"""Run clearing project tasks.
|
"""Run clearing project tasks.
|
||||||
"""
|
"""
|
||||||
@@ -297,7 +322,7 @@ class GuiNovelToolBar(QWidget):
|
|||||||
def _refreshNovelTree(self):
|
def _refreshNovelTree(self):
|
||||||
"""Rebuild the current tree.
|
"""Rebuild the current tree.
|
||||||
"""
|
"""
|
||||||
rootHandle = self.theProject.lastNovel
|
rootHandle = self.theProject.data.getLastHandle("novelTree")
|
||||||
self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True)
|
self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -322,10 +347,15 @@ class GuiNovelTree(QTreeWidget):
|
|||||||
|
|
||||||
C_TITLE = 0
|
C_TITLE = 0
|
||||||
C_WORDS = 1
|
C_WORDS = 1
|
||||||
C_LAST = 2
|
C_EXTRA = 2
|
||||||
|
C_MORE = 3
|
||||||
|
|
||||||
|
D_HANDLE = Qt.UserRole
|
||||||
|
D_TITLE = Qt.UserRole + 1
|
||||||
|
D_KEY = Qt.UserRole + 2
|
||||||
|
|
||||||
def __init__(self, novelView):
|
def __init__(self, novelView):
|
||||||
QTreeWidget.__init__(self, novelView)
|
super().__init__(parent=novelView)
|
||||||
|
|
||||||
logger.debug("Initialising GuiNovelTree ...")
|
logger.debug("Initialising GuiNovelTree ...")
|
||||||
|
|
||||||
@@ -339,6 +369,7 @@ class GuiNovelTree(QTreeWidget):
|
|||||||
self._treeMap = {}
|
self._treeMap = {}
|
||||||
self._lastBuild = 0
|
self._lastBuild = 0
|
||||||
self._lastCol = NovelTreeColumn.POV
|
self._lastCol = NovelTreeColumn.POV
|
||||||
|
self._actHandle = None
|
||||||
|
|
||||||
# Cached Strings
|
# Cached Strings
|
||||||
self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
|
self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
|
||||||
@@ -353,9 +384,11 @@ class GuiNovelTree(QTreeWidget):
|
|||||||
|
|
||||||
self.setIconSize(QSize(iPx, iPx))
|
self.setIconSize(QSize(iPx, iPx))
|
||||||
self.setFrameStyle(QFrame.NoFrame)
|
self.setFrameStyle(QFrame.NoFrame)
|
||||||
|
self.setUniformRowHeights(True)
|
||||||
|
self.setAllColumnsShowFocus(True)
|
||||||
self.setHeaderHidden(True)
|
self.setHeaderHidden(True)
|
||||||
self.setIndentation(0)
|
self.setIndentation(0)
|
||||||
self.setColumnCount(3)
|
self.setColumnCount(4)
|
||||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||||
self.setExpandsOnDoubleClick(False)
|
self.setExpandsOnDoubleClick(False)
|
||||||
@@ -367,7 +400,8 @@ class GuiNovelTree(QTreeWidget):
|
|||||||
treeHeader.setMinimumSectionSize(iPx + cMg)
|
treeHeader.setMinimumSectionSize(iPx + cMg)
|
||||||
treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.Stretch)
|
treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.Stretch)
|
||||||
treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents)
|
treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents)
|
||||||
treeHeader.setSectionResizeMode(self.C_LAST, QHeaderView.ResizeToContents)
|
treeHeader.setSectionResizeMode(self.C_EXTRA, QHeaderView.ResizeToContents)
|
||||||
|
treeHeader.setSectionResizeMode(self.C_MORE, QHeaderView.ResizeToContents)
|
||||||
|
|
||||||
# Pre-Generate Tree Formatting
|
# Pre-Generate Tree Formatting
|
||||||
fH1 = self.font()
|
fH1 = self.font()
|
||||||
@@ -378,20 +412,15 @@ class GuiNovelTree(QTreeWidget):
|
|||||||
fH2.setBold(True)
|
fH2.setBold(True)
|
||||||
|
|
||||||
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
|
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
|
||||||
self._pIndent = [
|
|
||||||
self.mainTheme.loadDecoration("deco_doc_h0", pxH=iPx),
|
|
||||||
self.mainTheme.loadDecoration("deco_doc_h1", pxH=iPx),
|
|
||||||
self.mainTheme.loadDecoration("deco_doc_h2", pxH=iPx),
|
|
||||||
self.mainTheme.loadDecoration("deco_doc_h3", pxH=iPx),
|
|
||||||
self.mainTheme.loadDecoration("deco_doc_h4", pxH=iPx),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Connect signals
|
# Connect signals
|
||||||
|
self.clicked.connect(self._treeItemClicked)
|
||||||
self.itemDoubleClicked.connect(self._treeDoubleClick)
|
self.itemDoubleClicked.connect(self._treeDoubleClick)
|
||||||
self.itemSelectionChanged.connect(self._treeSelectionChange)
|
self.itemSelectionChanged.connect(self._treeSelectionChange)
|
||||||
|
|
||||||
# Set custom settings
|
# Set custom settings
|
||||||
self.initSettings()
|
self.initSettings()
|
||||||
|
self.updateTheme()
|
||||||
|
|
||||||
logger.debug("GuiNovelTree initialisation complete")
|
logger.debug("GuiNovelTree initialisation complete")
|
||||||
|
|
||||||
@@ -413,6 +442,13 @@ class GuiNovelTree(QTreeWidget):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def updateTheme(self):
|
||||||
|
"""Update theme elements.
|
||||||
|
"""
|
||||||
|
iPx = self.mainTheme.baseIconSize
|
||||||
|
self._pMore = self.mainTheme.loadDecoration("deco_doc_more", pxH=iPx)
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Properties
|
# Properties
|
||||||
##
|
##
|
||||||
@@ -436,23 +472,23 @@ class GuiNovelTree(QTreeWidget):
|
|||||||
def refreshTree(self, rootHandle=None, overRide=False):
|
def refreshTree(self, rootHandle=None, overRide=False):
|
||||||
"""Called whenever the Novel tab is activated.
|
"""Called whenever the Novel tab is activated.
|
||||||
"""
|
"""
|
||||||
logger.verbose("Requesting refresh of the novel tree")
|
logger.debug("Requesting refresh of the novel tree")
|
||||||
if rootHandle is None:
|
if rootHandle is None:
|
||||||
rootHandle = self.theProject.tree.findRoot(nwItemClass.NOVEL)
|
rootHandle = self.theProject.tree.findRoot(nwItemClass.NOVEL)
|
||||||
|
|
||||||
treeChanged = self.mainGui.projView.changedSince(self._lastBuild)
|
treeChanged = self.mainGui.projView.changedSince(self._lastBuild)
|
||||||
indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
|
indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
|
||||||
if not (treeChanged or indexChanged or overRide):
|
if not (treeChanged or indexChanged or overRide):
|
||||||
logger.verbose("No changes have been made to the novel index")
|
logger.debug("No changes have been made to the novel index")
|
||||||
return
|
return
|
||||||
|
|
||||||
selItem = self.selectedItems()
|
selItem = self.selectedItems()
|
||||||
titleKey = None
|
titleKey = None
|
||||||
if selItem:
|
if selItem:
|
||||||
titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2]
|
titleKey = selItem[0].data(self.C_TITLE, self.D_KEY)
|
||||||
|
|
||||||
self._populateTree(rootHandle)
|
self._populateTree(rootHandle)
|
||||||
self.theProject.setLastNovelViewed(rootHandle)
|
self.theProject.data.setLastHandle(rootHandle, "novelTree")
|
||||||
|
|
||||||
if titleKey is not None and titleKey in self._treeMap:
|
if titleKey is not None and titleKey in self._treeMap:
|
||||||
self._treeMap[titleKey].setSelected(True)
|
self._treeMap[titleKey].setSelected(True)
|
||||||
@@ -476,8 +512,9 @@ class GuiNovelTree(QTreeWidget):
|
|||||||
tHandle = None
|
tHandle = None
|
||||||
tLine = 0
|
tLine = 0
|
||||||
if selItem:
|
if selItem:
|
||||||
tHandle = selItem[0].data(self.C_TITLE, Qt.UserRole)[0]
|
tHandle = selItem[0].data(self.C_TITLE, self.D_HANDLE)
|
||||||
tLine = checkInt(selItem[0].data(self.C_TITLE, Qt.UserRole)[1], 1) - 1
|
sTitle = selItem[0].data(self.C_TITLE, self.D_TITLE)
|
||||||
|
tLine = checkInt(sTitle[1:], 1) - 1
|
||||||
|
|
||||||
return tHandle, tLine
|
return tHandle, tLine
|
||||||
|
|
||||||
@@ -487,9 +524,34 @@ class GuiNovelTree(QTreeWidget):
|
|||||||
if self._lastCol != colType:
|
if self._lastCol != colType:
|
||||||
logger.debug("Changing last column to %s", colType.name)
|
logger.debug("Changing last column to %s", colType.name)
|
||||||
self._lastCol = colType
|
self._lastCol = colType
|
||||||
self.setColumnHidden(self.C_LAST, colType == NovelTreeColumn.HIDDEN)
|
self.setColumnHidden(self.C_EXTRA, colType == NovelTreeColumn.HIDDEN)
|
||||||
if doRefresh:
|
if doRefresh:
|
||||||
self.refreshTree(rootHandle=self.theProject.lastNovel, overRide=True)
|
lastNovel = self.theProject.data.getLastHandle("novelTree")
|
||||||
|
self.refreshTree(rootHandle=lastNovel, overRide=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
def setActiveHandle(self, tHandle):
|
||||||
|
"""Highlight the rows associated with a given handle.
|
||||||
|
"""
|
||||||
|
tStart = time()
|
||||||
|
|
||||||
|
self._actHandle = tHandle
|
||||||
|
for i in range(self.topLevelItemCount()):
|
||||||
|
tItem = self.topLevelItem(i)
|
||||||
|
if tItem is not None:
|
||||||
|
if tItem.data(self.C_TITLE, self.D_HANDLE) == tHandle:
|
||||||
|
tItem.setBackground(self.C_TITLE, self.palette().alternateBase())
|
||||||
|
tItem.setBackground(self.C_WORDS, self.palette().alternateBase())
|
||||||
|
tItem.setBackground(self.C_EXTRA, self.palette().alternateBase())
|
||||||
|
tItem.setBackground(self.C_MORE, self.palette().alternateBase())
|
||||||
|
else:
|
||||||
|
tItem.setBackground(self.C_TITLE, self.palette().base())
|
||||||
|
tItem.setBackground(self.C_WORDS, self.palette().base())
|
||||||
|
tItem.setBackground(self.C_EXTRA, self.palette().base())
|
||||||
|
tItem.setBackground(self.C_MORE, self.palette().base())
|
||||||
|
|
||||||
|
logger.debug("Highlighted Novel Tree in %.3f ms", (time() - tStart)*1000)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -501,7 +563,7 @@ class GuiNovelTree(QTreeWidget):
|
|||||||
mouse in a blank area of the tree view, and to load a document
|
mouse in a blank area of the tree view, and to load a document
|
||||||
for viewing if the user middle-clicked.
|
for viewing if the user middle-clicked.
|
||||||
"""
|
"""
|
||||||
QTreeWidget.mousePressEvent(self, theEvent)
|
super().mousePressEvent(theEvent)
|
||||||
|
|
||||||
if theEvent.button() == Qt.LeftButton:
|
if theEvent.button() == Qt.LeftButton:
|
||||||
selItem = self.indexAt(theEvent.pos())
|
selItem = self.indexAt(theEvent.pos())
|
||||||
@@ -521,10 +583,28 @@ class GuiNovelTree(QTreeWidget):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def focusOutEvent(self, theEvent):
|
||||||
|
"""Clear the selection when the tree no longer has focus.
|
||||||
|
"""
|
||||||
|
super().focusOutEvent(theEvent)
|
||||||
|
self.clearSelection()
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Private Slots
|
# Private Slots
|
||||||
##
|
##
|
||||||
|
|
||||||
|
@pyqtSlot("QModelIndex")
|
||||||
|
def _treeItemClicked(self, mIndex):
|
||||||
|
"""The user clicked on an item in the tree.
|
||||||
|
"""
|
||||||
|
if mIndex.column() == self.C_MORE:
|
||||||
|
tHandle = mIndex.siblingAtColumn(self.C_TITLE).data(self.D_HANDLE)
|
||||||
|
sTitle = mIndex.siblingAtColumn(self.C_TITLE).data(self.D_TITLE)
|
||||||
|
tipPos = self.mapToGlobal(self.visualRect(mIndex).topRight())
|
||||||
|
self._popMetaBox(tipPos, tHandle, sTitle)
|
||||||
|
return
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def _treeSelectionChange(self):
|
def _treeSelectionChange(self):
|
||||||
"""Extract the handle and line number of the currently selected
|
"""Extract the handle and line number of the currently selected
|
||||||
@@ -554,7 +634,7 @@ class GuiNovelTree(QTreeWidget):
|
|||||||
"""
|
"""
|
||||||
self.clearContent()
|
self.clearContent()
|
||||||
tStart = time()
|
tStart = time()
|
||||||
logger.verbose("Building novel tree for root item '%s'", rootHandle)
|
logger.debug("Building novel tree for root item '%s'", rootHandle)
|
||||||
|
|
||||||
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
|
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
|
||||||
for tKey, tHandle, sTitle, novIdx in novStruct:
|
for tKey, tHandle, sTitle, novIdx in novStruct:
|
||||||
@@ -563,25 +643,31 @@ class GuiNovelTree(QTreeWidget):
|
|||||||
if iLevel == 0:
|
if iLevel == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
newItem = QTreeWidgetItem()
|
hDec = self.mainTheme.getHeaderDecoration(iLevel)
|
||||||
theData = (tHandle, sTitle[1:].lstrip("0"), tKey)
|
|
||||||
|
|
||||||
newItem.setData(self.C_TITLE, Qt.DecorationRole, self._pIndent[iLevel])
|
newItem = QTreeWidgetItem()
|
||||||
|
newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
|
||||||
newItem.setText(self.C_TITLE, novIdx.title)
|
newItem.setText(self.C_TITLE, novIdx.title)
|
||||||
newItem.setData(self.C_TITLE, Qt.UserRole, theData)
|
newItem.setData(self.C_TITLE, self.D_HANDLE, tHandle)
|
||||||
|
newItem.setData(self.C_TITLE, self.D_TITLE, sTitle)
|
||||||
|
newItem.setData(self.C_TITLE, self.D_KEY, tKey)
|
||||||
newItem.setFont(self.C_TITLE, self._hFonts[iLevel])
|
newItem.setFont(self.C_TITLE, self._hFonts[iLevel])
|
||||||
newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}")
|
newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}")
|
||||||
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
|
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
|
||||||
|
newItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore)
|
||||||
|
|
||||||
|
# Custom column
|
||||||
lastText, toolTip = self._getLastColumnText(tHandle, sTitle)
|
lastText, toolTip = self._getLastColumnText(tHandle, sTitle)
|
||||||
newItem.setText(self.C_LAST, lastText)
|
newItem.setText(self.C_EXTRA, lastText)
|
||||||
if lastText:
|
if lastText:
|
||||||
newItem.setToolTip(self.C_LAST, toolTip)
|
newItem.setToolTip(self.C_EXTRA, toolTip)
|
||||||
|
|
||||||
self._treeMap[tKey] = newItem
|
self._treeMap[tKey] = newItem
|
||||||
self.addTopLevelItem(newItem)
|
self.addTopLevelItem(newItem)
|
||||||
|
|
||||||
logger.verbose("Novel Tree built in %.3f ms", (time() - tStart)*1000)
|
self.setActiveHandle(self._actHandle)
|
||||||
|
|
||||||
|
logger.debug("Novel Tree built in %.3f ms", (time() - tStart)*1000)
|
||||||
self._lastBuild = time()
|
self._lastBuild = time()
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -607,4 +693,49 @@ class GuiNovelTree(QTreeWidget):
|
|||||||
|
|
||||||
return "", ""
|
return "", ""
|
||||||
|
|
||||||
|
def _popMetaBox(self, qPos, tHandle, sTitle):
|
||||||
|
"""Show the novel meta data box.
|
||||||
|
"""
|
||||||
|
logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle)
|
||||||
|
|
||||||
|
pIndex = self.theProject.index
|
||||||
|
novIdx = pIndex.getNovelData(tHandle, sTitle)
|
||||||
|
refTags = pIndex.getReferences(tHandle, sTitle)
|
||||||
|
|
||||||
|
synopText = novIdx.synopsis
|
||||||
|
if synopText:
|
||||||
|
synopLabel = trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP])
|
||||||
|
synopText = f"<p><b>{synopLabel}</b>: {synopText}</p>"
|
||||||
|
|
||||||
|
refLines = []
|
||||||
|
refLines = self._appendMetaTag(refTags, nwKeyWords.POV_KEY, refLines)
|
||||||
|
refLines = self._appendMetaTag(refTags, nwKeyWords.FOCUS_KEY, refLines)
|
||||||
|
refLines = self._appendMetaTag(refTags, nwKeyWords.CHAR_KEY, refLines)
|
||||||
|
refLines = self._appendMetaTag(refTags, nwKeyWords.PLOT_KEY, refLines)
|
||||||
|
refLines = self._appendMetaTag(refTags, nwKeyWords.TIME_KEY, refLines)
|
||||||
|
refLines = self._appendMetaTag(refTags, nwKeyWords.WORLD_KEY, refLines)
|
||||||
|
refLines = self._appendMetaTag(refTags, nwKeyWords.OBJECT_KEY, refLines)
|
||||||
|
refLines = self._appendMetaTag(refTags, nwKeyWords.ENTITY_KEY, refLines)
|
||||||
|
refLines = self._appendMetaTag(refTags, nwKeyWords.CUSTOM_KEY, refLines)
|
||||||
|
|
||||||
|
refText = ""
|
||||||
|
if refLines:
|
||||||
|
refList = "<br>".join(refLines)
|
||||||
|
refText = f"<p>{refList}</p>"
|
||||||
|
|
||||||
|
ttText = refText + synopText or self.tr("No meta data")
|
||||||
|
if ttText:
|
||||||
|
QToolTip.showText(qPos, ttText)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _appendMetaTag(refs, key, lines):
|
||||||
|
"""Generate a reference list for a given reference key.
|
||||||
|
"""
|
||||||
|
tags = ", ".join(refs.get(key, []))
|
||||||
|
if tags:
|
||||||
|
lines.append(f"<b>{trConst(nwLabels.KEY_NAME[key])}</b>: {tags}")
|
||||||
|
return lines
|
||||||
|
|
||||||
# END Class GuiNovelTree
|
# END Class GuiNovelTree
|
||||||
|
|||||||
@@ -37,16 +37,16 @@ from PyQt5.QtCore import (
|
|||||||
Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP
|
Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP
|
||||||
)
|
)
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QAbstractItemView, QAction, QGridLayout, QGroupBox, QHBoxLayout, QLabel,
|
QAbstractItemView, QAction, QComboBox, QFrame, QGridLayout, QGroupBox,
|
||||||
QMenu, QScrollArea, QSplitter, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
QHBoxLayout, QLabel, QMenu, QScrollArea, QSizePolicy, QSplitter, QToolBar,
|
||||||
QWidget, QFrame, QToolBar, QSizePolicy, QComboBox, QToolButton
|
QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
|
||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter.enum import (
|
from novelwriter.enum import (
|
||||||
nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline
|
nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline
|
||||||
)
|
)
|
||||||
from novelwriter.common import checkInt
|
from novelwriter.common import checkInt
|
||||||
from novelwriter.constants import trConst, nwKeyWords, nwLabels
|
from novelwriter.constants import nwHeaders, trConst, nwKeyWords, nwLabels
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -57,20 +57,22 @@ class GuiOutlineView(QWidget):
|
|||||||
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
||||||
|
|
||||||
def __init__(self, mainGui):
|
def __init__(self, mainGui):
|
||||||
QWidget.__init__(self, mainGui)
|
super().__init__(parent=mainGui)
|
||||||
|
|
||||||
self.mainConf = novelwriter.CONFIG
|
self.mainConf = novelwriter.CONFIG
|
||||||
self.mainGui = mainGui
|
self.mainGui = mainGui
|
||||||
|
self.theProject = mainGui.theProject
|
||||||
|
|
||||||
# Build GUI
|
# Build GUI
|
||||||
self.outlineBar = GuiOutlineToolBar(self)
|
|
||||||
self.outlineTree = GuiOutlineTree(self)
|
self.outlineTree = GuiOutlineTree(self)
|
||||||
self.outlineData = GuiOutlineDetails(self)
|
self.outlineData = GuiOutlineDetails(self)
|
||||||
|
self.outlineBar = GuiOutlineToolBar(self)
|
||||||
|
self.outlineBar.setEnabled(False)
|
||||||
|
|
||||||
self.splitOutline = QSplitter(Qt.Vertical)
|
self.splitOutline = QSplitter(Qt.Vertical)
|
||||||
self.splitOutline.addWidget(self.outlineTree)
|
self.splitOutline.addWidget(self.outlineTree)
|
||||||
self.splitOutline.addWidget(self.outlineData)
|
self.splitOutline.addWidget(self.outlineData)
|
||||||
self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
|
self.splitOutline.setSizes(self.mainConf.outlinePanePos)
|
||||||
|
|
||||||
# Assemble
|
# Assemble
|
||||||
self.outerBox = QVBoxLayout()
|
self.outerBox = QVBoxLayout()
|
||||||
@@ -96,33 +98,67 @@ class GuiOutlineView(QWidget):
|
|||||||
# Methods
|
# Methods
|
||||||
##
|
##
|
||||||
|
|
||||||
def splitSizes(self):
|
def updateTheme(self):
|
||||||
return self.splitOutline.sizes()
|
"""Update theme elements.
|
||||||
|
"""
|
||||||
|
self.outlineBar.updateTheme()
|
||||||
|
self.refreshTree()
|
||||||
|
return
|
||||||
|
|
||||||
def clearOutline(self):
|
def initSettings(self):
|
||||||
|
"""Initialise GUI elements that depend on specific settings.
|
||||||
|
"""
|
||||||
|
self.outlineTree.initSettings()
|
||||||
|
self.outlineData.initSettings()
|
||||||
|
return
|
||||||
|
|
||||||
|
def refreshTree(self):
|
||||||
|
"""Refresh the current tree.
|
||||||
|
"""
|
||||||
|
self.outlineTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("outline"))
|
||||||
|
return
|
||||||
|
|
||||||
|
def clearProject(self):
|
||||||
|
"""Clear project-related GUI content.
|
||||||
|
"""
|
||||||
self.outlineData.clearDetails()
|
self.outlineData.clearDetails()
|
||||||
|
self.outlineBar.setEnabled(False)
|
||||||
return
|
return
|
||||||
|
|
||||||
def initOutline(self):
|
def openProjectTasks(self):
|
||||||
self.outlineTree.initOutline()
|
"""Run open project tasks.
|
||||||
self.outlineData.initDetails()
|
"""
|
||||||
|
lastOutline = self.theProject.data.getLastHandle("outline")
|
||||||
|
if not (lastOutline in self.theProject.tree or lastOutline is None):
|
||||||
|
lastOutline = self.theProject.tree.findRoot(nwItemClass.NOVEL)
|
||||||
|
|
||||||
|
logger.debug("Setting outline tree to root item '%s'", lastOutline)
|
||||||
|
|
||||||
|
self.clearProject()
|
||||||
|
self.outlineBar.populateNovelList()
|
||||||
|
self.outlineBar.setCurrentRoot(lastOutline)
|
||||||
|
self.outlineBar.setEnabled(True)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def closeOutline(self):
|
def closeProjectTasks(self):
|
||||||
self.outlineTree.closeOutline()
|
self.outlineTree.closeProjectTasks()
|
||||||
self.outlineData.updateClasses()
|
self.outlineData.updateClasses()
|
||||||
return
|
return
|
||||||
|
|
||||||
def refreshView(self, overRide=False, novelChanged=False):
|
def splitSizes(self):
|
||||||
self.outlineTree.refreshTree(overRide=overRide, novelChanged=novelChanged)
|
return self.splitOutline.sizes()
|
||||||
return
|
|
||||||
|
|
||||||
def treeHasFocus(self):
|
|
||||||
return self.outlineTree.hasFocus()
|
|
||||||
|
|
||||||
def setTreeFocus(self):
|
def setTreeFocus(self):
|
||||||
|
"""Set the focus to the tree widget.
|
||||||
|
"""
|
||||||
return self.outlineTree.setFocus()
|
return self.outlineTree.setFocus()
|
||||||
|
|
||||||
|
def treeHasFocus(self):
|
||||||
|
"""Check if the outline tree has focus.
|
||||||
|
"""
|
||||||
|
return self.outlineTree.hasFocus()
|
||||||
|
|
||||||
##
|
##
|
||||||
# Public Slots
|
# Public Slots
|
||||||
##
|
##
|
||||||
@@ -172,7 +208,7 @@ class GuiOutlineToolBar(QToolBar):
|
|||||||
viewColumnToggled = pyqtSignal(bool, Enum)
|
viewColumnToggled = pyqtSignal(bool, Enum)
|
||||||
|
|
||||||
def __init__(self, theOutline):
|
def __init__(self, theOutline):
|
||||||
QTreeWidget.__init__(self, theOutline)
|
super().__init__(parent=theOutline)
|
||||||
|
|
||||||
logger.debug("Initialising GuiOutlineToolBar ...")
|
logger.debug("Initialising GuiOutlineToolBar ...")
|
||||||
|
|
||||||
@@ -187,7 +223,6 @@ class GuiOutlineToolBar(QToolBar):
|
|||||||
self.setMovable(False)
|
self.setMovable(False)
|
||||||
self.setIconSize(QSize(iPx, iPx))
|
self.setIconSize(QSize(iPx, iPx))
|
||||||
self.setContentsMargins(0, 0, 0, 0)
|
self.setContentsMargins(0, 0, 0, 0)
|
||||||
self.setStyleSheet("QToolBar {border: 0px;}")
|
|
||||||
|
|
||||||
stretch = QWidget(self)
|
stretch = QWidget(self)
|
||||||
stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
@@ -202,7 +237,6 @@ class GuiOutlineToolBar(QToolBar):
|
|||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
self.aRefresh = QAction(self.tr("Refresh"), self)
|
self.aRefresh = QAction(self.tr("Refresh"), self)
|
||||||
self.aRefresh.setIcon(self.mainTheme.getIcon("refresh"))
|
|
||||||
self.aRefresh.triggered.connect(self._refreshRequested)
|
self.aRefresh.triggered.connect(self._refreshRequested)
|
||||||
|
|
||||||
# Column Menu
|
# Column Menu
|
||||||
@@ -212,7 +246,6 @@ class GuiOutlineToolBar(QToolBar):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.tbColumns = QToolButton(self)
|
self.tbColumns = QToolButton(self)
|
||||||
self.tbColumns.setIcon(self.mainTheme.getIcon("menu"))
|
|
||||||
self.tbColumns.setMenu(self.mColumns)
|
self.tbColumns.setMenu(self.mColumns)
|
||||||
self.tbColumns.setPopupMode(QToolButton.InstantPopup)
|
self.tbColumns.setPopupMode(QToolButton.InstantPopup)
|
||||||
|
|
||||||
@@ -224,6 +257,8 @@ class GuiOutlineToolBar(QToolBar):
|
|||||||
self.addWidget(self.tbColumns)
|
self.addWidget(self.tbColumns)
|
||||||
self.addWidget(stretch)
|
self.addWidget(stretch)
|
||||||
|
|
||||||
|
self.updateTheme()
|
||||||
|
|
||||||
logger.debug("GuiOutlineToolBar initialisation complete")
|
logger.debug("GuiOutlineToolBar initialisation complete")
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -232,6 +267,16 @@ class GuiOutlineToolBar(QToolBar):
|
|||||||
# Methods
|
# Methods
|
||||||
##
|
##
|
||||||
|
|
||||||
|
def updateTheme(self):
|
||||||
|
"""Update theme elements.
|
||||||
|
"""
|
||||||
|
self.setStyleSheet("QToolBar {border: 0px;}")
|
||||||
|
|
||||||
|
self.aRefresh.setIcon(self.mainTheme.getIcon("refresh"))
|
||||||
|
self.tbColumns.setIcon(self.mainTheme.getIcon("menu"))
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
def populateNovelList(self):
|
def populateNovelList(self):
|
||||||
"""Fill the novel combo box with a list of all novel folders.
|
"""Fill the novel combo box with a list of all novel folders.
|
||||||
"""
|
"""
|
||||||
@@ -243,6 +288,17 @@ class GuiOutlineToolBar(QToolBar):
|
|||||||
self.novelValue.addItem(tIcon, self.tr("All Novel Folders"), "")
|
self.novelValue.addItem(tIcon, self.tr("All Novel Folders"), "")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def setCurrentRoot(self, rootHandle):
|
||||||
|
"""Set the current active root handle.
|
||||||
|
"""
|
||||||
|
if rootHandle is None:
|
||||||
|
rootIdx = self.novelValue.count() - 1
|
||||||
|
else:
|
||||||
|
rootIdx = self.novelValue.findData(rootHandle)
|
||||||
|
if rootIdx >= 0:
|
||||||
|
self.novelValue.setCurrentIndex(rootIdx)
|
||||||
|
return
|
||||||
|
|
||||||
def setColumnHiddenState(self, hiddenState):
|
def setColumnHiddenState(self, hiddenState):
|
||||||
"""Forward the change of column hidden states to the menu.
|
"""Forward the change of column hidden states to the menu.
|
||||||
"""
|
"""
|
||||||
@@ -313,11 +369,14 @@ class GuiOutlineTree(QTreeWidget):
|
|||||||
nwOutline.SYNOP: False,
|
nwOutline.SYNOP: False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
D_HANDLE = Qt.UserRole
|
||||||
|
D_TITLE = Qt.UserRole + 1
|
||||||
|
|
||||||
hiddenStateChanged = pyqtSignal()
|
hiddenStateChanged = pyqtSignal()
|
||||||
activeItemChanged = pyqtSignal(str, str)
|
activeItemChanged = pyqtSignal(str, str)
|
||||||
|
|
||||||
def __init__(self, theOutline):
|
def __init__(self, theOutline):
|
||||||
QTreeWidget.__init__(self, theOutline)
|
super().__init__(parent=theOutline)
|
||||||
|
|
||||||
logger.debug("Initialising GuiOutlineTree ...")
|
logger.debug("Initialising GuiOutlineTree ...")
|
||||||
|
|
||||||
@@ -326,6 +385,7 @@ class GuiOutlineTree(QTreeWidget):
|
|||||||
self.theProject = theOutline.mainGui.theProject
|
self.theProject = theOutline.mainGui.theProject
|
||||||
self.mainTheme = theOutline.mainGui.mainTheme
|
self.mainTheme = theOutline.mainGui.mainTheme
|
||||||
|
|
||||||
|
self.setUniformRowHeights(True)
|
||||||
self.setFrameStyle(QFrame.NoFrame)
|
self.setFrameStyle(QFrame.NoFrame)
|
||||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||||
@@ -336,11 +396,28 @@ class GuiOutlineTree(QTreeWidget):
|
|||||||
|
|
||||||
iPx = self.mainTheme.baseIconSize
|
iPx = self.mainTheme.baseIconSize
|
||||||
self.setIconSize(QSize(iPx, iPx))
|
self.setIconSize(QSize(iPx, iPx))
|
||||||
self.setIndentation(iPx)
|
self.setIndentation(0)
|
||||||
|
|
||||||
self.treeHead = self.header()
|
self.treeHead = self.header()
|
||||||
self.treeHead.sectionMoved.connect(self._columnMoved)
|
self.treeHead.sectionMoved.connect(self._columnMoved)
|
||||||
|
|
||||||
|
# Pre-Generate Tree Formatting
|
||||||
|
fH1 = self.font()
|
||||||
|
fH1.setBold(True)
|
||||||
|
fH1.setUnderline(True)
|
||||||
|
|
||||||
|
fH2 = self.font()
|
||||||
|
fH2.setBold(True)
|
||||||
|
|
||||||
|
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
|
||||||
|
self._dIcon = {
|
||||||
|
"H0": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H0"),
|
||||||
|
"H1": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H1"),
|
||||||
|
"H2": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H2"),
|
||||||
|
"H3": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H3"),
|
||||||
|
"H4": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H4"),
|
||||||
|
}
|
||||||
|
|
||||||
# Internals
|
# Internals
|
||||||
self._treeOrder = []
|
self._treeOrder = []
|
||||||
self._colWidth = {}
|
self._colWidth = {}
|
||||||
@@ -350,8 +427,8 @@ class GuiOutlineTree(QTreeWidget):
|
|||||||
self._firstView = True
|
self._firstView = True
|
||||||
self._lastBuild = 0
|
self._lastBuild = 0
|
||||||
|
|
||||||
self.initOutline()
|
self.initSettings()
|
||||||
self.clearOutline()
|
self.clearContent()
|
||||||
|
|
||||||
self.hiddenStateChanged.emit()
|
self.hiddenStateChanged.emit()
|
||||||
|
|
||||||
@@ -371,7 +448,7 @@ class GuiOutlineTree(QTreeWidget):
|
|||||||
# Methods
|
# Methods
|
||||||
##
|
##
|
||||||
|
|
||||||
def initOutline(self):
|
def initSettings(self):
|
||||||
"""Set or update outline settings.
|
"""Set or update outline settings.
|
||||||
"""
|
"""
|
||||||
# Scroll bars
|
# Scroll bars
|
||||||
@@ -387,7 +464,7 @@ class GuiOutlineTree(QTreeWidget):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def clearOutline(self):
|
def clearContent(self):
|
||||||
"""Clear the tree and header and set the default values for the
|
"""Clear the tree and header and set the default values for the
|
||||||
columns arrays.
|
columns arrays.
|
||||||
"""
|
"""
|
||||||
@@ -425,18 +502,20 @@ class GuiOutlineTree(QTreeWidget):
|
|||||||
# If the novel index or novel tree has changed since the tree
|
# If the novel index or novel tree has changed since the tree
|
||||||
# was last built, we rebuild the tree from the updated index.
|
# was last built, we rebuild the tree from the updated index.
|
||||||
indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
|
indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
|
||||||
doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline
|
if not (novelChanged or indexChanged or overRide):
|
||||||
if doBuild or overRide:
|
logger.debug("No changes have been made to the novel index")
|
||||||
logger.debug("Rebuilding Project Outline")
|
return
|
||||||
self._populateTree(rootHandle)
|
|
||||||
|
self._populateTree(rootHandle)
|
||||||
|
self.theProject.data.setLastHandle(rootHandle or None, "outline")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def closeOutline(self):
|
def closeProjectTasks(self):
|
||||||
"""Called before a project is closed.
|
"""Called before a project is closed.
|
||||||
"""
|
"""
|
||||||
self._saveHeaderState()
|
self._saveHeaderState()
|
||||||
self.clearOutline()
|
self.clearContent()
|
||||||
self._firstView = True
|
self._firstView = True
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -448,7 +527,7 @@ class GuiOutlineTree(QTreeWidget):
|
|||||||
tHandle = None
|
tHandle = None
|
||||||
tLine = 0
|
tLine = 0
|
||||||
if selItem:
|
if selItem:
|
||||||
tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
|
tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
|
||||||
tLine = checkInt(selItem[0].text(self._colIdx[nwOutline.LINE]), 1) - 1
|
tLine = checkInt(selItem[0].text(self._colIdx[nwOutline.LINE]), 1) - 1
|
||||||
|
|
||||||
return tHandle, tLine
|
return tHandle, tLine
|
||||||
@@ -474,8 +553,8 @@ class GuiOutlineTree(QTreeWidget):
|
|||||||
"""
|
"""
|
||||||
selItems = self.selectedItems()
|
selItems = self.selectedItems()
|
||||||
if selItems:
|
if selItems:
|
||||||
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
|
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
|
||||||
sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole)
|
sTitle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
|
||||||
self.activeItemChanged.emit(tHandle, sTitle)
|
self.activeItemChanged.emit(tHandle, sTitle)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -494,11 +573,9 @@ class GuiOutlineTree(QTreeWidget):
|
|||||||
"""Receive the changes to column visibility forwarded by the
|
"""Receive the changes to column visibility forwarded by the
|
||||||
column selection menu.
|
column selection menu.
|
||||||
"""
|
"""
|
||||||
logger.verbose("User toggled Outline column '%s'", theItem.name)
|
|
||||||
if theItem in self._colIdx:
|
if theItem in self._colIdx:
|
||||||
self.setColumnHidden(self._colIdx[theItem], not isChecked)
|
self.setColumnHidden(self._colIdx[theItem], not isChecked)
|
||||||
self._saveHeaderState()
|
self._saveHeaderState()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -613,110 +690,60 @@ class GuiOutlineTree(QTreeWidget):
|
|||||||
self.setColumnWidth(self._colIdx[hItem], self._colWidth[hItem])
|
self.setColumnWidth(self._colIdx[hItem], self._colWidth[hItem])
|
||||||
self.setColumnHidden(self._colIdx[hItem], self._colHidden[hItem])
|
self.setColumnHidden(self._colIdx[hItem], self._colHidden[hItem])
|
||||||
|
|
||||||
# Make sure title column is always visible,
|
# Make sure title column is always visible
|
||||||
# and handle column always hidden
|
|
||||||
self.setColumnHidden(self._colIdx[nwOutline.TITLE], False)
|
self.setColumnHidden(self._colIdx[nwOutline.TITLE], False)
|
||||||
|
|
||||||
headItem = self.headerItem()
|
headItem = self.headerItem()
|
||||||
headItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
|
if headItem is not None:
|
||||||
headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
|
headItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
|
||||||
headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
|
headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
|
||||||
|
headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
|
||||||
currTitle = None
|
|
||||||
currChapter = None
|
|
||||||
currScene = None
|
|
||||||
|
|
||||||
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
|
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
|
||||||
for _, tHandle, sTitle, novIdx in novStruct:
|
for _, tHandle, sTitle, novIdx in novStruct:
|
||||||
|
|
||||||
tItem = self._createTreeItem(tHandle, sTitle, novIdx)
|
iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0)
|
||||||
|
if iLevel == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
tLevel = novIdx.level
|
trItem = QTreeWidgetItem()
|
||||||
if tLevel == "H1":
|
nwItem = self.theProject.tree[tHandle]
|
||||||
self.addTopLevelItem(tItem)
|
hDec = self.mainTheme.getHeaderDecoration(iLevel)
|
||||||
currTitle = tItem
|
|
||||||
currChapter = None
|
|
||||||
currScene = None
|
|
||||||
|
|
||||||
elif tLevel == "H2":
|
trItem.setData(self._colIdx[nwOutline.TITLE], Qt.DecorationRole, hDec)
|
||||||
if currTitle is None:
|
trItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title)
|
||||||
self.addTopLevelItem(tItem)
|
trItem.setData(self._colIdx[nwOutline.TITLE], self.D_HANDLE, tHandle)
|
||||||
else:
|
trItem.setData(self._colIdx[nwOutline.TITLE], self.D_TITLE, sTitle)
|
||||||
currTitle.addChild(tItem)
|
trItem.setFont(self._colIdx[nwOutline.TITLE], self._hFonts[iLevel])
|
||||||
currChapter = tItem
|
trItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level)
|
||||||
currScene = None
|
trItem.setIcon(self._colIdx[nwOutline.LABEL], self._dIcon[nwItem.mainHeading])
|
||||||
|
trItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName)
|
||||||
|
trItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0"))
|
||||||
|
trItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis)
|
||||||
|
trItem.setText(self._colIdx[nwOutline.CCOUNT], f"{novIdx.charCount:n}")
|
||||||
|
trItem.setText(self._colIdx[nwOutline.WCOUNT], f"{novIdx.wordCount:n}")
|
||||||
|
trItem.setText(self._colIdx[nwOutline.PCOUNT], f"{novIdx.paraCount:n}")
|
||||||
|
trItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
|
||||||
|
trItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
|
||||||
|
trItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
|
||||||
|
|
||||||
elif tLevel == "H3":
|
refs = self.theProject.index.getReferences(tHandle, sTitle)
|
||||||
if currChapter is None:
|
trItem.setText(self._colIdx[nwOutline.POV], ", ".join(refs[nwKeyWords.POV_KEY]))
|
||||||
if currTitle is None:
|
trItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(refs[nwKeyWords.FOCUS_KEY]))
|
||||||
self.addTopLevelItem(tItem)
|
trItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(refs[nwKeyWords.CHAR_KEY]))
|
||||||
else:
|
trItem.setText(self._colIdx[nwOutline.PLOT], ", ".join(refs[nwKeyWords.PLOT_KEY]))
|
||||||
currTitle.addChild(tItem)
|
trItem.setText(self._colIdx[nwOutline.TIME], ", ".join(refs[nwKeyWords.TIME_KEY]))
|
||||||
else:
|
trItem.setText(self._colIdx[nwOutline.WORLD], ", ".join(refs[nwKeyWords.WORLD_KEY]))
|
||||||
currChapter.addChild(tItem)
|
trItem.setText(self._colIdx[nwOutline.OBJECT], ", ".join(refs[nwKeyWords.OBJECT_KEY]))
|
||||||
currScene = tItem
|
trItem.setText(self._colIdx[nwOutline.ENTITY], ", ".join(refs[nwKeyWords.ENTITY_KEY]))
|
||||||
|
trItem.setText(self._colIdx[nwOutline.CUSTOM], ", ".join(refs[nwKeyWords.CUSTOM_KEY]))
|
||||||
|
|
||||||
elif tLevel == "H4":
|
self.addTopLevelItem(trItem)
|
||||||
if currScene is None:
|
|
||||||
if currChapter is None:
|
|
||||||
if currTitle is None:
|
|
||||||
self.addTopLevelItem(tItem)
|
|
||||||
else:
|
|
||||||
currTitle.addChild(tItem)
|
|
||||||
else:
|
|
||||||
currChapter.addChild(tItem)
|
|
||||||
else:
|
|
||||||
currScene.addChild(tItem)
|
|
||||||
|
|
||||||
tItem.setExpanded(True)
|
|
||||||
|
|
||||||
self._lastBuild = time()
|
self._lastBuild = time()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def _createTreeItem(self, tHandle, sTitle, novIdx):
|
|
||||||
"""Populate a tree item with all the column values.
|
|
||||||
"""
|
|
||||||
nwItem = self.theProject.tree[tHandle]
|
|
||||||
newItem = QTreeWidgetItem()
|
|
||||||
hIcon = "doc_%s" % novIdx.level.lower()
|
|
||||||
|
|
||||||
hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
|
|
||||||
dIcon = self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, hLevel)
|
|
||||||
|
|
||||||
cC = int(novIdx.charCount)
|
|
||||||
wC = int(novIdx.wordCount)
|
|
||||||
pC = int(novIdx.paraCount)
|
|
||||||
|
|
||||||
newItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title)
|
|
||||||
newItem.setData(self._colIdx[nwOutline.TITLE], Qt.UserRole, tHandle)
|
|
||||||
newItem.setIcon(self._colIdx[nwOutline.TITLE], self.mainTheme.getIcon(hIcon))
|
|
||||||
newItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level)
|
|
||||||
newItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName)
|
|
||||||
newItem.setIcon(self._colIdx[nwOutline.LABEL], dIcon)
|
|
||||||
newItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0"))
|
|
||||||
newItem.setData(self._colIdx[nwOutline.LINE], Qt.UserRole, sTitle)
|
|
||||||
newItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis)
|
|
||||||
newItem.setText(self._colIdx[nwOutline.CCOUNT], f"{cC:n}")
|
|
||||||
newItem.setText(self._colIdx[nwOutline.WCOUNT], f"{wC:n}")
|
|
||||||
newItem.setText(self._colIdx[nwOutline.PCOUNT], f"{pC:n}")
|
|
||||||
newItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
|
|
||||||
newItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
|
|
||||||
newItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
|
|
||||||
|
|
||||||
theRefs = self.theProject.index.getReferences(tHandle, sTitle)
|
|
||||||
newItem.setText(self._colIdx[nwOutline.POV], ", ".join(theRefs[nwKeyWords.POV_KEY]))
|
|
||||||
newItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(theRefs[nwKeyWords.FOCUS_KEY]))
|
|
||||||
newItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY]))
|
|
||||||
newItem.setText(self._colIdx[nwOutline.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY]))
|
|
||||||
newItem.setText(self._colIdx[nwOutline.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY]))
|
|
||||||
newItem.setText(self._colIdx[nwOutline.WORLD], ", ".join(theRefs[nwKeyWords.WORLD_KEY]))
|
|
||||||
newItem.setText(self._colIdx[nwOutline.OBJECT], ", ".join(theRefs[nwKeyWords.OBJECT_KEY]))
|
|
||||||
newItem.setText(self._colIdx[nwOutline.ENTITY], ", ".join(theRefs[nwKeyWords.ENTITY_KEY]))
|
|
||||||
newItem.setText(self._colIdx[nwOutline.CUSTOM], ", ".join(theRefs[nwKeyWords.CUSTOM_KEY]))
|
|
||||||
|
|
||||||
return newItem
|
|
||||||
|
|
||||||
# END Class GuiOutlineTree
|
# END Class GuiOutlineTree
|
||||||
|
|
||||||
|
|
||||||
@@ -725,7 +752,7 @@ class GuiOutlineHeaderMenu(QMenu):
|
|||||||
columnToggled = pyqtSignal(bool, Enum)
|
columnToggled = pyqtSignal(bool, Enum)
|
||||||
|
|
||||||
def __init__(self, theOutline):
|
def __init__(self, theOutline):
|
||||||
QMenu.__init__(self, theOutline)
|
super().__init__(parent=theOutline)
|
||||||
|
|
||||||
self.acceptToggle = True
|
self.acceptToggle = True
|
||||||
|
|
||||||
@@ -776,7 +803,7 @@ class GuiOutlineDetails(QScrollArea):
|
|||||||
itemTagClicked = pyqtSignal(str)
|
itemTagClicked = pyqtSignal(str)
|
||||||
|
|
||||||
def __init__(self, theOutline):
|
def __init__(self, theOutline):
|
||||||
QScrollArea.__init__(self, theOutline)
|
super().__init__(parent=theOutline)
|
||||||
|
|
||||||
logger.debug("Initialising GuiOutlineDetails ...")
|
logger.debug("Initialising GuiOutlineDetails ...")
|
||||||
|
|
||||||
@@ -963,13 +990,13 @@ class GuiOutlineDetails(QScrollArea):
|
|||||||
self.setWidgetResizable(True)
|
self.setWidgetResizable(True)
|
||||||
self.setFrameStyle(QFrame.NoFrame)
|
self.setFrameStyle(QFrame.NoFrame)
|
||||||
|
|
||||||
self.initDetails()
|
self.initSettings()
|
||||||
|
|
||||||
logger.debug("GuiOutlineDetails initialisation complete")
|
logger.debug("GuiOutlineDetails initialisation complete")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def initDetails(self):
|
def initSettings(self):
|
||||||
"""Set or update outline settings.
|
"""Set or update outline settings.
|
||||||
"""
|
"""
|
||||||
# Scroll bars
|
# Scroll bars
|
||||||
@@ -1032,7 +1059,7 @@ class GuiOutlineDetails(QScrollArea):
|
|||||||
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
|
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
|
||||||
self.titleValue.setText(novIdx.title)
|
self.titleValue.setText(novIdx.title)
|
||||||
|
|
||||||
itemStatus, _ = nwItem.getImportStatus()
|
itemStatus, _ = nwItem.getImportStatus(incIcon=False)
|
||||||
|
|
||||||
self.fileValue.setText(nwItem.itemName)
|
self.fileValue.setText(nwItem.itemName)
|
||||||
self.itemValue.setText(itemStatus)
|
self.itemValue.setText(itemStatus)
|
||||||
|
|||||||