Merge branch 'main' into i18n-de_DE-created

This commit is contained in:
Veronica Berglyd Olsen
2022-06-06 13:27:48 +02:00
committed by GitHub
133 changed files with 9088 additions and 6624 deletions
+89
View File
@@ -1,5 +1,94 @@
# novelWriter Changelog # novelWriter Changelog
## Version 1.7 Beta 1 [2022-05-17]
### Release Notes
This is a beta release of the next release version, and is intended for testing purposes. Please be
careful when using this version on live writing projects, and make sure you take frequent backups.
Please check the changelog for an overview of changes. The full release notes will be added to the
final release.
### Detailed Changelog
**Features**
* A simple tool to add Lorem Ipsum placeholder text has been added to the Insert menu. PR #1028.
* Status and Importance flags can now be reorganised in Project Settings. Issue #1035. PR #1040.
* It is now possible to create multiple Root Folders of the same kind. This makes it possible to
add multiple Novel root folders in a project, for instance. Issue #967. PR #1031.
* All documents can now be dragged and dropped anywhere in the project tree. The document layout
may be converted in the process. PR #1031.
* Documents in the project tree can now have other documents as child documents. Issue #1002.
PR #1047.
* Folders in the project tree that are not empty, can now be moved to trash. PR #1048.
* Empty folders are deleted on request, and not moved to trash. Issue #1052. PR #1055.
**User Interface**
* The tabs under the project tree and to the right of the main window have been replaced with a
toolbar on the left hand side. The toolbar has a set of buttons to change view between Project,
Novel and Outline. The three buttons that were available under the project tree have been moved
to the bottom of the new toolbar. Issue #1056. PR #1057.
* When a document changes from a project document to a note, and back again, the Status flag
setting is preserved. Previously, the Importance setting would overwrite it during the
conversion. PR #1030.
* Item labels, Status labels, and other labels on the GUI are now run through a "simplify" function
before being accepted. This functions strips out all whitespaces and consecutive whitespaces and
replace them with single plain whitespaces. This is a safer format to store in XML, and also
makes sure there aren't invisible characters floating around in the labels. PR #1038.
* Due to the changes to how drag and drop works, there are no longer any restrictions on folders
and documents. Only root folders remain restricted in terms of moving. Root folders can only be
reordered with the Move Up and Move Down commands. PR #1047.
* The label for the highlighting of redundant spaces in the Preferences dialog has been updated to
better reflect what it does. Issue #1043. PR #1046.
* The New Project Wizard will now try to check if the path selected for the new project can
actually be used before letting the user proceed to the next page. Issue #1058. PR #1062.
**Internationalisation**
* Dutch translations have been added by Martijn van der Kleijn (@mvdkleijn). PR #1027.
**Functionality**
* Documents that are missing in the project index when a project is opened are automatically
re-indexed. This also handles cases where the cached index is missing. PR #1039.
**Installation and Packaging**
* Python 3.6 is no longer supported. PR #1004.
* Ubuntu 18.04 packages will no longer be released, due to dropping Python 3.6. Issue #1005.
PR #1014.
**Project File Format**
* The item nodes in the content section of the main project XML file have been compacted. It now
consists of a main item node and meta and a name node. All settings have been made attributes of
one of these three nodes, except the item label which is the text value of the name node. The
file format version has been bumped to 1.4. Issue #995. PR #993.
* Both Importance and Status flag values are now saved to the project file. This means if a
document changes layout, the value is no longer lost. PR #1030.
**Code Improvements**
* The linting settings have been updated to select between mutually exclusive options in
pycodestyle. PR #1014.
* The Tokenizer class has been converted to an abstract base class. PR #1026.
* The class handling Status and Importance flags has been completely rewritten. The flags are now
handled using a unique random key as reference rather than relying on the text of the label
itself. This makes it a lot easier to rename them as there is no need to update project items.
PR #1034.
* Many of the decisions regarding where items are allowed to belong has been delegated to the
NWItem class that holds the item. Some is also handled by the NWTree class that holds the project
tree. A new maintenance function in the NWTree class will also ensure that the meta data of an
item is correct and up to date. This is especially important after an item has been moved, but is
also checked when items are initially loaded. PRs #1031 and #1054.
* Item handles are now generated using the standard library random number generator. The new
handles have the same format as the old algorithm, so they are compatible. PR #1044.
----
## Version 1.6.2 [2022-03-20] ## Version 1.6.2 [2022-03-20]
### Release Notes ### Release Notes
+6 -4
View File
@@ -11,13 +11,15 @@
## Translations ## Translations
The default language is English (UK) with English (US) as an option.
* Dutch: Martijn van der Kleijn (@mvdkleijn)
* French: Jan Lüdke (@jyhelle) * French: Jan Lüdke (@jyhelle)
* German: Myian (@heymyian)
* Latin American Spanish: Tommy Marplatt (@tmarplatt)
* Norwegian: Veronica Berglyd Olsen (@vkbo) * Norwegian: Veronica Berglyd Olsen (@vkbo)
* Portuguese: Bruno Meneguello (@bkmeneguello) * Portuguese: Bruno Meneguello (@bkmeneguello)
* Simplified Chinese: Qianzhi Long (@longqzh) * Simplified Chinese: Qianzhi Long (@longqzh)
* Latin American Spanish: Tommy Marplatt (@tmarplatt)
* Dutch: Martijn van der Kleijn (@mvdkleijn)
* German: Myian (@heymyian)
## Libraries ## Libraries
@@ -33,7 +35,7 @@ The following libraries are dependencies of novelWriter:
Some of the assets bundled with novelWriter were adapted from the following sources: Some of the assets bundled with novelWriter were adapted from the following sources:
* Typicons icons by Stephen Hutchings (CC BY-SA 4.0) * Typicons icons by [Stephen Hutchings](https://github.com/stephenhutchings/typicons.font) (CC BY-SA 4.0)
* Tomorrow syntax themes by Chris Kempson (MIT License) * Tomorrow syntax themes by Chris Kempson (MIT License)
* Owl syntax themes by Sarah Drasner (MIT License) * Owl syntax themes by Sarah Drasner (MIT License)
* Solarized themes by Ethan Schoonover, added by @nullbasis (MIT License) * Solarized themes by Ethan Schoonover, added by @nullbasis (MIT License)
+2
View File
@@ -29,6 +29,8 @@ The full documentation is available at
The full credits are listed in The full credits are listed in
[CREDITS.md](https://github.com/vkbo/novelWriter/blob/main/CREDITS.md). [CREDITS.md](https://github.com/vkbo/novelWriter/blob/main/CREDITS.md).
You can also follow novelWriter on Mastodon at [fosstodon.org/@novelwriter](https://fosstodon.org/@novelwriter).
## 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.3+). It is developed on
+2
View File
@@ -39,11 +39,13 @@ too. novelWriter can be run directly from the Python source, installed from the
* Website: https://novelwriter.io * Website: https://novelwriter.io
* Documentation: https://novelwriter.readthedocs.io * Documentation: https://novelwriter.readthedocs.io
* Internationalisation: https://crowdin.com/project/novelwriter
* Source Code: https://github.com/vkbo/novelWriter * Source Code: https://github.com/vkbo/novelWriter
* Source Releases: https://github.com/vkbo/novelWriter/releases * Source Releases: https://github.com/vkbo/novelWriter/releases
* Issue Tracker: https://github.com/vkbo/novelWriter/issues * Issue Tracker: https://github.com/vkbo/novelWriter/issues
* Feature Discussions: https://github.com/vkbo/novelWriter/discussions * Feature Discussions: https://github.com/vkbo/novelWriter/discussions
* PyPi Project: https://pypi.org/project/novelWriter * PyPi Project: https://pypi.org/project/novelWriter
* Social Media: https://fosstodon.org/@novelwriter
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
-2
View File
@@ -59,7 +59,6 @@ The main shorcuts are as follows:
":kbd:`Ctrl`:kbd:`Y`", "Redo latest undo." ":kbd:`Ctrl`:kbd:`Y`", "Redo latest undo."
":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes." ":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes."
":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking." ":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking."
":kbd:`Ctrl`:kbd:`F10`", "Toggle automatic updating of project outline."
":kbd:`Ctrl`:kbd:`Up`", "Move item one step up in the project tree." ":kbd:`Ctrl`:kbd:`Up`", "Move item one step up in the project tree."
":kbd:`Ctrl`:kbd:`Down`", "Move item one step down in the project tree." ":kbd:`Ctrl`:kbd:`Down`", "Move item one step down in the project tree."
":kbd:`Ctrl`:kbd:`Del`", "Delete next word in editor." ":kbd:`Ctrl`:kbd:`Del`", "Delete next word in editor."
@@ -88,7 +87,6 @@ The main shorcuts are as follows:
":kbd:`F7`", "Re-run spell checker." ":kbd:`F7`", "Re-run spell checker."
":kbd:`F8`", "Activate :guilabel:`Focus Mode`, hiding the project tree and document viewer." ":kbd:`F8`", "Activate :guilabel:`Focus Mode`, hiding the project tree and document viewer."
":kbd:`F9`", "Re-build the project index." ":kbd:`F9`", "Re-build the project index."
":kbd:`F10`", "Re-build the project outline."
":kbd:`F11`", "Activate full screen mode." ":kbd:`F11`", "Activate full screen mode."
":kbd:`Shift`:kbd:`F1`", "Open the local user manual (PDF) if it is available." ":kbd:`Shift`:kbd:`F1`", "Open the local user manual (PDF) if it is available."
":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document." ":kbd:`Shift`:kbd:`F3`", "Find previous occurrence of search word in current document."
+17
View File
@@ -2,6 +2,7 @@
The maintenance of translations has been moved to the Crowdin service. The translation strings can The maintenance of translations has been moved to the Crowdin service. The translation strings can
be edited there at the [novelWriter project page](https://crowdin.com/project/novelwriter). be edited there at the [novelWriter project page](https://crowdin.com/project/novelwriter).
However, please read the Translation Guidelines section below.
You can still use the manual approach listed below, and then upload the file through the website's You can still use the manual approach listed below, and then upload the file through the website's
interface. The translation strings for that language will then be updated and queued for approval. interface. The translation strings for that language will then be updated and queued for approval.
@@ -11,6 +12,22 @@ To verify a language file translated through the Crowdin tool, download and extr
[Generate an Updated Translation File](#generate-an-updated-translation-file) below. [Generate an Updated Translation File](#generate-an-updated-translation-file) below.
# Translation Guidelines
When contributing translations, keep the following things in mind.
* For descriptive labels and dialog boxes, make sure you do _not_ change the meaning of the text
when you translate it from English. The user must receive the same instructions or information
regardless of language. This is improtant, otherwise the documentation will be inconsistent with
the user interface and it will become a lot more difficult to handle user issues and questions.
* If you think a label or description is misleading or incomplete, please file an issue report. The
correct way to handle such changes is to change the text in the code first, which will then be
forwarded to _all_ translators such that the GUI is consistent across all languages.
* For very short labels, like button labels. it may be fine to replace the word with a similar
word, but only as long as the user understands what it is supposed to do. Some buttons and tabs
have limited space. If necessary, it is OK to use abbreviations.
# Direct Approach Using Qt Linguist # Direct Approach Using Qt Linguist
Here you will find instructions for translating novelWriter to a new language directly using Qt Here you will find instructions for translating novelWriter to a new language directly using Qt
+611 -602
View File
File diff suppressed because it is too large Load Diff
+615 -606
View File
File diff suppressed because it is too large Load Diff
+615 -606
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -60,9 +60,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-alpha0" __version__ = "1.7-beta1"
__hexversion__ = "0x010700a0" __hexversion__ = "0x010700b1"
__date__ = "2022-02-20" __date__ = "2022-05-17"
__status__ = "Stable" __status__ = "Stable"
__domain__ = "novelwriter.io" __domain__ = "novelwriter.io"
__url__ = "https://novelwriter.io" __url__ = "https://novelwriter.io"
+100 -100
View File
@@ -2,104 +2,104 @@
"Synopsis": "Synopsis", "Synopsis": "Synopsis",
"Comment": "Opmerking", "Comment": "Opmerking",
"Notes": "Notities", "Notes": "Notities",
"0": "Nul", "0": "nul",
"1": "Één", "1": "één",
"2": "Twee", "2": "twee",
"3": "Drie", "3": "drie",
"4": "Vier", "4": "vier",
"5": "Vijf", "5": "vijf",
"6": "Zes", "6": "zes",
"7": "Zeven", "7": "zeven",
"8": "Acht", "8": "acht",
"9": "Negen", "9": "negen",
"10": "Tien", "10": "tien",
"11": "Elf", "11": "elf",
"12": "Twaalf", "12": "twaalf",
"13": "Dertien", "13": "dertien",
"14": "Veertien", "14": "veertien",
"15": "Vijftien", "15": "vijftien",
"16": "Zestien", "16": "zestien",
"17": "Zeventien", "17": "zeventien",
"18": "Achttien", "18": "achttien",
"19": "Negentien", "19": "negentien",
"20": "Twintig", "20": "twintig",
"21": "Eenentwintig", "21": "eenentwintig",
"22": "Tweeentwintig", "22": "tweeëntwintig",
"23": "Drieentwintig", "23": "drieëntwintig",
"24": "Vierentwintig", "24": "vierentwintig",
"25": "Vijfentwintig", "25": "vijfentwintig",
"26": "Zesentwintig", "26": "zesentwintig",
"27": "Zevenentwintig", "27": "zevenentwintig",
"28": "Achtentwintig", "28": "achtentwintig",
"29": "Negenentwintig", "29": "negenentwintig",
"30": "Dertig", "30": "dertig",
"31": "Eenendertig", "31": "eenendertig",
"32": "Tweeendertig", "32": "tweeëndertig",
"33": "Drieendertig", "33": "drieëndertig",
"34": "Vierendertig", "34": "vierendertig",
"35": "Vijfendertig", "35": "vijfendertig",
"36": "Zesendertig", "36": "zesendertig",
"37": "Zevenendertig", "37": "zevenendertig",
"38": "Achtendertig", "38": "achtendertig",
"39": "Negenendertig", "39": "negenendertig",
"40": "Veertig", "40": "veertig",
"41": "Eenenveertig", "41": "eenenveertig",
"42": "Tweeënveertig", "42": "tweeënveertig",
"43": "Drieenveertig", "43": "drieënveertig",
"44": "Vierenveertig", "44": "vierenveertig",
"45": "Vijfenveertig", "45": "vijfenveertig",
"46": "Zesenveertig", "46": "zesenveertig",
"47": "Zevenenveertig", "47": "zevenenveertig",
"48": "Achtenveertig", "48": "achtenveertig",
"49": "Negenenveertig", "49": "negenenveertig",
"50": "Vijftig", "50": "vijftig",
"51": "Eenenvijftig", "51": "eenenvijftig",
"52": "Tweeenvijftig", "52": "tweeënvijftig",
"53": "Drieenvijftig", "53": "drieënvijftig",
"54": "Vierenvijftig", "54": "vierenvijftig",
"55": "Vijfenvijftig", "55": "vijfenvijftig",
"56": "Zesenvijftig", "56": "zesenvijftig",
"57": "Zevenenvijftig", "57": "zevenenvijftig",
"58": "Achtenvijftig", "58": "achtenvijftig",
"59": "Negenenvijftig", "59": "negenenvijftig",
"60": "Zestig", "60": "zestig",
"61": "Eenenzestig", "61": "eenenzestig",
"62": "Tweeenzestig", "62": "tweeënzestig",
"63": "Drieenzestig", "63": "drieënzestig",
"64": "Vierenzestig", "64": "vierenzestig",
"65": "Vijfenzestig", "65": "vijfenzestig",
"66": "Zesenzestig", "66": "zesenzestig",
"67": "Zevenenzestig", "67": "zevenenzestig",
"68": "Achtenzestig", "68": "achtenzestig",
"69": "Negenenzestig", "69": "negenenzestig",
"70": "Zeventig", "70": "zeventig",
"71": "Eenenzeventig", "71": "eenenzeventig",
"72": "Tweeenzeventig", "72": "tweeënzeventig",
"73": "Drieenzeventig", "73": "drieënzeventig",
"74": "Vierenzeventig", "74": "vierenzeventig",
"75": "Vijfenzeventig", "75": "vijfenzeventig",
"76": "Zesenzeventig", "76": "zesenzeventig",
"77": "Zevenenzeventig", "77": "zevenenzeventig",
"78": "Achtenzeventig", "78": "achtenzeventig",
"79": "Negenenzeventig", "79": "negenenzeventig",
"80": "Tachtig", "80": "tachtig",
"81": "Eenentachtig", "81": "eenentachtig",
"82": "Tweeentachtig", "82": "tweeëntachtig",
"83": "Drieentachtig", "83": "drieëntachtig",
"84": "Vierentachtig", "84": "vierentachtig",
"85": "Vijfentachtig", "85": "vijfentachtig",
"86": "Zesentachtig", "86": "zesentachtig",
"87": "Zevenentachtig", "87": "zevenentachtig",
"88": "Achtentachtig", "88": "achtentachtig",
"89": "Negenentachtig", "89": "negenentachtig",
"90": "Negentig", "90": "negentig",
"91": "Eenennegentig", "91": "eenennegentig",
"92": "Tweeennegentig", "92": "tweeënnegentig",
"93": "Drieennegentig", "93": "drieënnegentig",
"94": "Vierennegentig", "94": "vierennegentig",
"95": "Vijfennegentig", "95": "vijfennegentig",
"96": "Zesennegentig", "96": "zesennegentig",
"97": "Zevenennegentig", "97": "zevenennegentig",
"98": "Achtennegentig", "98": "achtennegentig",
"99": "Negenennegentig" "99": "negenennegentig"
} }
@@ -42,16 +42,20 @@ doc_h2 = mixed_heading2.svg
doc_h3 = mixed_heading3.svg doc_h3 = mixed_heading3.svg
doc_h4 = mixed_heading4.svg doc_h4 = mixed_heading4.svg
done = typ_input-checked.svg done = typ_input-checked.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 hash = typ_hash.svg
maximise = typ_arrow-maximise.svg maximise = typ_arrow-maximise.svg
menu = typ_th-menu.svg
minimise = typ_arrow-minimise.svg minimise = typ_arrow-minimise.svg
proj_chapter = mixed_document-chapter.svg proj_chapter = mixed_document-chapter.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_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
@@ -74,3 +78,8 @@ 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
up = typ_chevron-up.svg
view_build = typ_export.svg
view_editor = mixed_edit.svg
view_novel = typ_book-grey.svg
view_outline = typ_puzzle-outline.svg
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg7536"
sodipodi:docname="mixed_edit.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
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/">
<sodipodi:namedview
id="namedview1259"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="34.625"
inkscape:cx="8.7364621"
inkscape:cy="11.98556"
inkscape:window-width="2560"
inkscape:window-height="1330"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg7536" />
<metadata
id="metadata7542">
<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="defs7540" />
<path
id="path7534"
style="display:inline;fill:#aeaeae;fill-opacity:1;stroke-width:1.10527"
d="M 19.160156 0 C 18.73463 0 18.312122 0.160533 17.988281 0.484375 L 14.052734 4.421875 L 4.1054688 4.421875 C 3.4953635 4.421875 3 4.9141801 3 5.5253906 L 3 17.037109 L 3 18.429688 L 3 22.894531 C 3 23.505741 3.4953635 24 4.1054688 24 L 18.474609 24 C 19.084713 24 19.578125 23.505741 19.578125 22.894531 L 19.578125 18.429688 L 19.578125 17.037109 L 19.578125 9.9472656 L 23.515625 6.0117188 C 23.839466 5.6878766 24 5.2623116 24 4.8378906 C 24 4.4134695 23.839466 3.9918109 23.515625 3.6679688 L 20.332031 0.484375 C 20.008189 0.160533 19.584576 0 19.160156 0 z M 19.160156 2.4394531 L 21.560547 4.8398438 L 20.130859 6.2695312 L 17.730469 3.8691406 L 19.160156 2.4394531 z M 16.949219 4.6503906 L 19.349609 7.0507812 L 12.394531 14.005859 L 9.9941406 11.605469 L 16.949219 4.6503906 z M 5.2109375 6.6308594 L 11.841797 6.6308594 L 8.328125 10.146484 C 8.0042828 10.470327 7.7997073 11.043101 7.6328125 11.570312 C 7.4559704 12.122943 7.421875 12.737735 7.421875 13.195312 L 7.421875 16.578125 L 10.804688 16.578125 C 11.262267 16.578125 12.02928 16.467813 12.541016 16.257812 C 13.053858 16.047812 13.530778 15.874623 13.853516 15.550781 L 17.369141 12.158203 L 17.369141 17.037109 L 17.369141 18.429688 L 17.369141 21.789062 L 5.2109375 21.789062 L 5.2109375 18.429688 L 5.2109375 17.037109 L 5.2109375 6.6308594 z M 9.5644531 12.525391 L 11.505859 14.435547 L 9.6308594 14.369141 L 9.5644531 12.525391 z " />
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg8707">
<metadata
id="metadata8713">
<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="defs8711" />
<path
d="M 20,0 H 5.3333333 C 4.98,0 4.64,0.14 4.3906667,0.3906667 l -4,4 -0.076,0.082667 c -0.1853334,0.22 -0.3,0.4973334 -0.3133334,0.8 L 0,5.344 V 20 c 0,2.205333 1.7946667,4 4,4 h 12 c 1.738667,0 3.221333,-1.114667 3.772,-2.666667 h 0.894667 c 1.869333,0 3.333333,-1.756 3.333333,-4 V 4 C 24,1.7946667 22.205333,0 20,0 Z M 4,21.333333 C 3.2653333,21.333333 2.6666667,20.736 2.6666667,20 V 6.6666667 H 5.3333333 V 21.333333 Z M 17.333333,20 c 0,0.736 -0.598666,1.333333 -1.333333,1.333333 H 6.6666667 V 6.6666667 H 16 C 16.734667,6.6666667 17.333333,7.264 17.333333,8 Z m 4,-2.666667 c 0,0.826667 -0.432,1.333334 -0.666666,1.333334 H 20 V 8 C 20,5.7946667 18.205333,4 16,4 H 4.552 L 5.8853333,2.6666667 H 20 C 20.734667,2.6666667 21.333333,3.264 21.333333,4 Z"
id="path8705"
style="stroke-width:1.33333;fill:#aeaeae;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8980"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata8986">
<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="defs8984" />
<path
style="fill:#6699cc;fill-opacity:1;stroke-width:1.28574"
id="path8978"
d="m 3.7531209,6.4345258 c -1.004161,1.002875 -1.004161,2.63319 0,3.6360652 l 8.2467181,8.248004 8.246719,-8.248004 C 20.749281,9.5691528 21,8.9108558 21,8.2525578 c 0,-0.658297 -0.250719,-1.316595 -0.753442,-1.818032 -1.004161,-1.004161 -2.631904,-1.004161 -3.636065,0 L 11.999839,11.043894 7.3891859,6.4345258 c -1.004161,-1.004161 -2.631905,-1.004161 -3.636065,0 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8980"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata8986">
<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="defs8984" />
<path
style="fill:#6699cc;fill-opacity:1;stroke-width:1.28574"
id="path8978"
d="m 20.246879,17.565474 c 1.004161,-1.002875 1.004161,-2.63319 0,-3.636065 L 12.000161,5.6814052 3.7534421,13.929409 c -0.502723,0.501438 -0.753442,1.159735 -0.753442,1.818033 0,0.658297 0.250719,1.316595 0.753442,1.818032 1.004161,1.004161 2.631904,1.004161 3.636065,0 l 4.6106539,-4.609368 4.610653,4.609368 c 1.004161,1.004161 2.631905,1.004161 3.636065,0 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg1169"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1173" />
<path
d="M 6.8888889,16.888889 V 17.5 c 2.0667778,-3.150889 4.4000001,-4.831444 7.3333331,-4.888889 v 3.666666 c 0,0.673444 0.624556,1.222223 1.397,1.222223 0.444889,0 0.825,-0.193112 1.079223,-0.477889 C 19.061,14.542222 24,9.5555555 24,9.5555555 c 0,0 -4.939,-4.9891115 -7.302778,-7.5007781 -0.254222,-0.2505555 -0.633111,-0.4436667 -1.078,-0.4436667 -0.772444,0 -1.397,0.5463333 -1.397,1.2222222 v 3.6666667 c -5.6955553,0 -7.3333331,5.9534434 -7.3333331,10.3888894 z m -3.6666667,5.5 H 20.333333 c 0.675889,0 1.222222,-0.547557 1.222222,-1.222223 V 13.77711 c -0.811555,0.826222 -1.66711,1.702555 -2.444444,2.501889 v 3.665444 H 4.4444445 V 5.2777775 H 13 V 2.833333 H 3.2222222 C 2.5463333,2.833333 2,3.3808886 2,4.0555552 V 21.166666 c 0,0.674666 0.5463333,1.222223 1.2222222,1.222223 z"
id="path1167"
style="stroke-width:1.22222;fill:#aeaeae;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg11920">
<metadata
id="metadata11926">
<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="defs11924" />
<path
d="M 22.461538,10.769231 V 8.615385 C 22.461538,6.24 20.52923,4.3076923 18.153846,4.3076923 H 17.219692 C 17.046153,1.8683077 14.816,0 12,0 9.1839995,0 6.9538461,1.8683077 6.7803077,4.3076923 H 5.8461538 C 3.4707692,4.3076923 1.5384615,6.24 1.5384615,8.615385 v 2.153846 c 0,1.245538 0.6326154,2.273231 1.5938462,2.764307 C 2.6916923,13.764923 2.3101538,14.110769 2.032,14.548923 1.7095384,15.058462 1.5384615,15.666462 1.5384615,16.307692 v 3.384616 C 1.5384615,22.067692 3.4707692,24 5.8461538,24 H 9.2307695 C 10.432,24 11.484308,23.388308 11.990154,22.401231 l 0.04431,0.08123 C 12.569847,23.432615 13.592616,24 14.76923,24 h 3.384616 c 2.375384,0 4.307692,-1.932308 4.307692,-4.307692 v -3.384616 c 0,-1.246769 -0.633845,-2.275692 -1.596308,-2.765538 0.955077,-0.505846 1.596308,-1.545846 1.596308,-2.772923 z m -2.461539,8.923077 c 0,1.015384 -0.830769,1.846154 -1.846153,1.846154 H 14.76923 c -0.438154,0 -0.891077,-0.265847 -0.481231,-0.94277 0.30277,-0.344615 0.481231,-0.761846 0.481231,-1.211077 0,-1.190153 -1.239384,-2.153846 -2.769231,-2.153846 -1.529846,0 -2.7692295,0.963693 -2.7692295,2.153846 0,0.369231 0.116923,0.708923 0.313846,1.012923 0.6239995,0.828308 0.167384,1.140924 -0.313846,1.140924 H 5.8461538 C 4.8307692,21.538462 4,20.707692 4,19.692308 v -3.384616 c 0,-0.317538 0.1390769,-0.64123 0.4726153,-0.64123 0.128,0 0.2818462,0.048 0.4701539,0.16 0.3446154,0.302769 0.7630769,0.48123 1.2110769,0.48123 1.1889231,0 2.1538462,-1.240615 2.1538462,-2.76923 0,-1.528616 -0.9649231,-2.769231 -2.1538462,-2.769231 -0.3692308,0 -0.7089231,0.116923 -1.0116923,0.313846 C 4.8504615,11.293538 4.6227692,11.382154 4.4504615,11.382154 4.1304615,11.382154 4,11.080615 4,10.769231 V 8.615385 C 4,7.6 4.8307692,6.7692308 5.8461538,6.7692308 h 3.3846157 c 0.48123,0 0.9378465,-0.3126154 0.299077,-1.1409231 -0.182154,-0.304 -0.299077,-0.6436923 -0.299077,-1.0129231 0,-1.1901538 1.2393835,-2.1538461 2.7692305,-2.1538461 1.529847,0 2.76923,0.9636923 2.76923,2.1538461 0,0.4492308 -0.17846,0.8664616 -0.48123,1.2110769 -0.409846,0.6769231 0.04308,0.9427693 0.48123,0.9427693 h 3.384616 C 19.16923,6.7692308 20,7.6 20,8.615385 v 2.153846 c 0,0.317538 -0.139076,0.641231 -0.472616,0.641231 -0.128,0 -0.281846,-0.048 -0.470154,-0.16 -0.344615,-0.30277 -0.763077,-0.481231 -1.211077,-0.481231 -1.188923,0 -2.153846,1.240615 -2.153846,2.769231 0,1.528615 0.964923,2.76923 2.153846,2.76923 0.369231,0 0.708923,-0.116923 1.011693,-0.313846 0.291693,-0.210461 0.519384,-0.300308 0.691692,-0.299077 0.318769,0 0.449232,0.301539 0.449232,0.612923 v 3.384616 z M 4,12.546462 c 0.1427692,0.03938 0.2904615,0.06646 0.4492307,0.06646 0.4209231,0 0.8406154,-0.146461 1.2775385,-0.448 l 0.084923,-0.05046 C 5.9310769,12.036923 6.0430769,12 6.1538461,12 c 0.4356923,0 0.9230769,0.658462 0.9230769,1.538462 0,0.88 -0.4873846,1.538461 -0.9230769,1.538461 -0.1329231,0 -0.2670769,-0.05908 -0.3987692,-0.174769 l -0.176,-0.128 C 5.2086153,14.548923 4.8356923,14.435692 4.4726153,14.435692 4.3076923,14.435692 4.1513846,14.464002 4,14.508312 Z M 19.550769,14.464 c -0.420922,0 -0.840616,0.146462 -1.277539,0.448 l -0.08492,0.05046 c -0.119384,0.07754 -0.231383,0.114461 -0.340923,0.114461 -0.435692,0 -0.923077,-0.658461 -0.923077,-1.538461 0,-0.88 0.487385,-1.538462 0.923077,-1.538462 0.132924,0 0.267077,0.05908 0.39877,0.174769 l 0.175999,0.128 c 0.371693,0.225231 0.743385,0.338462 1.106462,0.338462 0.167384,0 0.322461,-0.03077 0.472616,-0.07631 v 1.965539 c -0.144,-0.03938 -0.291693,-0.06646 -0.450462,-0.06646 z m -8.54523,7.074462 C 11.129847,21.112615 11.12,20.534154 10.630154,19.820308 l -0.05415,-0.09108 c -0.07631,-0.120616 -0.115692,-0.232616 -0.115692,-0.343385 0,-0.435692 0.657231,-0.923077 1.538462,-0.923077 0.88123,0 1.538461,0.487385 1.538461,0.923077 0,0.132923 -0.05908,0.267077 -0.176,0.4 l -0.128,0.174769 c -0.4,0.660923 -0.382769,1.204923 -0.270769,1.580308 h -1.956923 z"
id="path11918"
style="stroke-width:1.23077;fill:#aeaeae;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg4455"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs4459" />
<path
d="M19 17h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2zM19 10h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2zM19 3h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2z"
id="path4453"
style="fill:#6699cc;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 571 B

@@ -42,16 +42,20 @@ doc_h2 = mixed_heading2.svg
doc_h3 = mixed_heading3.svg doc_h3 = mixed_heading3.svg
doc_h4 = mixed_heading4.svg doc_h4 = mixed_heading4.svg
done = typ_input-checked.svg done = typ_input-checked.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 hash = typ_hash.svg
maximise = typ_arrow-maximise.svg maximise = typ_arrow-maximise.svg
menu = typ_th-menu.svg
minimise = typ_arrow-minimise.svg minimise = typ_arrow-minimise.svg
proj_chapter = mixed_document-chapter.svg proj_chapter = mixed_document-chapter.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_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
@@ -74,3 +78,8 @@ 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
up = typ_chevron-up.svg
view_build = typ_export.svg
view_editor = mixed_edit.svg
view_novel = typ_book-grey.svg
view_outline = typ_puzzle-outline.svg
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg7536"
sodipodi:docname="mixed_edit.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
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/">
<sodipodi:namedview
id="namedview1259"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="34.625"
inkscape:cx="3.1913357"
inkscape:cy="11.98556"
inkscape:window-width="2560"
inkscape:window-height="1330"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg7536" />
<metadata
id="metadata7542">
<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="defs7540" />
<path
id="path7534"
style="display:inline;fill:#000000;fill-opacity:0.72156864;stroke-width:1.10527"
d="M 19.160156 0 C 18.73463 0 18.312122 0.160533 17.988281 0.484375 L 14.052734 4.421875 L 4.1054688 4.421875 C 3.4953635 4.421875 3 4.9141801 3 5.5253906 L 3 17.037109 L 3 18.429688 L 3 22.894531 C 3 23.505741 3.4953635 24 4.1054688 24 L 18.474609 24 C 19.084713 24 19.578125 23.505741 19.578125 22.894531 L 19.578125 18.429688 L 19.578125 17.037109 L 19.578125 9.9472656 L 23.515625 6.0117188 C 23.839466 5.6878766 24 5.2623116 24 4.8378906 C 24 4.4134695 23.839466 3.9918109 23.515625 3.6679688 L 20.332031 0.484375 C 20.008189 0.160533 19.584576 0 19.160156 0 z M 19.160156 2.4394531 L 21.560547 4.8398438 L 20.130859 6.2695312 L 17.730469 3.8691406 L 19.160156 2.4394531 z M 16.949219 4.6503906 L 19.349609 7.0507812 L 12.394531 14.005859 L 9.9941406 11.605469 L 16.949219 4.6503906 z M 5.2109375 6.6308594 L 11.841797 6.6308594 L 8.328125 10.146484 C 8.0042828 10.470327 7.7997073 11.043101 7.6328125 11.570312 C 7.4559704 12.122943 7.421875 12.737735 7.421875 13.195312 L 7.421875 16.578125 L 10.804688 16.578125 C 11.262267 16.578125 12.02928 16.467813 12.541016 16.257812 C 13.053858 16.047812 13.530778 15.874623 13.853516 15.550781 L 17.369141 12.158203 L 17.369141 17.037109 L 17.369141 18.429688 L 17.369141 21.789062 L 5.2109375 21.789062 L 5.2109375 18.429688 L 5.2109375 17.037109 L 5.2109375 6.6308594 z M 9.5644531 12.525391 L 11.505859 14.435547 L 9.6308594 14.369141 L 9.5644531 12.525391 z " />
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg8707">
<metadata
id="metadata8713">
<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="defs8711" />
<path
d="M 20,0 H 5.3333333 C 4.98,0 4.64,0.14 4.3906667,0.3906667 l -4,4 -0.076,0.082667 c -0.1853334,0.22 -0.3,0.4973334 -0.3133334,0.8 L 0,5.344 V 20 c 0,2.205333 1.7946667,4 4,4 h 12 c 1.738667,0 3.221333,-1.114667 3.772,-2.666667 h 0.894667 c 1.869333,0 3.333333,-1.756 3.333333,-4 V 4 C 24,1.7946667 22.205333,0 20,0 Z M 4,21.333333 C 3.2653333,21.333333 2.6666667,20.736 2.6666667,20 V 6.6666667 H 5.3333333 V 21.333333 Z M 17.333333,20 c 0,0.736 -0.598666,1.333333 -1.333333,1.333333 H 6.6666667 V 6.6666667 H 16 C 16.734667,6.6666667 17.333333,7.264 17.333333,8 Z m 4,-2.666667 c 0,0.826667 -0.432,1.333334 -0.666666,1.333334 H 20 V 8 C 20,5.7946667 18.205333,4 16,4 H 4.552 L 5.8853333,2.6666667 H 20 C 20.734667,2.6666667 21.333333,3.264 21.333333,4 Z"
id="path8705"
style="stroke-width:1.33333;fill:#000000;fill-opacity:0.72156864" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8980"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata8986">
<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="defs8984" />
<path
style="fill:#4271ae;fill-opacity:1;stroke-width:1.28574"
id="path8978"
d="m 3.7531209,6.4345258 c -1.004161,1.002875 -1.004161,2.63319 0,3.6360652 l 8.2467181,8.248004 8.246719,-8.248004 C 20.749281,9.5691528 21,8.9108558 21,8.2525578 c 0,-0.658297 -0.250719,-1.316595 -0.753442,-1.818032 -1.004161,-1.004161 -2.631904,-1.004161 -3.636065,0 L 11.999839,11.043894 7.3891859,6.4345258 c -1.004161,-1.004161 -2.631905,-1.004161 -3.636065,0 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8980"
viewBox="0 0 24 24"
height="24"
width="24"
version="1.2">
<metadata
id="metadata8986">
<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="defs8984" />
<path
style="fill:#4271ae;fill-opacity:1;stroke-width:1.28574"
id="path8978"
d="m 20.246879,17.565474 c 1.004161,-1.002875 1.004161,-2.63319 0,-3.636065 L 12.000161,5.6814052 3.7534421,13.929409 c -0.502723,0.501438 -0.753442,1.159735 -0.753442,1.818033 0,0.658297 0.250719,1.316595 0.753442,1.818032 1.004161,1.004161 2.631904,1.004161 3.636065,0 l 4.6106539,-4.609368 4.610653,4.609368 c 1.004161,1.004161 2.631905,1.004161 3.636065,0 z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg1169"
sodipodi:docname="typ_export.svg"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview2676"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
showgrid="false"
inkscape:zoom="34.625"
inkscape:cx="6.4259928"
inkscape:cy="11.98556"
inkscape:window-width="2560"
inkscape:window-height="1330"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1169" />
<defs
id="defs1173" />
<path
d="M 6.8888889,16.888889 V 17.5 c 2.0667778,-3.150889 4.4000001,-4.831444 7.3333331,-4.888889 v 3.666666 c 0,0.673444 0.624556,1.222223 1.397,1.222223 0.444889,0 0.825,-0.193112 1.079223,-0.477889 C 19.061,14.542222 24,9.5555555 24,9.5555555 c 0,0 -4.939,-4.9891115 -7.302778,-7.5007781 -0.254222,-0.2505555 -0.633111,-0.4436667 -1.078,-0.4436667 -0.772444,0 -1.397,0.5463333 -1.397,1.2222222 v 3.6666667 c -5.6955553,0 -7.3333331,5.9534434 -7.3333331,10.3888894 z m -3.6666667,5.5 H 20.333333 c 0.675889,0 1.222222,-0.547557 1.222222,-1.222223 V 13.77711 c -0.811555,0.826222 -1.66711,1.702555 -2.444444,2.501889 v 3.665444 H 4.4444445 V 5.2777775 H 13 V 2.833333 H 3.2222222 C 2.5463333,2.833333 2,3.3808886 2,4.0555552 V 21.166666 c 0,0.674666 0.5463333,1.222223 1.2222222,1.222223 z"
id="path1167"
style="stroke-width:1.22222;fill:#000000;fill-opacity:0.72156864" />
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg11920">
<metadata
id="metadata11926">
<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="defs11924" />
<path
d="M 22.461538,10.769231 V 8.615385 C 22.461538,6.24 20.52923,4.3076923 18.153846,4.3076923 H 17.219692 C 17.046153,1.8683077 14.816,0 12,0 9.1839995,0 6.9538461,1.8683077 6.7803077,4.3076923 H 5.8461538 C 3.4707692,4.3076923 1.5384615,6.24 1.5384615,8.615385 v 2.153846 c 0,1.245538 0.6326154,2.273231 1.5938462,2.764307 C 2.6916923,13.764923 2.3101538,14.110769 2.032,14.548923 1.7095384,15.058462 1.5384615,15.666462 1.5384615,16.307692 v 3.384616 C 1.5384615,22.067692 3.4707692,24 5.8461538,24 H 9.2307695 C 10.432,24 11.484308,23.388308 11.990154,22.401231 l 0.04431,0.08123 C 12.569847,23.432615 13.592616,24 14.76923,24 h 3.384616 c 2.375384,0 4.307692,-1.932308 4.307692,-4.307692 v -3.384616 c 0,-1.246769 -0.633845,-2.275692 -1.596308,-2.765538 0.955077,-0.505846 1.596308,-1.545846 1.596308,-2.772923 z m -2.461539,8.923077 c 0,1.015384 -0.830769,1.846154 -1.846153,1.846154 H 14.76923 c -0.438154,0 -0.891077,-0.265847 -0.481231,-0.94277 0.30277,-0.344615 0.481231,-0.761846 0.481231,-1.211077 0,-1.190153 -1.239384,-2.153846 -2.769231,-2.153846 -1.529846,0 -2.7692295,0.963693 -2.7692295,2.153846 0,0.369231 0.116923,0.708923 0.313846,1.012923 0.6239995,0.828308 0.167384,1.140924 -0.313846,1.140924 H 5.8461538 C 4.8307692,21.538462 4,20.707692 4,19.692308 v -3.384616 c 0,-0.317538 0.1390769,-0.64123 0.4726153,-0.64123 0.128,0 0.2818462,0.048 0.4701539,0.16 0.3446154,0.302769 0.7630769,0.48123 1.2110769,0.48123 1.1889231,0 2.1538462,-1.240615 2.1538462,-2.76923 0,-1.528616 -0.9649231,-2.769231 -2.1538462,-2.769231 -0.3692308,0 -0.7089231,0.116923 -1.0116923,0.313846 C 4.8504615,11.293538 4.6227692,11.382154 4.4504615,11.382154 4.1304615,11.382154 4,11.080615 4,10.769231 V 8.615385 C 4,7.6 4.8307692,6.7692308 5.8461538,6.7692308 h 3.3846157 c 0.48123,0 0.9378465,-0.3126154 0.299077,-1.1409231 -0.182154,-0.304 -0.299077,-0.6436923 -0.299077,-1.0129231 0,-1.1901538 1.2393835,-2.1538461 2.7692305,-2.1538461 1.529847,0 2.76923,0.9636923 2.76923,2.1538461 0,0.4492308 -0.17846,0.8664616 -0.48123,1.2110769 -0.409846,0.6769231 0.04308,0.9427693 0.48123,0.9427693 h 3.384616 C 19.16923,6.7692308 20,7.6 20,8.615385 v 2.153846 c 0,0.317538 -0.139076,0.641231 -0.472616,0.641231 -0.128,0 -0.281846,-0.048 -0.470154,-0.16 -0.344615,-0.30277 -0.763077,-0.481231 -1.211077,-0.481231 -1.188923,0 -2.153846,1.240615 -2.153846,2.769231 0,1.528615 0.964923,2.76923 2.153846,2.76923 0.369231,0 0.708923,-0.116923 1.011693,-0.313846 0.291693,-0.210461 0.519384,-0.300308 0.691692,-0.299077 0.318769,0 0.449232,0.301539 0.449232,0.612923 v 3.384616 z M 4,12.546462 c 0.1427692,0.03938 0.2904615,0.06646 0.4492307,0.06646 0.4209231,0 0.8406154,-0.146461 1.2775385,-0.448 l 0.084923,-0.05046 C 5.9310769,12.036923 6.0430769,12 6.1538461,12 c 0.4356923,0 0.9230769,0.658462 0.9230769,1.538462 0,0.88 -0.4873846,1.538461 -0.9230769,1.538461 -0.1329231,0 -0.2670769,-0.05908 -0.3987692,-0.174769 l -0.176,-0.128 C 5.2086153,14.548923 4.8356923,14.435692 4.4726153,14.435692 4.3076923,14.435692 4.1513846,14.464002 4,14.508312 Z M 19.550769,14.464 c -0.420922,0 -0.840616,0.146462 -1.277539,0.448 l -0.08492,0.05046 c -0.119384,0.07754 -0.231383,0.114461 -0.340923,0.114461 -0.435692,0 -0.923077,-0.658461 -0.923077,-1.538461 0,-0.88 0.487385,-1.538462 0.923077,-1.538462 0.132924,0 0.267077,0.05908 0.39877,0.174769 l 0.175999,0.128 c 0.371693,0.225231 0.743385,0.338462 1.106462,0.338462 0.167384,0 0.322461,-0.03077 0.472616,-0.07631 v 1.965539 c -0.144,-0.03938 -0.291693,-0.06646 -0.450462,-0.06646 z m -8.54523,7.074462 C 11.129847,21.112615 11.12,20.534154 10.630154,19.820308 l -0.05415,-0.09108 c -0.07631,-0.120616 -0.115692,-0.232616 -0.115692,-0.343385 0,-0.435692 0.657231,-0.923077 1.538462,-0.923077 0.88123,0 1.538461,0.487385 1.538461,0.923077 0,0.132923 -0.05908,0.267077 -0.176,0.4 l -0.128,0.174769 c -0.4,0.660923 -0.382769,1.204923 -0.270769,1.580308 h -1.956923 z"
id="path11918"
style="stroke-width:1.23077;fill:#000000;fill-opacity:0.72156864" />
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg4455"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs4459" />
<path
d="M19 17h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2zM19 10h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2zM19 3h-14c-1.103 0-2 .897-2 2s.897 2 2 2h14c1.103 0 2-.897 2-2s-.897-2-2-2z"
id="path4453"
style="fill:#4271ae;fill-opacity:1" />
</svg>

After

Width:  |  Height:  |  Size: 571 B

+7 -47
View File
@@ -2,56 +2,16 @@
<html> <html>
<body> <body>
<h2>Release Notes for 1.6</h2> <h2>Release Notes for 1.7 Beta 1</h2>
<p><i>Released on 20 February 2022</i></p> <p><i>Released on 17 May 2022</i></p>
<p>This release does not introduce any major new features, but is instead a collection of minor <p>This is a beta release of the next release version, and is intended for testing purposes. Please
improvements and tweaks based on user requests. There are also a number of changes under the hood be careful when using this version on live writing projects, and make sure you take frequent
to improve the structure and performance of novelWriter.</p> backups.</p>
<p><u>Some key improvements to the user interface are:</u></p> <p>Please check the changelog for an overview of changes. The full release notes will be added to
<p>&#10003; The max text width setting in Preferences now also applies to the document viewer, and the final release.</p>
the setting itself on the Preference dialog has been simplified a bit.</p>
<p>&#10003; When text is selected in the document editor, the number of words selected is displayed
in the editor's footer area.</p>
<p>&#10003; The search tool in the document editor now shows the number of results in the
document.</p>
<p>&#10003; The Enter and Ctrl+O keyboard shortcuts should now work the same way in all tree
views.</p>
<p>&#10003; It is now possible to set a blank section title format on the Build Novel Project tool
and get empty paragraphs in the output. Previously, a blank format would just remove the section
break entirely. This change allows the user to define hard and soft scene breaks using level three
and four headings. The scene and section titles can be hidden completely with two new switches
added to the user interface.</p>
<p><u>Other feature changes include:</u></p>
<p>&#10003; The project index is now automatically rebuilt in the event it is empty or incomplete
when the project is opened.</p>
<p>&#10003; The user can now add their own syntax and GUI theme files in the app folder in their
user area on the host operating system. Where the custom files must be added is described in the
documentation.</p>
<p>&#10003; A Windows installer is yet again provided for novelWriter. If you have novelWriter
installed using another method, make sure you uninstall it properly first as the two methods are
not compatible.</p>
<p>&#10003; Release versions for Ubuntu 21.04 have been dropped, and added for the upcoming Ubuntu
22.04.</p>
<p>&#10003; Most translations have been updated. A Dutch translation is in the works.</p>
<p><i>See also the <a href="https://github.com/vkbo/novelWriter/releases">Releases</a> page.</i></p> <p><i>See also the <a href="https://github.com/vkbo/novelWriter/releases">Releases</a> page.</i></p>
<h2>Patch Notes</h2>
<h3>Patch 1.6.1 &ndash; 16 March 2022</h3>
<p>This is a bugfix and patch release that fixes two recursion/loop issues. One would potentially
cause a crash if the window was resized rapidly, and one would cause a hang with certain search
parameters in the editor's search box. The Latin American Spanish translation has also been
updated.</p>
<h3>Patch 1.6.2 &ndash; 20 March 2022</h3>
<p>This is a bugfix release that fixes a couple of minor issues. Projects containing one or more
empty documents would trigger a rebuild of the index each time the project was opened. This has now
been fixed. Another fix resolves an error message being written to the console logging output when
a new document was created. Both errors were harmless.</p>
</body> </body>
</html> </html>
+13
View File
@@ -184,6 +184,12 @@ def checkIntRange(value, first, last, default):
return default return default
def minmax(value, minVal, maxVal):
"""Make sure an integer is between min and max value (inclusive).
"""
return min(maxVal, max(minVal, value))
def checkIntTuple(value, valid, default): def checkIntTuple(value, valid, default):
"""Check that an int is an element of a tuple. If it isn't, return """Check that an int is an element of a tuple. If it isn't, return
the default value. the default value.
@@ -245,6 +251,13 @@ def formatTime(tS):
# String Functions # String Functions
# =============================================================================================== # # =============================================================================================== #
def simplified(string):
"""Take a string an strip leading and trailing whitespaces, and
replace all occurences of (multiple) whitespaces with a 0x20 space.
"""
return " ".join(str(string).strip().split())
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.
+4 -4
View File
@@ -978,12 +978,12 @@ class Config:
""" """
try: try:
import enchant # noqa: F401 import enchant # noqa: F401
self.hasEnchant = True except ImportError:
logger.debug("Checking package 'pyenchant': OK")
except Exception:
self.hasEnchant = False self.hasEnchant = False
logger.debug("Checking package 'pyenchant': Missing") logger.debug("Checking package 'pyenchant': Missing")
else:
self.hasEnchant = True
logger.debug("Checking package 'pyenchant': OK")
return return
# END Class Config # END Class Config
+6 -25
View File
@@ -25,7 +25,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwOutline from novelwriter.enum import nwItemClass, nwItemLayout, nwOutline
def trConst(tString): def trConst(tString):
@@ -34,7 +34,7 @@ def trConst(tString):
return QCoreApplication.translate("Constant", tString) return QCoreApplication.translate("Constant", tString)
class nwConst(): class nwConst:
# Date and Time Formats # Date and Time Formats
FMT_TSTAMP = "%Y-%m-%d %H:%M:%S" # Default format FMT_TSTAMP = "%Y-%m-%d %H:%M:%S" # Default format
@@ -42,32 +42,13 @@ class nwConst():
FMT_DSTAMP = "%Y-%m-%d" # Date only format FMT_DSTAMP = "%Y-%m-%d" # Date only format
# Various Hard Limits # Various Hard Limits
MAX_DEPTH = 30 # Maximum folder depth of a project
MAX_DOCSIZE = 5000000 # Maxium size of a single document MAX_DOCSIZE = 5000000 # Maxium size of a single document
MAX_BUILDSIZE = 10000000 # Maxium size of a project build MAX_BUILDSIZE = 10000000 # Maxium size of a project build
# END Class nwConst # END Class nwConst
class nwLists(): class nwRegEx:
"""Lists used for grouping various other constants.
"""
# Regular user-accessible item types
REG_TYPES = {nwItemType.ROOT, nwItemType.FOLDER, nwItemType.FILE}
# Item classes where the full list of novel layouts are allowed
CLS_NOVEL = {nwItemClass.NOVEL, nwItemClass.ARCHIVE}
# Item classes which do not require items to have same class
FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH}
# Deprecated nwItemLayout entries
DEP_LAYOUT = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE")
# END Class nwLists
class nwRegEx():
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)" FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)" FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
@@ -76,7 +57,7 @@ class nwRegEx():
# END Class nwRegEx # END Class nwRegEx
class nwFiles(): class nwFiles:
PROJ_FILE = "nwProject.nwx" PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt" PROJ_DICT = "wordlist.txt"
@@ -126,7 +107,7 @@ class nwKeyWords:
# END Class nwKeyWords # END Class nwKeyWords
class nwLabels(): class nwLabels:
CLASS_NAME = { CLASS_NAME = {
nwItemClass.NO_CLASS: QT_TRANSLATE_NOOP("Constant", "None"), nwItemClass.NO_CLASS: QT_TRANSLATE_NOOP("Constant", "None"),
@@ -204,7 +185,7 @@ class nwLabels():
# END Class nwLabels # END Class nwLabels
class nwQuotes(): class nwQuotes:
"""Allowed quotation marks. """Allowed quotation marks.
Source: https://en.wikipedia.org/wiki/Quotation_mark Source: https://en.wikipedia.org/wiki/Quotation_mark
""" """
+1 -2
View File
@@ -20,7 +20,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from novelwriter.core.document import NWDoc from novelwriter.core.document import NWDoc
from novelwriter.core.index import NWIndex, 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
from novelwriter.core.tohtml import ToHtml from novelwriter.core.tohtml import ToHtml
@@ -30,7 +30,6 @@ from novelwriter.core.tomd import ToMarkdown
__all__ = [ __all__ = [
"countWords", "countWords",
"NWDoc", "NWDoc",
"NWIndex",
"NWProject", "NWProject",
"NWSpellEnchant", "NWSpellEnchant",
"ToHtml", "ToHtml",
+1 -1
View File
@@ -52,7 +52,7 @@ class NWDoc():
self._docHandle = theHandle self._docHandle = theHandle
if self._docHandle is not None: if self._docHandle is not None:
self._theItem = self.theProject.projTree[theHandle] self._theItem = self.theProject.tree[theHandle]
return return
+743 -352
View File
File diff suppressed because it is too large Load Diff
+140 -65
View File
@@ -29,9 +29,9 @@ 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 checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified
) )
from novelwriter.constants import nwLabels, nwLists, trConst from novelwriter.constants import nwLabels, trConst
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -45,6 +45,7 @@ class NWItem():
self._name = "" self._name = ""
self._handle = None self._handle = None
self._parent = None self._parent = None
self._root = None
self._order = 0 self._order = 0
self._type = nwItemType.NO_TYPE self._type = nwItemType.NO_TYPE
self._class = nwItemClass.NO_CLASS self._class = nwItemClass.NO_CLASS
@@ -85,6 +86,10 @@ class NWItem():
def itemParent(self): def itemParent(self):
return self._parent return self._parent
@property
def itemRoot(self):
return self._root
@property @property
def itemOrder(self): def itemOrder(self):
return self._order return self._order
@@ -147,6 +152,7 @@ class NWItem():
itemAttrib = {} itemAttrib = {}
itemAttrib["handle"] = str(self._handle) itemAttrib["handle"] = str(self._handle)
itemAttrib["parent"] = str(self._parent) itemAttrib["parent"] = str(self._parent)
itemAttrib["root"] = str(self._root)
itemAttrib["order"] = str(self._order) itemAttrib["order"] = str(self._order)
itemAttrib["type"] = str(self._type.name) itemAttrib["type"] = str(self._type.name)
itemAttrib["class"] = str(self._class.name) itemAttrib["class"] = str(self._class.name)
@@ -154,13 +160,12 @@ class NWItem():
itemAttrib["layout"] = str(self._layout.name) itemAttrib["layout"] = str(self._layout.name)
metaAttrib = {} metaAttrib = {}
metaAttrib["expanded"] = str(self._expanded)
if self._type == nwItemType.FILE: if self._type == nwItemType.FILE:
metaAttrib["charCount"] = str(self._charCount) metaAttrib["charCount"] = str(self._charCount)
metaAttrib["wordCount"] = str(self._wordCount) metaAttrib["wordCount"] = str(self._wordCount)
metaAttrib["paraCount"] = str(self._paraCount) metaAttrib["paraCount"] = str(self._paraCount)
metaAttrib["cursorPos"] = str(self._cursorPos) metaAttrib["cursorPos"] = str(self._cursorPos)
else:
metaAttrib["expanded"] = str(self._expanded)
nameAttrib = {} nameAttrib = {}
nameAttrib["status"] = str(self._status) nameAttrib["status"] = str(self._status)
@@ -188,6 +193,7 @@ class NWItem():
return False return False
self.setParent(xItem.attrib.get("parent", None)) self.setParent(xItem.attrib.get("parent", None))
self.setRoot(xItem.attrib.get("root", None))
self.setOrder(xItem.attrib.get("order", 0)) self.setOrder(xItem.attrib.get("order", 0))
self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE)) self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE))
self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS)) self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS))
@@ -233,6 +239,17 @@ class NWItem():
# version of novelWriter that doesn't know the tag # version of novelWriter that doesn't know the tag
logger.error("Unknown tag '%s'", xValue.tag) logger.error("Unknown tag '%s'", xValue.tag)
# Make some checks to ensure consistency
if self._type == nwItemType.ROOT:
self._root = self._handle # Root items are their own ancestor
self._parent = None # Root items cannot have a parent
if self._type != nwItemType.FILE:
self._charCount = 0 # Only set for files
self._wordCount = 0 # Only set for files
self._paraCount = 0 # Only set for files
self._cursorPos = 0 # Only set for files
return True return True
@staticmethod @staticmethod
@@ -249,7 +266,7 @@ class NWItem():
return return
## ##
# Methods # Lookup Methods
## ##
def describeMe(self, hLevel=None): def describeMe(self, hLevel=None):
@@ -275,168 +292,226 @@ class NWItem():
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, "")) return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
def isNovelLike(self):
"""Returns true if the item is of a novel-like class.
"""
return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE)
def documentAllowed(self):
"""Returns true if the item is allowed to be of document layout.
"""
return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE, nwItemClass.TRASH)
def isInactive(self):
"""Returns true if the item is in an inactive class.
"""
return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH)
def getImportStatus(self): def getImportStatus(self):
"""Return the relevant importance or status label and icon for """Return the relevant importance or status label and icon for
the current item based on its class. the current item based on its class.
""" """
if self._class in nwLists.CLS_NOVEL: if self.isNovelLike():
stName = self.theProject.statusItems.checkEntry(self._status) stName = self.theProject.statusItems.name(self._status)
stIcon = self.theProject.statusItems.getIcon(stName) stIcon = self.theProject.statusItems.icon(self._status)
else: else:
stName = self.theProject.importItems.checkEntry(self._import) stName = self.theProject.importItems.name(self._import)
stIcon = self.theProject.importItems.getIcon(stName) stIcon = self.theProject.importItems.icon(self._import)
return stName, stIcon return stName, stIcon
def setImportStatus(self, theLabel): ##
# Special Setters
##
def setImportStatus(self, value):
"""Update the importance or status value based on class. This is """Update the importance or status value based on class. This is
a wrapper setter for setStatus and setImport. a wrapper setter for setStatus and setImport.
""" """
if self._class in nwLists.CLS_NOVEL: if self.isNovelLike():
self.setStatus(theLabel) self.setStatus(value)
else: else:
self.setImport(theLabel) self.setImport(value)
return
def setClassDefaults(self, itemClass):
"""Set the default values based on the item's class and the
project settings.
"""
if self._parent is not None:
# Only update for child items
self.setClass(itemClass)
if self._layout == nwItemLayout.NO_LAYOUT:
# If no layout is set, pick one
if self.isNovelLike():
self._layout = nwItemLayout.DOCUMENT
else:
self._layout = nwItemLayout.NOTE
elif not self.documentAllowed():
# Change layout to note if it is not in an allowed folder
self._layout = nwItemLayout.NOTE
if self._status is None:
self.setStatus("New") # This forces a default value lookup
if self._import is None:
self.setImport("New") # This forces a default value lookup
return return
## ##
# Set Item Values # Set Item Values
## ##
def setName(self, theName): def setName(self, name):
"""Set the item name. """Set the item name.
""" """
if isinstance(theName, str): if isinstance(name, str):
self._name = theName.strip() self._name = simplified(name)
else: else:
self._name = "" self._name = ""
return return
def setHandle(self, theHandle): def setHandle(self, handle):
"""Set the item handle, and ensure it is valid. """Set the item handle, and ensure it is valid.
""" """
if isHandle(theHandle): if isHandle(handle):
self._handle = theHandle self._handle = handle
else: else:
self._handle = None self._handle = None
return return
def setParent(self, theParent): def setParent(self, handle):
"""Set the parent handle, and ensure it is valid. """Set the parent handle, and ensure it is valid.
""" """
if theParent is None: if handle is None:
self._parent = None self._parent = None
elif isHandle(theParent): elif isHandle(handle):
self._parent = theParent self._parent = handle
else: else:
self._parent = None self._parent = None
return return
def setOrder(self, theOrder): def setRoot(self, handle):
"""Set the root handle, and ensure it is valid.
"""
if handle is None:
self._root = None
elif isHandle(handle):
self._root = handle
else:
self._root = None
return
def setOrder(self, order):
"""Set the item order, and ensure that it is valid. This value """Set the item order, and ensure that it is valid. This value
is purely a meta value, and not actually used by novelWriter at is purely a meta value, and not actually used by novelWriter at
the moment. the moment.
""" """
self._order = checkInt(theOrder, 0) self._order = checkInt(order, 0)
return return
def setType(self, theType): def setType(self, value):
"""Set the item type from either a proper nwItemType, or set it """Set the item type from either a proper nwItemType, or set it
from a string representing an nwItemType. from a string representing an nwItemType.
""" """
if isinstance(theType, nwItemType): if isinstance(value, nwItemType):
self._type = theType self._type = value
elif isItemType(theType): elif isItemType(value):
self._type = nwItemType[theType] self._type = nwItemType[value]
elif value == "TRASH":
self._type = nwItemType.ROOT
else: else:
logger.error("Unrecognised item type '%s'", theType) logger.error("Unrecognised item type '%s'", value)
self._type = nwItemType.NO_TYPE self._type = nwItemType.NO_TYPE
return return
def setClass(self, theClass): def setClass(self, value):
"""Set the item class from either a proper nwItemClass, or set """Set the item class from either a proper nwItemClass, or set
it from a string representing an nwItemClass. it from a string representing an nwItemClass.
""" """
if isinstance(theClass, nwItemClass): if isinstance(value, nwItemClass):
self._class = theClass self._class = value
elif isItemClass(theClass): elif isItemClass(value):
self._class = nwItemClass[theClass] self._class = nwItemClass[value]
else: else:
logger.error("Unrecognised item class '%s'", theClass) logger.error("Unrecognised item class '%s'", value)
self._class = nwItemClass.NO_CLASS self._class = nwItemClass.NO_CLASS
return return
def setLayout(self, theLayout): def setLayout(self, value):
"""Set the item layout from either a proper nwItemLayout, or set """Set the item layout from either a proper nwItemLayout, or set
it from a string representing an nwItemLayout. it from a string representing an nwItemLayout.
""" """
if isinstance(theLayout, nwItemLayout): if isinstance(value, nwItemLayout):
self._layout = theLayout self._layout = value
elif isItemLayout(theLayout): elif isItemLayout(value):
self._layout = nwItemLayout[theLayout] self._layout = nwItemLayout[value]
elif theLayout in nwLists.DEP_LAYOUT: elif value in ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"):
self._layout = nwItemLayout.DOCUMENT self._layout = nwItemLayout.DOCUMENT
else: else:
logger.error("Unrecognised item layout '%s'", theLayout) logger.error("Unrecognised item layout '%s'", value)
self._layout = nwItemLayout.NO_LAYOUT self._layout = nwItemLayout.NO_LAYOUT
return return
def setStatus(self, theStatus): def setStatus(self, value):
"""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.checkEntry(theStatus) self._status = self.theProject.statusItems.check(value)
return return
def setImport(self, theImport): 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.checkEntry(theImport) self._import = self.theProject.importItems.check(value)
return return
def setExpanded(self, expState): 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(expState, str): if isinstance(state, str):
self._expanded = (expState == str(True)) self._expanded = (state == str(True))
else: else:
self._expanded = (expState is True) self._expanded = (state is True)
return return
def setExported(self, expState): def setExported(self, state):
"""Set the export flag. """Set the export flag.
""" """
if isinstance(expState, str): if isinstance(state, str):
self._exported = (expState == str(True)) self._exported = (state == str(True))
else: else:
self._exported = (expState is True) self._exported = (state is True)
return return
## ##
# Set Document Meta Data # Set Document Meta Data
## ##
def setCharCount(self, theCount): 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(theCount, 0)) self._charCount = max(0, checkInt(count, 0))
return return
def setWordCount(self, theCount): 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(theCount, 0)) self._wordCount = max(0, checkInt(count, 0))
return return
def setParaCount(self, theCount): 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(theCount, 0)) self._paraCount = max(0, checkInt(count, 0))
return return
def setCursorPos(self, thePosition): 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(thePosition, 0)) self._cursorPos = max(0, checkInt(position, 0))
return return
def saveInitialCount(self): def saveInitialCount(self):
+191 -153
View File
@@ -37,6 +37,7 @@ from PyQt5.QtCore import QCoreApplication
from novelwriter.core.tree import NWTree from novelwriter.core.tree import NWTree
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
from novelwriter.core.index import NWIndex
from novelwriter.core.status import NWStatus from novelwriter.core.status import NWStatus
from novelwriter.core.options import OptionState from novelwriter.core.options import OptionState
from novelwriter.core.document import NWDoc from novelwriter.core.document import NWDoc
@@ -44,16 +45,16 @@ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import ( from novelwriter.common import (
checkString, checkBool, checkInt, isHandle, formatTimeStamp, checkString, checkBool, checkInt, isHandle, formatTimeStamp,
makeFileNameSafe, hexToInt makeFileNameSafe, hexToInt, simplified
) )
from novelwriter.constants import nwLists, trConst, nwFiles, nwLabels from novelwriter.constants import trConst, nwFiles, nwLabels
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class NWProject(): class NWProject():
FILE_VERSION = "1.4" FILE_VERSION = "1.4" # The current project file format version
def __init__(self, theParent): def __init__(self, theParent):
@@ -62,9 +63,10 @@ class NWProject():
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
# Core Elements # Core Elements
self.optState = OptionState(self) # Project-specific GUI options self._optState = OptionState(self) # Project-specific GUI options
self.projTree = NWTree(self) # The project tree self._projTree = NWTree(self) # The project tree
self.langData = {} # Localisation data self._projIndex = NWIndex(self) # The projecty index
self._langData = {} # Localisation data
# Project Status # Project Status
self.projOpened = 0 # The time stamp of when the project file was opened self.projOpened = 0 # The time stamp of when the project file was opened
@@ -87,7 +89,7 @@ class NWProject():
self.projFiles = [] # A list of all files in the content folder on load self.projFiles = [] # A list of all files in the content folder on load
# Project Meta # Project Meta
self.projName = "" # Project name (working title) self.projName = "" # Project name
self.bookTitle = "" # The final title; should only be used for exports self.bookTitle = "" # The final title; should only be used for exports
self.bookAuthors = [] # A list of book authors self.bookAuthors = [] # A list of book authors
@@ -116,63 +118,70 @@ class NWProject():
return return
##
# Properties
##
@property
def index(self):
return self._projIndex
@property
def tree(self):
return self._projTree
@property
def options(self):
return self._optState
## ##
# Item Methods # Item Methods
## ##
def newRoot(self, rootName, rootClass): def newRoot(self, itemClass, label=None):
"""Add a new root item. These items are unique, except for item class """Add a new root item. If label is None, use the class label.
CUSTOM, and always have parent handle set to None.
""" """
if not self.projTree.checkRootUnique(rootClass): if label is None:
self.theParent.makeAlert(self.tr("Duplicate root item detected."), nwAlert.ERROR) label = trConst(nwLabels.CLASS_NAME[itemClass])
return None
newItem = NWItem(self) newItem = NWItem(self)
newItem.setName(rootName) newItem.setName(label)
newItem.setType(nwItemType.ROOT) newItem.setType(nwItemType.ROOT)
newItem.setClass(rootClass) newItem.setClass(itemClass)
newItem.setStatus(0) self._projTree.append(None, None, newItem)
self.projTree.append(None, None, newItem) self._projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle return newItem.itemHandle
def newFolder(self, folderName, folderClass, pHandle): def newFolder(self, label, pHandle):
"""Add a new folder with a given name and class and parent item. """Add a new folder with a given label and parent item.
""" """
newItem = NWItem(self) newItem = NWItem(self)
newItem.setName(folderName) newItem.setName(label)
newItem.setType(nwItemType.FOLDER) newItem.setType(nwItemType.FOLDER)
newItem.setClass(folderClass) self._projTree.append(None, pHandle, newItem)
newItem.setStatus(0) self._projTree.updateItemData(newItem.itemHandle)
self.projTree.append(None, pHandle, newItem)
return newItem.itemHandle return newItem.itemHandle
def newFile(self, fileName, fileClass, pHandle): def newFile(self, label, pHandle):
"""Add a new file with a given name and class, and set a layout """Add a new file with a given label and parent item.
based on the class. DOCUMENT for NOVEL, otherwise NOTE.
""" """
newItem = NWItem(self) newItem = NWItem(self)
newItem.setName(fileName) newItem.setName(label)
newItem.setType(nwItemType.FILE) newItem.setType(nwItemType.FILE)
if fileClass == nwItemClass.NOVEL: self._projTree.append(None, pHandle, newItem)
newItem.setLayout(nwItemLayout.DOCUMENT) self._projTree.updateItemData(newItem.itemHandle)
else:
newItem.setLayout(nwItemLayout.NOTE)
newItem.setClass(fileClass)
newItem.setStatus(0)
self.projTree.append(None, pHandle, newItem)
return newItem.itemHandle return newItem.itemHandle
def trashFolder(self): def trashFolder(self):
"""Add the special trash root folder to the project. """Add the special trash root folder to the project.
""" """
trashHandle = self.projTree.trashRoot() trashHandle = self._projTree.trashRoot()
if trashHandle is None: if trashHandle is None:
newItem = NWItem(self) newItem = NWItem(self)
newItem.setName(trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])) newItem.setName(trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH]))
newItem.setType(nwItemType.TRASH) newItem.setType(nwItemType.ROOT)
newItem.setClass(nwItemClass.TRASH) newItem.setClass(nwItemClass.TRASH)
self.projTree.append(None, None, newItem) self._projTree.append(None, None, newItem)
self._projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle return newItem.itemHandle
return trashHandle return trashHandle
@@ -193,7 +202,7 @@ class NWProject():
self.autoCount = 0 self.autoCount = 0
# Project Tree # Project Tree
self.projTree.clear() self._projTree.clear()
# Project Settings # Project Settings
self.projPath = None self.projPath = None
@@ -218,16 +227,16 @@ class NWProject():
} }
self.spellCheck = False self.spellCheck = False
self.autoOutline = True self.autoOutline = True
self.statusItems = NWStatus() self.statusItems = NWStatus(NWStatus.STATUS)
self.statusItems.addEntry(self.tr("New"), (100, 100, 100)) self.statusItems.write(None, self.tr("New"), (100, 100, 100))
self.statusItems.addEntry(self.tr("Note"), (200, 50, 0)) self.statusItems.write(None, self.tr("Note"), (200, 50, 0))
self.statusItems.addEntry(self.tr("Draft"), (200, 150, 0)) self.statusItems.write(None, self.tr("Draft"), (200, 150, 0))
self.statusItems.addEntry(self.tr("Finished"), (50, 200, 0)) self.statusItems.write(None, self.tr("Finished"), (50, 200, 0))
self.importItems = NWStatus() self.importItems = NWStatus(NWStatus.IMPORT)
self.importItems.addEntry(self.tr("New"), (100, 100, 100)) self.importItems.write(None, self.tr("New"), (100, 100, 100))
self.importItems.addEntry(self.tr("Minor"), (200, 50, 0)) self.importItems.write(None, self.tr("Minor"), (200, 50, 0))
self.importItems.addEntry(self.tr("Major"), (200, 150, 0)) self.importItems.write(None, self.tr("Major"), (200, 150, 0))
self.importItems.addEntry(self.tr("Main"), (50, 200, 0)) self.importItems.write(None, self.tr("Main"), (50, 200, 0))
self.lastEdited = None self.lastEdited = None
self.lastViewed = None self.lastViewed = None
self.lastWCount = 0 self.lastWCount = 0
@@ -267,6 +276,7 @@ class NWProject():
logger.error("No project path set for the new project") logger.error("No project path set for the new project")
return False return False
self.clearProject()
if not self.setProjectPath(projPath, newProject=True): if not self.setProjectPath(projPath, newProject=True):
return False return False
@@ -274,86 +284,88 @@ class NWProject():
self.setBookTitle(projTitle) self.setBookTitle(projTitle)
self.setBookAuthors(projAuthors) self.setBookAuthors(projAuthors)
hNovelRoot = self.newRoot(nwItemClass.NOVEL)
hTitlePage = self.newFile(self.tr("Title Page"), hNovelRoot)
titlePage = "#! %s\n\n" % (self.bookTitle if self.bookTitle else self.projName) titlePage = "#! %s\n\n" % (self.bookTitle if self.bookTitle else self.projName)
if self.bookAuthors: if self.bookAuthors:
titlePage = "%s>> %s %s <<\n" % (titlePage, self.tr("By"), self.getAuthors()) titlePage = "%s>> %s %s <<\n" % (titlePage, self.tr("By"), self.getAuthors())
if popMinimal: aDoc = NWDoc(self, hTitlePage)
# Creating a minimal project with a few root folders and a
# single chapter folder with a single file.
xHandle = {}
xHandle[1] = self.newRoot(self.tr("Novel"), nwItemClass.NOVEL)
xHandle[2] = self.newRoot(self.tr("Plot"), nwItemClass.PLOT)
xHandle[3] = self.newRoot(self.tr("Characters"), nwItemClass.CHARACTER)
xHandle[4] = self.newRoot(self.tr("World"), nwItemClass.WORLD)
xHandle[5] = self.newFile(self.tr("Title Page"), nwItemClass.NOVEL, xHandle[1])
xHandle[6] = self.newFolder(self.tr("New Chapter"), nwItemClass.NOVEL, xHandle[1])
xHandle[7] = self.newFile(self.tr("New Chapter"), nwItemClass.NOVEL, xHandle[6])
xHandle[8] = self.newFile(self.tr("New Scene"), nwItemClass.NOVEL, xHandle[6])
aDoc = NWDoc(self, xHandle[5])
aDoc.writeDocument(titlePage) aDoc.writeDocument(titlePage)
aDoc = NWDoc(self, xHandle[7]) if popMinimal:
# Creating a minimal project with a few root folders and a
# single chapter with a single scene.
hChapter = self.newFile(self.tr("New Chapter"), hNovelRoot)
aDoc = NWDoc(self, hChapter)
aDoc.writeDocument("## %s\n\n" % self.tr("New Chapter")) aDoc.writeDocument("## %s\n\n" % self.tr("New Chapter"))
aDoc = NWDoc(self, xHandle[8]) hScene = self.newFile(self.tr("New Scene"), hChapter)
aDoc = NWDoc(self, hScene)
aDoc.writeDocument("### %s\n\n" % self.tr("New Scene")) aDoc.writeDocument("### %s\n\n" % self.tr("New Scene"))
self.newRoot(nwItemClass.PLOT)
self.newRoot(nwItemClass.CHARACTER)
self.newRoot(nwItemClass.WORLD)
self.newRoot(nwItemClass.ARCHIVE)
elif popCustom: elif popCustom:
# Create a project structure based on selected root folders # Create a project structure based on selected root folders
# and a number of chapters and scenes selected in the # and a number of chapters and scenes selected in the
# wizard's custom page. # wizard's custom page.
# Create root folders
nHandle = self.newRoot(self.tr("Novel"), nwItemClass.NOVEL)
for newRoot in projData.get("addRoots", []):
if newRoot in nwItemClass:
self.newRoot(trConst(nwLabels.CLASS_NAME[newRoot]), newRoot)
# Create a title page
tHandle = self.newFile(self.tr("Title Page"), nwItemClass.NOVEL, nHandle)
self.projTree.setFileItemLayout(tHandle, nwItemLayout.DOCUMENT)
aDoc = NWDoc(self, tHandle)
aDoc.writeDocument(titlePage)
# Create chapters and scenes # Create chapters and scenes
numChapters = projData.get("numChapters", 0) numChapters = projData.get("numChapters", 0)
numScenes = projData.get("numScenes", 0) numScenes = projData.get("numScenes", 0)
chFolders = projData.get("chFolders", False)
chSynop = self.tr("Summary of the chapter.")
scSynop = self.tr("Summary of the scene.")
# Create chapters # Create chapters
if numChapters > 0: if numChapters > 0:
for ch in range(numChapters): for ch in range(numChapters):
chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}") chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
pHandle = nHandle cHandle = self.newFile(chTitle, hNovelRoot)
if chFolders:
pHandle = self.newFolder(chTitle, nwItemClass.NOVEL, nHandle)
cHandle = self.newFile(chTitle, nwItemClass.NOVEL, pHandle)
self.projTree.setFileItemLayout(cHandle, nwItemLayout.DOCUMENT)
aDoc = NWDoc(self, cHandle) aDoc = NWDoc(self, cHandle)
aDoc.writeDocument("## %s\n\n" % chTitle) aDoc.writeDocument(f"## {chTitle}\n\n% Synopsis: {chSynop}\n\n")
# Create chapter scenes # Create chapter scenes
if numScenes > 0: if numScenes > 0:
for sc in range(numScenes): for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}") scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
sHandle = self.newFile(scTitle, nwItemClass.NOVEL, pHandle) sHandle = self.newFile(scTitle, cHandle)
aDoc = NWDoc(self, sHandle) aDoc = NWDoc(self, sHandle)
aDoc.writeDocument("### %s\n\n" % scTitle) aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
# Create scenes (no chapters) # Create scenes (no chapters)
elif numScenes > 0: elif numScenes > 0:
for sc in range(numScenes): for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}") scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
sHandle = self.newFile(scTitle, nwItemClass.NOVEL, nHandle) sHandle = self.newFile(scTitle, hNovelRoot)
aDoc = NWDoc(self, sHandle) aDoc = NWDoc(self, sHandle)
aDoc.writeDocument("### %s\n\n" % scTitle) 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 = projData.get("addNotes", False)
for newRoot in projData.get("addRoots", []):
if newRoot in nwItemClass:
rHandle = self.newRoot(newRoot)
if addNotes:
aHandle = self.newFile(noteTitles[newRoot], rHandle)
ntTag = simplified(noteTitles[newRoot]).replace(" ", "")
aDoc = NWDoc(self, aHandle)
aDoc.writeDocument(f"# {noteTitles[newRoot]}\n\n@tag: {ntTag}\n\n")
# Also add the archive and trash folders
self.newRoot(nwItemClass.ARCHIVE)
self.trashFolder()
# Finalise # Finalise
if popCustom or popMinimal: if popCustom or popMinimal:
@@ -480,8 +492,9 @@ class NWProject():
# documents and one for project notes. Introduced in # documents and one for project notes. Introduced in
# version 1.5. # version 1.5.
# 1.4 : Introduces a more compact format for storing items. All # 1.4 : Introduces a more compact format for storing items. All
# settings aside from name are now attributes. Introduced # settings aside from name are now attributes. This format
# in version 1.7. # also changes the way satus and importance labels are
# stored and handled. Introduced in version 1.7.
if fileVersion not in ("1.0", "1.1", "1.2", "1.3", "1.4"): if fileVersion not in ("1.0", "1.1", "1.2", "1.3", "1.4"):
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
@@ -533,14 +546,16 @@ class NWProject():
if xItem.text is None: if xItem.text is None:
continue continue
if xItem.tag == "name": if xItem.tag == "name":
logger.verbose("Working Title: '%s'", xItem.text) self.projName = checkString(simplified(xItem.text), "")
self.projName = xItem.text logger.verbose("Working Title: '%s'", self.projName)
elif xItem.tag == "title": elif xItem.tag == "title":
logger.verbose("Title is '%s'", xItem.text) self.bookTitle = checkString(simplified(xItem.text), "")
self.bookTitle = xItem.text logger.verbose("Title is '%s'", self.bookTitle)
elif xItem.tag == "author": elif xItem.tag == "author":
logger.verbose("Author: '%s'", xItem.text) author = checkString(simplified(xItem.text), "")
self.bookAuthors.append(xItem.text) if author:
self.bookAuthors.append(author)
logger.verbose("Author: '%s'", author)
elif xItem.tag == "saveCount": elif xItem.tag == "saveCount":
self.saveCount = checkInt(xItem.text, 0) self.saveCount = checkInt(xItem.text, 0)
elif xItem.tag == "autoCount": elif xItem.tag == "autoCount":
@@ -591,9 +606,9 @@ class NWProject():
elif xChild.tag == "content": elif xChild.tag == "content":
logger.debug("Found project content") logger.debug("Found project content")
self.projTree.unpackXML(xChild) self._projTree.unpackXML(xChild)
self.optState.loadSettings() self._optState.loadSettings()
# Sort out old file locations # Sort out old file locations
if legacyList: if legacyList:
@@ -610,7 +625,13 @@ class NWProject():
self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time()) self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time())
self.mainConf.saveRecentCache() self.mainConf.saveRecentCache()
self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName)) # Check the project tree consistency
for tItem in self._projTree:
tHandle = tItem.itemHandle
logger.verbose("Checking item '%s'", tHandle)
if not self._projTree.updateItemData(tHandle):
logger.error("There was a problem item '%s', and it has been removed", tHandle)
del self._projTree[tHandle] # The file will be re-added as orphaned
self._scanProjectFolder() self._scanProjectFolder()
self._loadProjectLocalisation() self._loadProjectLocalisation()
@@ -621,6 +642,7 @@ class NWProject():
self._writeLockFile() self._writeLockFile()
self.setProjectChanged(False) self.setProjectChanged(False)
self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName))
return True return True
@@ -687,6 +709,8 @@ class NWProject():
if len(aKey) > 0: if len(aKey) > 0:
self._packProjectValue(xTitleFmt, aKey, aValue) self._packProjectValue(xTitleFmt, aKey, aValue)
# Save Status/Importance
self.countStatus()
xStatus = etree.SubElement(xSettings, "status") xStatus = etree.SubElement(xSettings, "status")
self.statusItems.packXML(xStatus) self.statusItems.packXML(xStatus)
xStatus = etree.SubElement(xSettings, "importance") xStatus = etree.SubElement(xSettings, "importance")
@@ -694,7 +718,7 @@ class NWProject():
# Save Tree Content # Save Tree Content
logger.debug("Writing project content") logger.debug("Writing project content")
self.projTree.packXML(nwXML) self._projTree.packXML(nwXML)
# Write the xml tree to file # Write the xml tree to file
tempFile = os.path.join(self.projPath, self.projFile+"~") tempFile = os.path.join(self.projPath, self.projFile+"~")
@@ -727,7 +751,7 @@ class NWProject():
return False return False
# Save project GUI options # Save project GUI options
self.optState.saveSettings() self._optState.saveSettings()
# Update recent projects # Update recent projects
self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime) self.mainConf.updateRecentCache(self.projPath, self.projName, self.currWCount, saveTime)
@@ -743,8 +767,8 @@ class NWProject():
"""Close the current project and clear all meta data. """Close the current project and clear all meta data.
""" """
logger.info("Closing project: %s", self.projPath) logger.info("Closing project: %s", self.projPath)
self.optState.saveSettings() self._optState.saveSettings()
self.projTree.writeToCFile() self._projTree.writeToCFile()
self._appendSessionStats(idleTime) self._appendSessionStats(idleTime)
self._clearLockFile() self._clearLockFile()
self.clearProject() self.clearProject()
@@ -950,17 +974,17 @@ class NWProject():
return True return True
def setProjectName(self, projName): def setProjectName(self, projName):
"""Set the project name (working title), This is the the title """Set the project name, This is the the name used for backup
used for backup files etc. files etc.
""" """
self.projName = projName.strip() self.projName = simplified(projName)
self.setProjectChanged(True) self.setProjectChanged(True)
return True return True
def setBookTitle(self, bookTitle): def setBookTitle(self, bookTitle):
"""Set the book title, that is, the title to include in exports. """Set the book title, that is, the title to include in exports.
""" """
self.bookTitle = bookTitle.strip() self.bookTitle = simplified(bookTitle)
self.setProjectChanged(True) self.setProjectChanged(True)
return True return True
@@ -972,7 +996,7 @@ class NWProject():
self.bookAuthors = [] self.bookAuthors = []
for bookAuthor in bookAuthors.splitlines(): for bookAuthor in bookAuthors.splitlines():
bookAuthor = bookAuthor.strip() bookAuthor = simplified(bookAuthor)
if bookAuthor == "": if bookAuthor == "":
continue continue
self.bookAuthors.append(bookAuthor) self.bookAuthors.append(bookAuthor)
@@ -1019,6 +1043,7 @@ class NWProject():
self.projSpell = theLang self.projSpell = theLang
self.setProjectChanged(True) self.setProjectChanged(True)
return True return True
return False
def setProjectLang(self, theLang): def setProjectLang(self, theLang):
"""Set the project-specific language. """Set the project-specific language.
@@ -1043,9 +1068,9 @@ class NWProject():
items in the GUI project tree. The user can rearrange the order items in the GUI project tree. The user can rearrange the order
by drag-and-drop. Forwarded to the NWTree class. by drag-and-drop. Forwarded to the NWTree class.
""" """
if len(self.projTree) != len(newOrder): if len(self._projTree) != len(newOrder):
logger.warning("Sizes of new and old tree order do not match") logger.warning("Sizes of new and old tree order do not match")
self.projTree.setOrder(newOrder) self._projTree.setOrder(newOrder)
self.setProjectChanged(True) self.setProjectChanged(True)
return True return True
@@ -1065,34 +1090,22 @@ class NWProject():
self.setProjectChanged(True) self.setProjectChanged(True)
return True return True
def setStatusColours(self, newCols): def setStatusColours(self, newCols, delCols):
"""Update the list of novel file status flags. Also iterate """Update the list of novel file status flags.
through the project and replace keys that have been renamed.
""" """
replaceMap = self.statusItems.setNewEntries(newCols) return self._setStatusImport(newCols, delCols, self.statusItems)
for nwItem in self.projTree:
if nwItem.itemClass in nwLists.CLS_NOVEL:
if nwItem.itemStatus in replaceMap:
nwItem.setStatus(replaceMap[nwItem.itemStatus])
self.setProjectChanged(True)
return True
def setImportColours(self, newCols): def setImportColours(self, newCols, delCols):
"""Update the list of note file importance flags. Also iterate """Update the list of note file importance flags.
through the project and replace keys that have been renamed.
""" """
replaceMap = self.importItems.setNewEntries(newCols) return self._setStatusImport(newCols, delCols, self.importItems)
for nwItem in self.projTree:
if nwItem.itemClass not in nwLists.CLS_NOVEL:
if nwItem.itemImport in replaceMap:
nwItem.setImport(replaceMap[nwItem.itemImport])
self.setProjectChanged(True)
return True
def setAutoReplace(self, autoReplace): def setAutoReplace(self, autoReplace):
"""Update the auto-replace dictionary. """Update the auto-replace dictionary.
""" """
self.autoReplace = autoReplace self.autoReplace = {}
for key, entry in autoReplace.items():
self.autoReplace[key] = simplified(entry)
self.setProjectChanged(True) self.setProjectChanged(True)
return True return True
@@ -1101,7 +1114,9 @@ class NWProject():
""" """
for valKey, valEntry in titleFormat.items(): for valKey, valEntry in titleFormat.items():
if valKey in self.titleFormat: if valKey in self.titleFormat:
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey]) self.titleFormat[valKey] = checkString(
simplified(valEntry), self.titleFormat[valKey]
)
return True return True
def setProjectChanged(self, bValue): def setProjectChanged(self, bValue):
@@ -1149,16 +1164,16 @@ class NWProject():
capable of handling it. capable of handling it.
""" """
sentItems = [] sentItems = []
iterItems = self.projTree.handles() iterItems = self._projTree.handles()
n = 0 n = 0
nMax = min(len(iterItems), 10000) nMax = min(len(iterItems), 10000)
while n < nMax: while n < nMax:
tHandle = iterItems[n] tHandle = iterItems[n]
tItem = self.projTree[tHandle] tItem = self._projTree[tHandle]
n += 1 n += 1
if tItem is None: if tItem is None:
# Technically a bug since treeOrder is built from the # Technically a bug since treeOrder is built from the
# same data as projTree # same data as _projTree
continue continue
elif tItem.itemParent is None: elif tItem.itemParent is None:
# Item is a root, or already been identified as an # Item is a root, or already been identified as an
@@ -1189,7 +1204,7 @@ class NWProject():
def updateWordCounts(self): def updateWordCounts(self):
"""Update the total word count values. """Update the total word count values.
""" """
wcNovel, wcNotes = self.projTree.sumWords() wcNovel, wcNotes = self._projTree.sumWords()
wcTotal = wcNovel + wcNotes wcTotal = wcNovel + wcNotes
if wcTotal != self.currWCount: if wcTotal != self.currWCount:
self.currNovelWC = wcNovel self.currNovelWC = wcNovel
@@ -1205,11 +1220,11 @@ class NWProject():
""" """
self.statusItems.resetCounts() self.statusItems.resetCounts()
self.importItems.resetCounts() self.importItems.resetCounts()
for nwItem in self.projTree: for nwItem in self._projTree:
if nwItem.itemClass in nwLists.CLS_NOVEL: if nwItem.isNovelLike():
self.statusItems.countEntry(nwItem.itemStatus) self.statusItems.increment(nwItem.itemStatus)
else: else:
self.importItems.countEntry(nwItem.itemImport) self.importItems.increment(nwItem.itemImport)
return return
def localLookup(self, theWord): def localLookup(self, theWord):
@@ -1217,17 +1232,39 @@ class NWProject():
return it. The variable is cast to a string before lookup. If return it. The variable is cast to a string before lookup. If
the word does not exist, it returns itself. the word does not exist, it returns itself.
""" """
return self.langData.get(str(theWord), str(theWord)) return self._langData.get(str(theWord), str(theWord))
## ##
# Internal Functions # Internal Functions
## ##
def _setStatusImport(self, new, delete, target):
"""Update the list of novel file status or importance flags, and
delete those that have been requested deleted.
"""
if not (new or delete):
return False
order = []
for entry in new:
key = entry.get("key", None)
name = entry.get("name", "")
cols = entry.get("cols", (100, 100, 100))
if name:
order.append(target.write(key, name, cols))
for key in delete:
target.remove(key)
target.reorder(order)
return True
def _loadProjectLocalisation(self): def _loadProjectLocalisation(self):
"""Load the language data for the current project language. """Load the language data for the current project language.
""" """
if self.projLang is None: if self.projLang is None:
self.langData = {} self._langData = {}
return False return False
langFile = os.path.join(self.mainConf.nwLangPath, "project_%s.json" % self.projLang) langFile = os.path.join(self.mainConf.nwLangPath, "project_%s.json" % self.projLang)
@@ -1236,7 +1273,7 @@ class NWProject():
try: try:
with open(langFile, mode="r", encoding="utf-8") as inFile: with open(langFile, mode="r", encoding="utf-8") as inFile:
self.langData = json.load(inFile) self._langData = json.load(inFile)
logger.debug("Loaded project language file: %s", os.path.basename(langFile)) logger.debug("Loaded project language file: %s", os.path.basename(langFile))
except Exception: except Exception:
@@ -1371,7 +1408,7 @@ class NWProject():
logger.warning("Skipping file: %s", fileItem) logger.warning("Skipping file: %s", fileItem)
continue continue
if fHandle in self.projTree: if fHandle in self._projTree:
self.projFiles.append(fHandle) self.projFiles.append(fHandle)
logger.debug("Checking file %s, handle '%s': OK", fileItem, fHandle) logger.debug("Checking file %s, handle '%s': OK", fileItem, fHandle)
else: else:
@@ -1418,10 +1455,10 @@ class NWProject():
if oLayout is None: if oLayout is None:
oLayout = nwItemLayout.NOTE oLayout = nwItemLayout.NOTE
if oParent is None or oParent not in self.projTree: if oParent is None or oParent not in self._projTree:
oParent = self.projTree.findRoot(oClass) oParent = self._projTree.findRoot(oClass)
if oParent is None: if oParent is None:
oParent = self.projTree.findRoot(nwItemClass.NOVEL) oParent = self._projTree.findRoot(nwItemClass.NOVEL)
# If the file still has no parent item, skip it # If the file still has no parent item, skip it
if oParent is None: if oParent is None:
@@ -1433,7 +1470,8 @@ class NWProject():
orphItem.setType(nwItemType.FILE) orphItem.setType(nwItemType.FILE)
orphItem.setClass(oClass) orphItem.setClass(oClass)
orphItem.setLayout(oLayout) orphItem.setLayout(oLayout)
self.projTree.append(oHandle, oParent, orphItem) self._projTree.append(oHandle, oParent, orphItem)
self._projTree.updateItemData(orphItem.itemHandle)
if noWhere: if noWhere:
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
+197 -108
View File
@@ -5,6 +5,7 @@ Data class for the status/importance settings of a project item
File History: File History:
Created: 2019-05-19 [0.1.3] Created: 2019-05-19 [0.1.3]
Rewritten: 2022-04-05 [1.7a0]
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen Copyright 20182022, Veronica Berglyd Olsen
@@ -23,6 +24,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/>.
""" """
import random
import logging import logging
import novelwriter import novelwriter
@@ -30,134 +32,208 @@ from lxml import etree
from PyQt5.QtGui import QIcon, QPixmap, QColor from PyQt5.QtGui import QIcon, QPixmap, QColor
from novelwriter.common import checkInt from novelwriter.common import checkInt, minmax, simplified
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class NWStatus(): class NWStatus():
def __init__(self): STATUS = 1
IMPORT = 2
def __init__(self, type):
self._type = type
self._store = {}
self._reverse = {}
self._default = None
self._theLabels = []
self._theColours = []
self._theCounts = []
self._theIcons = []
self._theMap = {}
self._theLength = 0
self._theIndex = 0
self._iconSize = novelwriter.CONFIG.pxInt(32) self._iconSize = novelwriter.CONFIG.pxInt(32)
pixmap = QPixmap(self._iconSize, self._iconSize)
pixmap.fill(QColor(100, 100, 100))
self._defaultIcon = QIcon(pixmap)
if self._type == self.STATUS:
self._prefix = "s"
elif self._type == self.IMPORT:
self._prefix = "i"
else:
raise Exception("This is a bug!")
return return
def addEntry(self, theLabel, theColours): def write(self, key, name, cols, count=None):
"""Add a status entry to the status object, but ensure it isn't """Add or update a status entry. If the key is invalid, a new
a duplicate. key is generated.
""" """
theLabel = theLabel.strip() if not self._isKey(key):
if self._getIndex(theLabel) is None: key = self._newKey()
theIcon = QPixmap(self._iconSize, self._iconSize) if not isinstance(cols, tuple):
theIcon.fill(QColor(*theColours)) cols = (100, 100, 100)
self._theIcons.append(QIcon(theIcon)) if len(cols) != 3:
self._theLabels.append(theLabel) cols = (100, 100, 100)
self._theColours.append(theColours)
self._theCounts.append(0) pixmap = QPixmap(self._iconSize, self._iconSize)
self._theMap[theLabel] = self._theLength pixmap.fill(QColor(*cols))
self._theLength += 1
name = simplified(name)
if count is None:
count = self._store[key]["count"] if key in self._store else 0
self._store[key] = {
"name": name,
"icon": QIcon(pixmap),
"cols": cols,
"count": count,
}
self._reverse[name] = key
if self._default is None:
self._default = key
return key
def remove(self, key):
"""Remove an entry in the list, but not if the count is larger
than 0.
"""
if key not in self._store:
return False
if self._store[key]["count"] > 0:
return False
del self._reverse[self._store[key]["name"]]
del self._store[key]
keys = list(self._store.keys())
if key == self._default:
if len(keys) > 0:
self._default = keys[0]
else:
self._default = None
return True return True
def checkEntry(self, theStatus): def check(self, value):
"""Check if a status value is valid, and returns the safe """Check the key against the stored status names.
reference to be used internally.
""" """
if isinstance(theStatus, str): if self._isKey(value) and value in self._store:
if self._getIndex(theStatus) is not None: return value
return theStatus.strip() elif value in self._reverse:
return self._theLabels[0] return self._reverse[value]
elif self._default is not None:
return self._default
else:
return ""
def getIcon(self, theLabel): def name(self, key):
"""Return the icon for the given status item. """Return the name associated with a given key.
""" """
theIndex = self._getIndex(theLabel) if key in self._store:
if theIndex is not None: return self._store[key]["name"]
return self._theIcons[theIndex] elif self._default is not None:
return QIcon() return self._store[self._default]["name"]
else:
return ""
def setNewEntries(self, newList): def cols(self, key):
"""Update the list of entries after they have been modified by """Return the colours associated with a given key.
the GUI tool.
""" """
replaceMap = {} if key in self._store:
return self._store[key]["cols"]
elif self._default is not None:
return self._store[self._default]["cols"]
else:
return (100, 100, 100)
if newList is not None: def count(self, key):
self._theLabels = [] """Return the count associated with a given key.
self._theColours = [] """
self._theCounts = [] if key in self._store:
self._theIcons = [] return self._store[key]["count"]
self._theMap = {} elif self._default is not None:
self._theLength = 0 return self._store[self._default]["count"]
self._theIndex = 0 else:
return 0
for nName, nR, nG, nB, oName in newList: def icon(self, key):
self.addEntry(nName, (nR, nG, nB)) """Return the icon associated with a given key.
if nName != oName and oName is not None: """
replaceMap[oName] = nName if key in self._store:
return self._store[key]["icon"]
elif self._default is not None:
return self._store[self._default]["icon"]
else:
return self._defaultIcon
return replaceMap def reorder(self, order):
"""Reorder the items according to list.
"""
if len(order) != len(self._store):
logger.error("Length mismatch between new and old order")
return False
if order == list(self._store.keys()):
return False
store = {}
for key in order:
if key in self._store:
store[key] = self._store[key]
else:
logger.error("Unknown key '%s' in order", key)
return False
self._store = store
return True
def resetCounts(self): def resetCounts(self):
"""Clear the counts of references to the status entries. """Clear the counts of references to the status entries.
""" """
self._theCounts = [0]*self._theLength for key in self._store:
self._store[key]["count"] = 0
return return
def countEntry(self, theLabel): def increment(self, key):
"""Increment the counter for a given label. This should be used """Increment the counter for a given entry.
together with resetCounts in a loop over project items.
""" """
theIndex = self._getIndex(theLabel) if key in self._store:
if theIndex is not None: self._store[key]["count"] += 1
self._theCounts[theIndex] += 1
return return
def packXML(self, xParent): def packXML(self, xParent):
"""Pack the status entries into an XML object for saving to the """Pack the status entries into an XML object for saving to the
main project file. main project file.
""" """
for n in range(self._theLength): for key, data in self._store.items():
xSub = etree.SubElement(xParent, "entry", attrib={ xSub = etree.SubElement(xParent, "entry", attrib={
"red": str(self._theColours[n][0]), "key": key,
"green": str(self._theColours[n][1]), "count": str(data["count"]),
"blue": str(self._theColours[n][2]), "red": str(data["cols"][0]),
"green": str(data["cols"][1]),
"blue": str(data["cols"][2]),
}) })
xSub.text = self._theLabels[n] xSub.text = data["name"]
return True return True
def unpackXML(self, xParent): def unpackXML(self, xParent):
"""Unpack an XML tree and set the class values. """Unpack an XML tree and set the class values.
""" """
theLabels = [] self._store = {}
theColours = [] self._reverse = {}
self._default = None
for xChild in xParent: for xChild in xParent:
theLabels.append(xChild.text) key = xChild.attrib.get("key", None)
cR = checkInt(xChild.attrib.get("red", 0), 0, False) name = xChild.text.strip()
cG = checkInt(xChild.attrib.get("green", 0), 0, False) count = max(checkInt(xChild.attrib.get("count", 0), 0), 0)
cB = checkInt(xChild.attrib.get("blue", 0), 0, False) red = minmax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255)
theColours.append((cR, cG, cB)) green = minmax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255)
blue = minmax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255)
if len(theLabels) > 0: self.write(key, name, (red, green, blue), count)
self._theLabels = []
self._theColours = []
self._theCounts = []
self._theIcons = []
self._theMap = {}
self._theLength = 0
self._theIndex = 0
for n in range(len(theLabels)):
self.addEntry(theLabels[n], theColours[n])
return True return True
@@ -165,39 +241,52 @@ class NWStatus():
# Internal Functions # Internal Functions
## ##
def _getIndex(self, theLabel): def _newKey(self):
"""Look up a status entry in the object lists, and return it if """Generate a new key for a status flag. This method is
it exists. recursive, but should only fail if there is an issue with the
random number generator or the user has added a lot of status
flags. The Python recursion limit is given the job to handle
the extreme case and will cause an app crash.
""" """
if theLabel is None: key = f"{self._prefix}{random.getrandbits(24):06x}"
return None if key in self._store:
return self._theMap.get(theLabel.strip(), None) key = self._newKey()
return key
def _isKey(self, value):
"""Check if a value is a key or not.
"""
if not isinstance(value, str):
return False
if len(value) != 7:
return False
if value[0] != self._prefix:
return False
for c in value[1:]:
if c not in "0123456789abcdef":
return False
return True
## ##
# Iterator Bits # Iterator Bits
## ##
def __getitem__(self, n): def __len__(self):
"""Return an entry by its index. return len(self._store)
"""
if n >= 0 and n < self._theLength: def __getitem__(self, key):
return self._theLabels[n], self._theColours[n], self._theCounts[n], self._theIcons[n] return self._store[key]
return None, None, None, QIcon()
def __iter__(self): def __iter__(self):
"""Initialise the iterator. return iter(self._store)
"""
self._theIndex = 0
return self
def __next__(self): def keys(self):
"""Return the next entry for the iterator. return self._store.keys()
"""
if self._theIndex < self._theLength: def items(self):
theLabel, theColour, theCount, theIcon = self.__getitem__(self._theIndex) return self._store.items()
self._theIndex += 1
return theLabel, theColour, theCount, theIcon def values(self):
else: return self._store.values()
raise StopIteration
# END Class NWStatus # END Class NWStatus
+1 -1
View File
@@ -451,7 +451,7 @@ class ToHtml(Tokenizer):
def _formatKeywords(self, tText): def _formatKeywords(self, tText):
"""Apply HTML formatting to keywords. """Apply HTML formatting to keywords.
""" """
isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText) isValid, theBits, _ = self.theProject.index.scanThis("@"+tText)
if not isValid or not theBits: if not isValid or not theBits:
return "" return ""
+3 -3
View File
@@ -275,7 +275,7 @@ class Tokenizer(ABC):
def addRootHeading(self, theHandle): def addRootHeading(self, theHandle):
"""Add a heading at the start of a new root folder. """Add a heading at the start of a new root folder.
""" """
if not self.theProject.projTree.checkType(theHandle, nwItemType.ROOT): if not self.theProject.tree.checkType(theHandle, nwItemType.ROOT):
return False return False
if self._isFirst: if self._isFirst:
@@ -284,7 +284,7 @@ class Tokenizer(ABC):
else: else:
textAlign = self.A_PBB | self.A_CENTRE textAlign = self.A_PBB | self.A_CENTRE
theItem = self.theProject.projTree[theHandle] theItem = self.theProject.tree[theHandle]
locNotes = self._localLookup("Notes") locNotes = self._localLookup("Notes")
theTitle = f"{locNotes}: {theItem.itemName}" theTitle = f"{locNotes}: {theItem.itemName}"
self._theTokens = [] self._theTokens = []
@@ -301,7 +301,7 @@ class Tokenizer(ABC):
not set, load it from the file. not set, load it from the file.
""" """
self._theHandle = theHandle self._theHandle = theHandle
self._theItem = self.theProject.projTree[theHandle] self._theItem = self.theProject.tree[theHandle]
if self._theItem is None: if self._theItem is None:
return False return False
+1 -1
View File
@@ -193,7 +193,7 @@ class ToMarkdown(Tokenizer):
def _formatKeywords(self, tText, tStyle): def _formatKeywords(self, tText, tStyle):
"""Apply Markdown formatting to keywords. """Apply Markdown formatting to keywords.
""" """
isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText) isValid, theBits, _ = self.theProject.index.scanThis("@"+tText)
if not isValid or not theBits: if not isValid or not theBits:
return "" return ""
+1 -1
View File
@@ -550,7 +550,7 @@ class ToOdt(Tokenizer):
def _formatKeywords(self, tText): def _formatKeywords(self, tText):
"""Apply formatting to keywords. """Apply formatting to keywords.
""" """
isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText) isValid, theBits, _ = self.theProject.index.scanThis("@"+tText)
if not isValid or not theBits: if not isValid or not theBits:
return "" return ""
+103 -131
View File
@@ -24,16 +24,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import os import os
import random
import logging import logging
from time import time
from lxml import etree from lxml import etree
from hashlib import sha256
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout from novelwriter.enum import nwItemType, 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 nwConst, nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -41,21 +40,20 @@ logger = logging.getLogger(__name__)
class NWTree(): class NWTree():
MAX_DEPTH = 1000 # Cap of tree traversing for loops
def __init__(self, theProject): def __init__(self, theProject):
self.theProject = theProject self.theProject = theProject
self._projTree = {} # Holds all the items of the project self._projTree = {} # Holds all the items of the project
self._treeOrder = [] # The order of the tree items on the tree view self._treeOrder = [] # The order of the tree items on the tree view
self._treeRoots = [] # The root items of the tree self._treeRoots = {} # The root items of the tree
self._trashRoot = None # The handle of the trash root folder self._trashRoot = None # The handle of the trash root folder
self._archRoot = None # The handle of the archive root folder self._archRoot = None # The handle of the archive root folder
self._theIndex = 0 # The current iterator index self._theIndex = 0 # The current iterator index
self._treeChanged = False # True if tree structure has changed self._treeChanged = False # True if tree structure has changed
self._handleSeed = None # Used for generating handles for testing
self._handleCount = 0 # A counter that is added to the handle generator
return return
## ##
@@ -67,7 +65,7 @@ class NWTree():
""" """
self._projTree = {} self._projTree = {}
self._treeOrder = [] self._treeOrder = []
self._treeRoots = [] self._treeRoots = {}
self._trashRoot = None self._trashRoot = None
self._archRoot = None self._archRoot = None
self._theIndex = 0 self._theIndex = 0
@@ -98,12 +96,11 @@ class NWTree():
if nwItem.itemType == nwItemType.ROOT: if nwItem.itemType == nwItemType.ROOT:
logger.verbose("Item '%s' is a root item", str(tHandle)) logger.verbose("Item '%s' is a root item", str(tHandle))
self._treeRoots.append(tHandle) 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.verbose("Item '%s' is the archive folder", str(tHandle))
self._archRoot = tHandle self._archRoot = tHandle
elif nwItem.itemClass == nwItemClass.TRASH:
if nwItem.itemType == nwItemType.TRASH:
if self._trashRoot is None: if self._trashRoot is None:
logger.verbose("Item '%s' is the trash folder", str(tHandle)) logger.verbose("Item '%s' is the trash folder", str(tHandle))
self._trashRoot = tHandle self._trashRoot = tHandle
@@ -207,9 +204,30 @@ class NWTree():
return novelWords, noteWords return novelWords, noteWords
## ##
# Tree Structure Methods # Tree Item Methods
## ##
def updateItemData(self, tHandle):
"""Update the root item handle of a given item. Returns True if
a root was found and data updated, otherwise False.
"""
tItem = self.__getitem__(tHandle)
if tItem is None:
return False
iItem = tItem
for _ in range(self.MAX_DEPTH):
if iItem.itemParent is None:
tItem.setRoot(iItem.itemHandle)
tItem.setClassDefaults(iItem.itemClass)
return True
else:
iItem = self.__getitem__(iItem.itemParent)
if iItem is None:
return False
else:
raise RecursionError("Critical internal error")
def checkType(self, tHandle, itemType): def checkType(self, tHandle, itemType):
"""Return true of item exists and is of the specified item type. """Return true of item exists and is of the specified item type.
""" """
@@ -218,71 +236,6 @@ class NWTree():
return False return False
return tItem.itemType == itemType return tItem.itemType == itemType
def trashRoot(self):
"""Returns the handle of the trash folder, or None if there
isn't one.
"""
if self._trashRoot:
return self._trashRoot
return None
def isTrashRoot(self, tHandle):
"""Check if a handle is the trash folder.
"""
if self._trashRoot is None:
return False
return tHandle == self._trashRoot
def archiveRoot(self):
"""Returns the handle of the archive folder, or None if there
isn't one.
"""
if self._archRoot:
return self._archRoot
return None
def findRoot(self, theClass):
"""Find the root item for a given class.
Note: This returns the first item for class CUSTOM.
"""
for aRoot in self._treeRoots:
tItem = self.__getitem__(aRoot)
if tItem is None:
continue
if theClass == tItem.itemClass:
return tItem.itemHandle
return None
def checkRootUnique(self, theClass):
"""Checks if there already is a root entry of class 'theClass'
in the root of the project tree. CUSTOM class is skipped as it
is not required to be unique.
"""
if theClass == nwItemClass.CUSTOM:
return True
for aRoot in self._treeRoots:
tItem = self.__getitem__(aRoot)
if tItem is None:
continue
if theClass == tItem.itemClass:
return False
return True
def getRootItem(self, tHandle):
"""Iterate upwards in the tree until we find the item with
parent None, the root item. We do this with a for loop with a
maximum depth to make infinite loops impossible.
"""
tItem = self.__getitem__(tHandle)
if tItem is not None:
for i in range(nwConst.MAX_DEPTH + 1):
if tItem.itemParent is None:
return tItem
else:
tHandle = tItem.itemParent
tItem = self.__getitem__(tHandle)
return None
def getItemPath(self, tHandle): def getItemPath(self, tHandle):
"""Iterate upwards in the tree until we find the item with """Iterate upwards in the tree until we find the item with
parent None, the root item, and return the list of handles. parent None, the root item, and return the list of handles.
@@ -293,7 +246,7 @@ class NWTree():
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem is not None: if tItem is not None:
tTree.append(tHandle) tTree.append(tHandle)
for _ in range(nwConst.MAX_DEPTH + 1): for _ in range(self.MAX_DEPTH):
if tItem.itemParent is None: if tItem.itemParent is None:
return tTree return tTree
else: else:
@@ -303,8 +256,72 @@ class NWTree():
return tTree return tTree
else: else:
tTree.append(tHandle) tTree.append(tHandle)
else:
raise RecursionError("Critical internal error")
return tTree return tTree
##
# Tree Root Methods
##
def rootClasses(self):
"""Return a set of all root classes in use by the project.
"""
rootClasses = set()
for nwItem in self._treeRoots.values():
rootClasses.add(nwItem.itemClass)
return rootClasses
def iterRoots(self, itemClass):
"""Iterate over all items of a given class.
"""
for tHandle, nwItem in self._treeRoots.items():
if nwItem.itemClass == itemClass:
yield tHandle, nwItem
return
def isRoot(self, tHandle):
"""Check if a handle is a root item.
"""
return tHandle in self._treeRoots
def isTrash(self, tHandle):
"""Check if an item is in or is the trash folder.
"""
tItem = self.__getitem__(tHandle)
if tItem is None:
return True
if tItem.itemClass == nwItemClass.TRASH:
return True
if self._trashRoot is not None:
if tHandle == self._trashRoot:
return True
elif tItem.itemParent == self._trashRoot:
return True
elif tItem.itemRoot == self._trashRoot:
return True
return False
def trashRoot(self):
"""Returns the handle of the trash folder, or None if there
isn't one.
"""
if self._trashRoot:
return self._trashRoot
return None
def findRoot(self, theClass):
"""Find the first root item for a given class.
"""
for aRoot in self._treeRoots:
tItem = self.__getitem__(aRoot)
if tItem is None:
continue
if theClass == tItem.itemClass:
return tItem.itemHandle
return None
## ##
# Setters # Setters
## ##
@@ -334,14 +351,6 @@ class NWTree():
return return
def setSeed(self, theSeed):
"""Used for debugging!
Sets a seed for generating handles so that they always come out
in a predictable order.
"""
self._handleSeed = theSeed
return
def setFileItemLayout(self, tHandle, itemLayout): def setFileItemLayout(self, tHandle, itemLayout):
"""Set the nwItemLayout for a specific file. """Set the nwItemLayout for a specific file.
""" """
@@ -358,30 +367,6 @@ class NWTree():
return True return True
##
# Getters
##
def countTypes(self):
"""Count the number of files, folders and roots in the project.
"""
nRoot = 0
nFolder = 0
nFile = 0
for tHandle in self._treeOrder:
tItem = self.__getitem__(tHandle)
if tItem is None:
continue
elif tItem.itemType == nwItemType.ROOT:
nRoot += 1
elif tItem.itemType == nwItemType.FOLDER:
nFolder += 1
elif tItem.itemType == nwItemType.FILE:
nFile += 1
return nRoot, nFolder, nFile
## ##
# Meta Methods # Meta Methods
## ##
@@ -420,7 +405,7 @@ class NWTree():
return return
if tHandle in self._treeRoots: if tHandle in self._treeRoots:
self._treeRoots.remove(tHandle) del self._treeRoots[tHandle]
if tHandle == self._trashRoot: if tHandle == self._trashRoot:
self._trashRoot = None self._trashRoot = None
if tHandle == self._archRoot: if tHandle == self._archRoot:
@@ -468,29 +453,16 @@ class NWTree():
self.theProject.setProjectChanged(True) self.theProject.setProjectChanged(True)
return return
def _makeHandle(self, addSeed=""): def _makeHandle(self):
"""Generate a unique item handle. In the event that the key """Generate a unique item handle. In the event that the key
already exists, salt the seed and generate a new handle. already exists, generate a new one.
A key collision is very unlikely to be caused by the truncation
of the sha256 hash to 13 characters. Assuming it is near-random,
it will on average happen every 4.5^15 times. However, the clock
seed is likely to occasionally generate a collision if the
handle requests come faster than the clock resolution.
""" """
if self._handleSeed is None: logger.verbose("Generating new handle")
newSeed = "%s_%d_%s" % (str(time()), self._handleCount, addSeed) handle = f"{random.getrandbits(52):013x}"
self._handleCount += 1 if handle in self._projTree:
else:
# This is used for debugging
newSeed = str(self._handleSeed)
self._handleSeed += 1
logger.verbose("Generating handle with seed '%s'", newSeed)
itemHandle = sha256(newSeed.encode()).hexdigest()[0:13]
if itemHandle in self._projTree:
logger.warning("Duplicate handle encountered! Retrying ...") logger.warning("Duplicate handle encountered! Retrying ...")
itemHandle = self._makeHandle(addSeed+"!") handle = self._makeHandle()
return itemHandle return handle
# END Class NWTree # END Class NWTree
+3 -3
View File
@@ -181,14 +181,14 @@ class GuiAbout(QDialog):
aboutMsg += "<h4>{0}</h4><p>{1}</p>".format( aboutMsg += "<h4>{0}</h4><p>{1}</p>".format(
self.tr("Translations"), self.tr("Translations"),
self._wrapTable([ self._wrapTable([
("Deutsch", "Myian"),
("English", "Veronica Berglyd Olsen"), ("English", "Veronica Berglyd Olsen"),
("Español Latinoamericano", "Tommy Marplatt"),
("Français", "Jan Lüdke (jyhelle)"), ("Français", "Jan Lüdke (jyhelle)"),
("Nederlands", "Martijn van der Kleijn"),
("Norsk Bokmål", "Veronica Berglyd Olsen"), ("Norsk Bokmål", "Veronica Berglyd Olsen"),
("Português", "Bruno Meneguello"), ("Português", "Bruno Meneguello"),
("简体中文", "Qianzhi Long"), ("简体中文", "Qianzhi Long"),
("Español Latinoamericano", "Tommy Marplatt"),
("Nederlands", "Martijn van der Kleijn"),
("Deutsch", "Myian"),
]) ])
) )
+5 -5
View File
@@ -125,13 +125,13 @@ class GuiDocMerge(QDialog):
), nwAlert.ERROR) ), nwAlert.ERROR)
return False return False
srcItem = self.theProject.projTree[self.sourceItem] srcItem = self.theProject.tree[self.sourceItem]
if srcItem is None: if srcItem is None:
self.theParent.makeAlert(self.tr("Internal error."), nwAlert.ERROR) self.theParent.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
return False return False
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent) nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent)
newItem = self.theProject.projTree[nHandle] newItem = self.theProject.tree[nHandle]
newItem.setStatus(srcItem.itemStatus) newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport) newItem.setImport(srcItem.itemImport)
@@ -170,7 +170,7 @@ class GuiDocMerge(QDialog):
if tHandle is None: if tHandle is None:
return False return False
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.tree[tHandle]
if nwItem is None: if nwItem is None:
return False return False
@@ -182,7 +182,7 @@ class GuiDocMerge(QDialog):
for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle): for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle):
newItem = QListWidgetItem() newItem = QListWidgetItem()
nwItem = self.theProject.projTree[sHandle] nwItem = self.theProject.tree[sHandle]
if nwItem.itemType is not nwItemType.FILE: if nwItem.itemType is not nwItemType.FILE:
continue continue
newItem.setText(nwItem.itemName) newItem.setText(nwItem.itemName)
+9 -27
View File
@@ -33,8 +33,7 @@ from PyQt5.QtWidgets import (
) )
from novelwriter.core import NWDoc from novelwriter.core import NWDoc
from novelwriter.enum import nwAlert, nwItemType, nwItemClass, nwItemLayout from novelwriter.enum import nwAlert, nwItemType
from novelwriter.constants import nwConst
from novelwriter.gui.custom import QHelpLabel from novelwriter.gui.custom import QHelpLabel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -51,7 +50,6 @@ class GuiDocSplit(QDialog):
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.theParent = theParent self.theParent = theParent
self.theProject = theParent.theProject self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.sourceItem = None self.sourceItem = None
self.sourceText = [] self.sourceText = []
@@ -76,7 +74,7 @@ class GuiDocSplit(QDialog):
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(
self.optState.getInt("GuiDocSplit", "spLevel", 3) self.theProject.options.getInt("GuiDocSplit", "spLevel", 3)
) )
if spIndex != -1: if spIndex != -1:
self.splitLevel.setCurrentIndex(spIndex) self.splitLevel.setCurrentIndex(spIndex)
@@ -122,7 +120,7 @@ class GuiDocSplit(QDialog):
), nwAlert.ERROR) ), nwAlert.ERROR)
return False return False
srcItem = self.theProject.projTree[self.sourceItem] srcItem = self.theProject.tree[self.sourceItem]
if srcItem is None: if srcItem is None:
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
"Could not parse source document." "Could not parse source document."
@@ -160,16 +158,6 @@ class GuiDocSplit(QDialog):
), nwAlert.ERROR) ), nwAlert.ERROR)
return False return False
# Check that another folder can be created
parTree = self.theProject.projTree.getItemPath(srcItem.itemParent)
if len(parTree) >= nwConst.MAX_DEPTH - 1:
self.theParent.makeAlert(self.tr(
"Cannot add new folder for the document split. "
"Maximum folder depth has been reached. "
"Please move the file to another level in the project tree."
), nwAlert.ERROR)
return False
msgYes = self.theParent.askQuestion( msgYes = self.theParent.askQuestion(
self.tr("Split Document"), self.tr("Split Document"),
"{0}<br><br>{1}".format( "{0}<br><br>{1}".format(
@@ -186,22 +174,16 @@ class GuiDocSplit(QDialog):
return False return False
# Create the folder # Create the folder
fHandle = self.theProject.newFolder( fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent)
srcItem.itemName, srcItem.itemClass, srcItem.itemParent
)
self.theParent.treeView.revealNewTreeItem(fHandle) self.theParent.treeView.revealNewTreeItem(fHandle)
logger.verbose("Creating folder '%s'", fHandle) logger.verbose("Creating folder '%s'", fHandle)
# Loop through, and create the files # Loop through, and create the files
for wTitle, iStart, iEnd in finalOrder: for wTitle, iStart, iEnd in finalOrder:
isNovel = srcItem.itemClass == nwItemClass.NOVEL
itemLayout = nwItemLayout.DOCUMENT if isNovel else nwItemLayout.NOTE
wTitle = wTitle.lstrip("#").strip() wTitle = wTitle.lstrip("#").strip()
nHandle = self.theProject.newFile(wTitle, srcItem.itemClass, fHandle) nHandle = self.theProject.newFile(wTitle, fHandle)
newItem = self.theProject.projTree[nHandle] newItem = self.theProject.tree[nHandle]
newItem.setLayout(itemLayout)
newItem.setStatus(srcItem.itemStatus) newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport) newItem.setImport(srcItem.itemImport)
logger.verbose( logger.verbose(
@@ -228,7 +210,7 @@ class GuiDocSplit(QDialog):
def _doClose(self): def _doClose(self):
"""Close the dialog window without doing anything. """Close the dialog window without doing anything.
""" """
self.optState.saveSettings() self.theProject.options.saveSettings()
self.close() self.close()
return return
@@ -249,7 +231,7 @@ class GuiDocSplit(QDialog):
if self.sourceItem is None: if self.sourceItem is None:
return False return False
nwItem = self.theProject.projTree[self.sourceItem] nwItem = self.theProject.tree[self.sourceItem]
if nwItem is None: if nwItem is None:
return False return False
@@ -266,7 +248,7 @@ class GuiDocSplit(QDialog):
return False return False
spLevel = self.splitLevel.currentData() spLevel = self.splitLevel.currentData()
self.optState.setValue("GuiDocSplit", "spLevel", spLevel) self.theProject.options.setValue("GuiDocSplit", "spLevel", spLevel)
logger.debug( logger.debug(
"Scanning document '%s' for headings level <= %d", "Scanning document '%s' for headings level <= %d",
self.sourceItem, spLevel self.sourceItem, spLevel
+21 -17
View File
@@ -33,7 +33,7 @@ from PyQt5.QtWidgets import (
) )
from novelwriter.enum import nwItemLayout, nwItemType from novelwriter.enum import nwItemLayout, nwItemType
from novelwriter.constants import trConst, nwLists, nwLabels from novelwriter.constants import trConst, nwLabels
from novelwriter.gui.custom import QSwitch from novelwriter.gui.custom import QSwitch
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -55,7 +55,7 @@ class GuiItemEditor(QDialog):
# Build GUI # Build GUI
## ##
self.theItem = self.theProject.projTree[tHandle] self.theItem = self.theProject.tree[tHandle]
if self.theItem is None: if self.theItem is None:
self.close() self.close()
return return
@@ -74,19 +74,28 @@ class GuiItemEditor(QDialog):
# Item Status # Item Status
self.editStatus = QComboBox() self.editStatus = QComboBox()
self.editStatus.setMinimumWidth(mVd) self.editStatus.setMinimumWidth(mVd)
if self.theItem.itemClass in nwLists.CLS_NOVEL: if self.theItem.isNovelLike():
for sLabel, _, _, sIcon in self.theProject.statusItems: for key, entry in self.theProject.statusItems.items():
self.editStatus.addItem(sIcon, sLabel, sLabel) self.editStatus.addItem(entry["icon"], entry["name"], key)
index = self.editStatus.findData(self.theItem.itemStatus)
if index != -1:
self.editStatus.setCurrentIndex(index)
else: else:
for sLabel, _, _, sIcon in self.theProject.importItems: for key, entry in self.theProject.importItems.items():
self.editStatus.addItem(sIcon, sLabel, sLabel) self.editStatus.addItem(entry["icon"], entry["name"], key)
index = self.editStatus.findData(self.theItem.itemImport)
if index != -1:
self.editStatus.setCurrentIndex(index)
# Item Layout # Item Layout
self.editLayout = QComboBox() self.editLayout = QComboBox()
self.editLayout.setMinimumWidth(mVd) self.editLayout.setMinimumWidth(mVd)
validLayouts = [] validLayouts = []
if self.theItem.itemType == nwItemType.FILE: if self.theItem.itemType == nwItemType.FILE:
if self.theItem.itemClass in nwLists.CLS_NOVEL: if self.theItem.documentAllowed():
validLayouts.append(nwItemLayout.DOCUMENT) validLayouts.append(nwItemLayout.DOCUMENT)
validLayouts.append(nwItemLayout.NOTE) validLayouts.append(nwItemLayout.NOTE)
else: else:
@@ -97,6 +106,10 @@ class GuiItemEditor(QDialog):
if itemLayout in validLayouts: if itemLayout in validLayouts:
self.editLayout.addItem(trConst(nwLabels.LAYOUT_NAME[itemLayout]), itemLayout) self.editLayout.addItem(trConst(nwLabels.LAYOUT_NAME[itemLayout]), itemLayout)
index = self.editLayout.findData(self.theItem.itemLayout)
if index != -1:
self.editLayout.setCurrentIndex(index)
# Export Switch # Export Switch
self.textExport = QLabel(self.tr("Include when building project")) self.textExport = QLabel(self.tr("Include when building project"))
self.editExport = QSwitch() self.editExport = QSwitch()
@@ -116,15 +129,6 @@ class GuiItemEditor(QDialog):
self.editName.setText(self.theItem.itemName) self.editName.setText(self.theItem.itemName)
self.editName.selectAll() self.editName.selectAll()
currStatus, _ = self.theItem.getImportStatus()
statusIdx = self.editStatus.findData(currStatus)
if statusIdx != -1:
self.editStatus.setCurrentIndex(statusIdx)
layoutIdx = self.editLayout.findData(self.theItem.itemLayout)
if layoutIdx != -1:
self.editLayout.setCurrentIndex(layoutIdx)
## ##
# Assemble # Assemble
## ##
+19 -23
View File
@@ -203,6 +203,22 @@ class GuiPreferencesGeneral(QWidget):
self.tr("Requires restart.") self.tr("Requires restart.")
) )
# Editor Theme
self.guiSyntax = QComboBox()
self.guiSyntax.setMinimumWidth(self.mainConf.pxInt(200))
self.theSyntaxes = self.theTheme.listSyntax()
for syntaxFile, syntaxName in self.theSyntaxes:
self.guiSyntax.addItem(syntaxName, syntaxFile)
syntaxIdx = self.guiSyntax.findData(self.mainConf.guiSyntax)
if syntaxIdx != -1:
self.guiSyntax.setCurrentIndex(syntaxIdx)
self.mainForm.addRow(
self.tr("Editor theme"),
self.guiSyntax,
self.tr("Colour theme for the editor and viewer.")
)
# Font Family # Font Family
self.guiFont = QLineEdit() self.guiFont = QLineEdit()
self.guiFont.setReadOnly(True) self.guiFont.setReadOnly(True)
@@ -275,6 +291,7 @@ class GuiPreferencesGeneral(QWidget):
guiLang = self.guiLang.currentData() guiLang = self.guiLang.currentData()
guiTheme = self.guiTheme.currentData() guiTheme = self.guiTheme.currentData()
guiIcons = self.guiIcons.currentData() guiIcons = self.guiIcons.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()
@@ -294,6 +311,7 @@ class GuiPreferencesGeneral(QWidget):
self.mainConf.guiLang = guiLang self.mainConf.guiLang = guiLang
self.mainConf.guiTheme = guiTheme self.mainConf.guiTheme = guiTheme
self.mainConf.guiIcons = guiIcons self.mainConf.guiIcons = guiIcons
self.mainConf.guiSyntax = guiSyntax
self.mainConf.guiFont = guiFont self.mainConf.guiFont = guiFont
self.mainConf.guiFontSize = guiFontSize self.mainConf.guiFontSize = guiFontSize
self.mainConf.emphLabels = emphLabels self.mainConf.emphLabels = emphLabels
@@ -834,25 +852,6 @@ class GuiPreferencesSyntax(QWidget):
self.mainForm.setHelpTextStyle(self.theTheme.helpText) self.mainForm.setHelpTextStyle(self.theTheme.helpText)
self.setLayout(self.mainForm) self.setLayout(self.mainForm)
# Highlighting Theme
# ==================
self.mainForm.addGroupLabel(self.tr("Highlighting Theme"))
self.guiSyntax = QComboBox()
self.guiSyntax.setMinimumWidth(self.mainConf.pxInt(200))
self.theSyntaxes = self.theTheme.listSyntax()
for syntaxFile, syntaxName in self.theSyntaxes:
self.guiSyntax.addItem(syntaxName, syntaxFile)
syntaxIdx = self.guiSyntax.findData(self.mainConf.guiSyntax)
if syntaxIdx != -1:
self.guiSyntax.setCurrentIndex(syntaxIdx)
self.mainForm.addRow(
self.tr("Highlighting theme"),
self.guiSyntax,
self.tr("Colour theme for the editor and viewer.")
)
# Quotes & Dialogue # Quotes & Dialogue
# ================= # =================
self.mainForm.addGroupLabel(self.tr("Quotes & Dialogue")) self.mainForm.addGroupLabel(self.tr("Quotes & Dialogue"))
@@ -902,7 +901,7 @@ class GuiPreferencesSyntax(QWidget):
self.showMultiSpaces = QSwitch() self.showMultiSpaces = QSwitch()
self.showMultiSpaces.setChecked(self.mainConf.showMultiSpaces) self.showMultiSpaces.setChecked(self.mainConf.showMultiSpaces)
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Highlight multiple spaces"), self.tr("Highlight multiple or trailing spaces"),
self.showMultiSpaces, self.showMultiSpaces,
self.tr("Applies to the document editor only.") self.tr("Applies to the document editor only.")
) )
@@ -912,9 +911,6 @@ class GuiPreferencesSyntax(QWidget):
def saveValues(self): def saveValues(self):
"""Save the values set for this tab. """Save the values set for this tab.
""" """
# Highlighting Theme
self.mainConf.guiSyntax = self.guiSyntax.currentData()
# Quotes & Dialogue # Quotes & Dialogue
self.mainConf.highlightQuotes = self.highlightQuotes.isChecked() self.mainConf.highlightQuotes = self.highlightQuotes.isChecked()
self.mainConf.allowOpenSQuote = self.allowOpenSQuote.isChecked() self.mainConf.allowOpenSQuote = self.allowOpenSQuote.isChecked()
+27 -27
View File
@@ -52,18 +52,18 @@ class GuiProjectDetails(PagedDialog):
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.theParent = theParent self.theParent = theParent
self.theProject = theParent.theProject self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.setWindowTitle(self.tr("Project Details")) self.setWindowTitle(self.tr("Project Details"))
wW = self.mainConf.pxInt(600) wW = self.mainConf.pxInt(600)
wH = self.mainConf.pxInt(400) wH = self.mainConf.pxInt(400)
pOptions = self.theProject.options
self.setMinimumWidth(wW) self.setMinimumWidth(wW)
self.setMinimumHeight(wH) self.setMinimumHeight(wH)
self.resize( self.resize(
self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "winWidth", wW)), self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "winWidth", wW)),
self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "winHeight", wH)) self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "winHeight", wH))
) )
self.tabMain = GuiProjectDetailsMain(self.theParent, self.theProject) self.tabMain = GuiProjectDetailsMain(self.theParent, self.theProject)
@@ -120,16 +120,17 @@ class GuiProjectDetails(PagedDialog):
countFrom = self.tabContents.poValue.value() countFrom = self.tabContents.poValue.value()
clearDouble = self.tabContents.dblValue.isChecked() clearDouble = self.tabContents.dblValue.isChecked()
self.optState.setValue("GuiProjectDetails", "winWidth", winWidth) pOptions = self.theProject.options
self.optState.setValue("GuiProjectDetails", "winHeight", winHeight) pOptions.setValue("GuiProjectDetails", "winWidth", winWidth)
self.optState.setValue("GuiProjectDetails", "widthCol0", widthCol0) pOptions.setValue("GuiProjectDetails", "winHeight", winHeight)
self.optState.setValue("GuiProjectDetails", "widthCol1", widthCol1) pOptions.setValue("GuiProjectDetails", "widthCol0", widthCol0)
self.optState.setValue("GuiProjectDetails", "widthCol2", widthCol2) pOptions.setValue("GuiProjectDetails", "widthCol1", widthCol1)
self.optState.setValue("GuiProjectDetails", "widthCol3", widthCol3) pOptions.setValue("GuiProjectDetails", "widthCol2", widthCol2)
self.optState.setValue("GuiProjectDetails", "widthCol4", widthCol4) pOptions.setValue("GuiProjectDetails", "widthCol3", widthCol3)
self.optState.setValue("GuiProjectDetails", "wordsPerPage", wordsPerPage) pOptions.setValue("GuiProjectDetails", "widthCol4", widthCol4)
self.optState.setValue("GuiProjectDetails", "countFrom", countFrom) pOptions.setValue("GuiProjectDetails", "wordsPerPage", wordsPerPage)
self.optState.setValue("GuiProjectDetails", "clearDouble", clearDouble) pOptions.setValue("GuiProjectDetails", "countFrom", countFrom)
pOptions.setValue("GuiProjectDetails", "clearDouble", clearDouble)
return return
@@ -145,7 +146,6 @@ class GuiProjectDetailsMain(QWidget):
self.theParent = theParent self.theParent = theParent
self.theProject = theProject self.theProject = theProject
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
self.theIndex = theParent.theIndex
fPx = self.theTheme.fontPixelSize fPx = self.theTheme.fontPixelSize
fPt = self.theTheme.fontPointSize fPt = self.theTheme.fontPointSize
@@ -245,8 +245,9 @@ class GuiProjectDetailsMain(QWidget):
def updateValues(self): def updateValues(self):
"""Set all the values. """Set all the values.
""" """
hCounts = self.theIndex.getNovelTitleCounts() pIndex = self.theProject.index
nwCount = self.theIndex.getNovelWordCount() hCounts = pIndex.getNovelTitleCounts()
nwCount = pIndex.getNovelWordCount()
edTime = self.theProject.getCurrentEditTime() edTime = self.theProject.getCurrentEditTime()
self.wordCountVal.setText(f"{nwCount:n}") self.wordCountVal.setText(f"{nwCount:n}")
@@ -277,8 +278,6 @@ class GuiProjectDetailsContents(QWidget):
self.theParent = theParent self.theParent = theParent
self.theProject = theProject self.theProject = theProject
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
self.theIndex = theParent.theIndex
self.optState = theProject.optState
# Internal # Internal
self._theToC = [] self._theToC = []
@@ -286,6 +285,7 @@ class GuiProjectDetailsContents(QWidget):
iPx = self.theTheme.baseIconSize iPx = self.theTheme.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
# Contents Tree # Contents Tree
# ============= # =============
@@ -314,11 +314,11 @@ class GuiProjectDetailsContents(QWidget):
treeHeader.setStretchLastSection(True) treeHeader.setStretchLastSection(True)
treeHeader.setMinimumSectionSize(hPx) treeHeader.setMinimumSectionSize(hPx)
wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol0", 200)) wCol0 = self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol0", 200))
wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol1", 60)) wCol1 = self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol1", 60))
wCol2 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol2", 60)) wCol2 = self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol2", 60))
wCol3 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol3", 60)) wCol3 = self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol3", 60))
wCol4 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol4", 90)) wCol4 = self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol4", 90))
self.tocTree.setColumnWidth(0, wCol0) self.tocTree.setColumnWidth(0, wCol0)
self.tocTree.setColumnWidth(1, wCol1) self.tocTree.setColumnWidth(1, wCol1)
@@ -330,9 +330,9 @@ class GuiProjectDetailsContents(QWidget):
# Options # Options
# ======= # =======
wordsPerPage = self.optState.getInt("GuiProjectDetails", "wordsPerPage", 350) wordsPerPage = pOptions.getInt("GuiProjectDetails", "wordsPerPage", 350)
countFrom = self.optState.getInt("GuiProjectDetails", "countFrom", 1) countFrom = pOptions.getInt("GuiProjectDetails", "countFrom", 1)
clearDouble = self.optState.getInt("GuiProjectDetails", "clearDouble", True) clearDouble = pOptions.getInt("GuiProjectDetails", "clearDouble", True)
wordsHelp = ( wordsHelp = (
self.tr("Typical word count for a 5 by 8 inch book page with 11 pt font is 350.") self.tr("Typical word count for a 5 by 8 inch book page with 11 pt font is 350.")
@@ -424,7 +424,7 @@ class GuiProjectDetailsContents(QWidget):
"""Extract the data for the tree. """Extract the data for the tree.
""" """
self._theToC = [] self._theToC = []
self._theToC = self.theIndex.getTableOfContents(2) self._theToC = self.theProject.index.getTableOfContents(2)
self._theToC.append(("", 0, self.tr("END"), 0)) self._theToC.append(("", 0, self.tr("END"), 0))
return return
+110 -75
View File
@@ -35,6 +35,7 @@ from PyQt5.QtWidgets import (
) )
from novelwriter.enum import nwAlert from novelwriter.enum import nwAlert
from novelwriter.common import simplified
from novelwriter.gui.custom import QSwitch, PagedDialog, QConfigLayout from novelwriter.gui.custom import QSwitch, PagedDialog, QConfigLayout
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -51,19 +52,19 @@ class GuiProjectSettings(PagedDialog):
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.theParent = theParent self.theParent = theParent
self.theProject = theParent.theProject self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.theProject.countStatus() self.theProject.countStatus()
self.setWindowTitle(self.tr("Project Settings")) self.setWindowTitle(self.tr("Project Settings"))
wW = self.mainConf.pxInt(570) wW = self.mainConf.pxInt(570)
wH = self.mainConf.pxInt(375) wH = self.mainConf.pxInt(375)
pOptions = self.theProject.options
self.setMinimumWidth(wW) self.setMinimumWidth(wW)
self.setMinimumHeight(wH) self.setMinimumHeight(wH)
self.resize( self.resize(
self.mainConf.pxInt(self.optState.getInt("GuiProjectSettings", "winWidth", wW)), self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winWidth", wW)),
self.mainConf.pxInt(self.optState.getInt("GuiProjectSettings", "winHeight", wH)) self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH))
) )
self.tabMain = GuiProjectEditMain(self.theParent, self.theProject) self.tabMain = GuiProjectEditMain(self.theParent, self.theProject)
@@ -81,6 +82,9 @@ class GuiProjectSettings(PagedDialog):
self.buttonBox.rejected.connect(self._doClose) self.buttonBox.rejected.connect(self._doClose)
self.addControls(self.buttonBox) self.addControls(self.buttonBox)
# Flags
self.spellChanged = False
logger.debug("GuiProjectSettings initialisation complete") logger.debug("GuiProjectSettings initialisation complete")
return return
@@ -103,16 +107,18 @@ class GuiProjectSettings(PagedDialog):
self.theProject.setProjectName(projName) self.theProject.setProjectName(projName)
self.theProject.setBookTitle(bookTitle) self.theProject.setBookTitle(bookTitle)
self.theProject.setBookAuthors(bookAuthors) self.theProject.setBookAuthors(bookAuthors)
self.theProject.setSpellLang(spellLang)
self.theProject.setProjBackup(doBackup) self.theProject.setProjBackup(doBackup)
# Remember this as updating spell dictionary can be expensive
self.spellChanged = self.theProject.setSpellLang(spellLang)
if self.tabStatus.colChanged: if self.tabStatus.colChanged:
statusCol = self.tabStatus.getNewList() newList, delList = self.tabStatus.getNewList()
self.theProject.setStatusColours(statusCol) self.theProject.setStatusColours(newList, delList)
if self.tabImport.colChanged: if self.tabImport.colChanged:
importCol = self.tabImport.getNewList() newList, delList = self.tabImport.getNewList()
self.theProject.setImportColours(importCol) self.theProject.setImportColours(newList, delList)
if self.tabStatus.colChanged or self.tabImport.colChanged: if self.tabStatus.colChanged or self.tabImport.colChanged:
self.theParent.rebuildTrees() self.theParent.rebuildTrees()
@@ -146,11 +152,12 @@ class GuiProjectSettings(PagedDialog):
statusColW = self.mainConf.rpxInt(self.tabStatus.listBox.columnWidth(0)) statusColW = self.mainConf.rpxInt(self.tabStatus.listBox.columnWidth(0))
importColW = self.mainConf.rpxInt(self.tabImport.listBox.columnWidth(0)) importColW = self.mainConf.rpxInt(self.tabImport.listBox.columnWidth(0))
self.optState.setValue("GuiProjectSettings", "winWidth", winWidth) pOptions = self.theProject.options
self.optState.setValue("GuiProjectSettings", "winHeight", winHeight) pOptions.setValue("GuiProjectSettings", "winWidth", winWidth)
self.optState.setValue("GuiProjectSettings", "replaceColW", replaceColW) pOptions.setValue("GuiProjectSettings", "winHeight", winHeight)
self.optState.setValue("GuiProjectSettings", "statusColW", statusColW) pOptions.setValue("GuiProjectSettings", "replaceColW", replaceColW)
self.optState.setValue("GuiProjectSettings", "importColW", importColW) pOptions.setValue("GuiProjectSettings", "statusColW", statusColW)
pOptions.setValue("GuiProjectSettings", "importColW", importColW)
return return
@@ -181,7 +188,7 @@ class GuiProjectEditMain(QWidget):
self.editName.setMaximumWidth(xW) self.editName.setMaximumWidth(xW)
self.editName.setText(self.theProject.projName) self.editName.setText(self.theProject.projName)
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Working title"), self.tr("Project name"),
self.editName, self.editName,
self.tr("Should be set only once.") self.tr("Should be set only once.")
) )
@@ -245,13 +252,16 @@ class GuiProjectEditStatus(QWidget):
COL_LABEL = 0 COL_LABEL = 0
COL_USAGE = 1 COL_USAGE = 1
KEY_ROLE = Qt.UserRole
COL_ROLE = Qt.UserRole + 1
NUM_ROLE = Qt.UserRole + 2
def __init__(self, theParent, theProject, isStatus): def __init__(self, theParent, theProject, isStatus):
QWidget.__init__(self, theParent) QWidget.__init__(self, theParent)
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.theParent = theParent self.theParent = theParent
self.theProject = theProject self.theProject = theProject
self.optState = theProject.optState
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
if isStatus: if isStatus:
@@ -264,13 +274,12 @@ class GuiProjectEditStatus(QWidget):
colSetting = "importColW" colSetting = "importColW"
wCol0 = self.mainConf.pxInt( wCol0 = self.mainConf.pxInt(
self.optState.getInt("GuiProjectSettings", colSetting, 130) self.theProject.options.getInt("GuiProjectSettings", colSetting, 130)
) )
self.colData = [] self.colDeleted = []
self.colCounts = []
self.colChanged = False self.colChanged = False
self.selColour = None self.selColour = QColor(100, 100, 100)
self.iPx = self.theTheme.baseIconSize self.iPx = self.theTheme.baseIconSize
@@ -285,8 +294,8 @@ class GuiProjectEditStatus(QWidget):
self.listBox.setColumnWidth(self.COL_LABEL, wCol0) self.listBox.setColumnWidth(self.COL_LABEL, wCol0)
self.listBox.setIndentation(0) self.listBox.setIndentation(0)
for iName, iCol, nUse, _ in self.theStatus: for key, entry in self.theStatus.items():
self._addItem(iName, iCol, iName, nUse) self._addItem(key, entry["name"], entry["cols"], entry["count"])
# List Controls # List Controls
# ============= # =============
@@ -297,6 +306,12 @@ class GuiProjectEditStatus(QWidget):
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "") self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
self.delButton.clicked.connect(self._delItem) self.delButton.clicked.connect(self._delItem)
self.upButton = QPushButton(self.theTheme.getIcon("up"), "")
self.upButton.clicked.connect(lambda: self._moveItem(-1))
self.dnButton = QPushButton(self.theTheme.getIcon("down"), "")
self.dnButton.clicked.connect(lambda: self._moveItem(1))
# Edit Form # Edit Form
# ========= # =========
@@ -306,7 +321,7 @@ class GuiProjectEditStatus(QWidget):
self.editName.setPlaceholderText(self.tr("Select item to edit")) self.editName.setPlaceholderText(self.tr("Select item to edit"))
self.colPixmap = QPixmap(self.iPx, self.iPx) self.colPixmap = QPixmap(self.iPx, self.iPx)
self.colPixmap.fill(QColor(120, 120, 120)) self.colPixmap.fill(QColor(100, 100, 100))
self.colButton = QPushButton(QIcon(self.colPixmap), self.tr("Colour")) self.colButton = QPushButton(QIcon(self.colPixmap), self.tr("Colour"))
self.colButton.setIconSize(self.colPixmap.rect().size()) self.colButton.setIconSize(self.colPixmap.rect().size())
self.colButton.clicked.connect(self._selectColour) self.colButton.clicked.connect(self._selectColour)
@@ -320,6 +335,8 @@ class GuiProjectEditStatus(QWidget):
self.listControls = QVBoxLayout() self.listControls = QVBoxLayout()
self.listControls.addWidget(self.addButton) self.listControls.addWidget(self.addButton)
self.listControls.addWidget(self.delButton) self.listControls.addWidget(self.delButton)
self.listControls.addWidget(self.upButton)
self.listControls.addWidget(self.dnButton)
self.listControls.addStretch(1) self.listControls.addStretch(1)
self.editBox = QHBoxLayout() self.editBox = QHBoxLayout()
@@ -349,12 +366,15 @@ class GuiProjectEditStatus(QWidget):
if self.colChanged: if self.colChanged:
newList = [] newList = []
for n in range(self.listBox.topLevelItemCount()): for n in range(self.listBox.topLevelItemCount()):
nItem = self.listBox.topLevelItem(n) item = self.listBox.topLevelItem(n)
nIdx = nItem.data(self.COL_LABEL, Qt.UserRole) newList.append({
newList.append(self.colData[nIdx]) "key": item.data(self.COL_LABEL, self.KEY_ROLE),
return newList "name": item.text(self.COL_LABEL),
"cols": item.data(self.COL_LABEL, self.COL_ROLE),
})
return newList, self.colDeleted
return None return [], []
## ##
# User Actions # User Actions
@@ -369,16 +389,16 @@ class GuiProjectEditStatus(QWidget):
) )
if newCol.isValid(): if newCol.isValid():
self.selColour = newCol self.selColour = newCol
colPixmap = QPixmap(self.iPx, self.iPx) pixmap = QPixmap(self.iPx, self.iPx)
colPixmap.fill(newCol) pixmap.fill(newCol)
self.colButton.setIcon(QIcon(colPixmap)) self.colButton.setIcon(QIcon(pixmap))
self.colButton.setIconSize(colPixmap.rect().size()) self.colButton.setIconSize(pixmap.rect().size())
return return
def _newItem(self): def _newItem(self):
"""Create a new status item. """Create a new status item.
""" """
newItem = self._addItem(self.tr("New Item"), (0, 0, 0), None, 0) newItem = self._addItem(None, self.tr("New Item"), (100, 100, 100), 0)
newItem.setBackground(self.COL_LABEL, QBrush(QColor(0, 255, 0, 70))) newItem.setBackground(self.COL_LABEL, QBrush(QColor(0, 255, 0, 70)))
newItem.setBackground(self.COL_USAGE, QBrush(QColor(0, 255, 0, 70))) newItem.setBackground(self.COL_USAGE, QBrush(QColor(0, 255, 0, 70)))
self.colChanged = True self.colChanged = True
@@ -390,14 +410,14 @@ class GuiProjectEditStatus(QWidget):
selItem = self._getSelectedItem() selItem = self._getSelectedItem()
if selItem is not None: if selItem is not None:
iRow = self.listBox.indexOfTopLevelItem(selItem) iRow = self.listBox.indexOfTopLevelItem(selItem)
selIdx = selItem.data(self.COL_LABEL, Qt.UserRole) if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0:
if self.colCounts[selIdx] == 0:
self.listBox.takeTopLevelItem(iRow)
self.colChanged = True
else:
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
"Cannot delete a status item that is in use." "Cannot delete a status item that is in use."
), nwAlert.ERROR) ), nwAlert.ERROR)
else:
self.listBox.takeTopLevelItem(iRow)
self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE))
self.colChanged = True
return return
def _saveItem(self): def _saveItem(self):
@@ -405,50 +425,72 @@ class GuiProjectEditStatus(QWidget):
""" """
selItem = self._getSelectedItem() selItem = self._getSelectedItem()
if selItem is not None: if selItem is not None:
selIdx = selItem.data(self.COL_LABEL, Qt.UserRole) selItem.setText(self.COL_LABEL, simplified(self.editName.text()))
self.colData[selIdx] = (
self.editName.text().strip(),
self.selColour.red(),
self.selColour.green(),
self.selColour.blue(),
self.colData[selIdx][4]
)
selItem.setText(self.COL_LABEL, self.colData[selIdx][0])
selItem.setText(self.COL_USAGE, self._usageString(self.colCounts[selIdx]))
selItem.setIcon(self.COL_LABEL, self.colButton.icon()) selItem.setIcon(self.COL_LABEL, self.colButton.icon())
selItem.setData(self.COL_LABEL, self.COL_ROLE, (
self.selColour.red(), self.selColour.green(), self.selColour.blue()
))
self.editName.setEnabled(False) self.editName.setEnabled(False)
self.colChanged = True self.colChanged = True
return return
def _addItem(self, iName, iCol, oName, nUse): def _addItem(self, key, name, cols, count):
"""Add a status item to the list. """Add a status item to the list.
""" """
newIcon = QPixmap(self.iPx, self.iPx) pixmap = QPixmap(self.iPx, self.iPx)
newIcon.fill(QColor(*iCol)) pixmap.fill(QColor(*cols))
newItem = QTreeWidgetItem()
newItem.setText(self.COL_LABEL, iName) item = QTreeWidgetItem()
newItem.setText(self.COL_USAGE, self._usageString(nUse)) item.setText(self.COL_LABEL, name)
newItem.setIcon(self.COL_LABEL, QIcon(newIcon)) item.setIcon(self.COL_LABEL, QIcon(pixmap))
newItem.setData(self.COL_LABEL, Qt.UserRole, len(self.colData)) item.setData(self.COL_LABEL, self.KEY_ROLE, key)
self.listBox.addTopLevelItem(newItem) item.setData(self.COL_LABEL, self.COL_ROLE, cols)
self.colData.append((iName, iCol[0], iCol[1], iCol[2], oName)) item.setData(self.COL_LABEL, self.NUM_ROLE, count)
self.colCounts.append(nUse) item.setText(self.COL_USAGE, self._usageString(count))
return newItem
self.listBox.addTopLevelItem(item)
return item
def _moveItem(self, step):
"""Move and item up or down step.
"""
selItem = self._getSelectedItem()
if selItem is None:
return
tIndex = self.listBox.indexOfTopLevelItem(selItem)
nChild = self.listBox.topLevelItemCount()
nIndex = tIndex + step
if nIndex < 0 or nIndex >= nChild:
return
cItem = self.listBox.takeTopLevelItem(tIndex)
self.listBox.insertTopLevelItem(nIndex, cItem)
self.listBox.clearSelection()
cItem.setSelected(True)
self.colChanged = True
return
def _selectedItem(self): def _selectedItem(self):
"""Extract the info of a selected item and populate the settings """Extract the info of a selected item and populate the settings
boxes and button. boxes and button.
""" """
selItem = self._getSelectedItem() selItem = self._getSelectedItem()
if selItem is not None: if selItem is None:
selIdx = selItem.data(self.COL_LABEL, Qt.UserRole) return
selVal = self.colData[selIdx]
self.selColour = QColor(selVal[1], selVal[2], selVal[3]) cols = selItem.data(self.COL_LABEL, self.COL_ROLE)
newIcon = QPixmap(self.iPx, self.iPx) name = selItem.text(self.COL_LABEL)
newIcon.fill(self.selColour)
self.editName.setText(selVal[0]) pixmap = QPixmap(self.iPx, self.iPx)
self.colButton.setIcon(QIcon(newIcon)) pixmap.fill(QColor(*cols))
self.selColour = QColor(*cols)
self.editName.setText(name)
self.colButton.setIcon(QIcon(pixmap))
self.editName.setEnabled(True) self.editName.setEnabled(True)
self.editName.selectAll() self.editName.selectAll()
self.editName.setFocus() self.editName.setFocus()
@@ -467,12 +509,6 @@ class GuiProjectEditStatus(QWidget):
return selItem[0] return selItem[0]
return None return None
def _rowsMoved(self):
"""A row has been moved, so set the changed flag.
"""
self.colChanged = True
return
def _usageString(self, nUse): def _usageString(self, nUse):
"""Generate usage string. """Generate usage string.
""" """
@@ -498,11 +534,10 @@ class GuiProjectEditReplace(QWidget):
self.theParent = theParent self.theParent = theParent
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
self.theProject = theProject self.theProject = theProject
self.optState = theProject.optState
self.arChanged = False self.arChanged = False
wCol0 = self.mainConf.pxInt( wCol0 = self.mainConf.pxInt(
self.optState.getInt("GuiProjectSettings", "replaceColW", 130) self.theProject.options.getInt("GuiProjectSettings", "replaceColW", 130)
) )
pageLabel = self.tr("Text Replace List for Preview and Export") pageLabel = self.tr("Text Replace List for Preview and Export")
+6 -5
View File
@@ -52,19 +52,19 @@ class GuiWordList(QDialog):
self.theParent = theParent self.theParent = theParent
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
self.theProject = theParent.theProject self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.setWindowTitle(self.tr("Project Word List")) self.setWindowTitle(self.tr("Project Word List"))
mS = self.mainConf.pxInt(250) mS = self.mainConf.pxInt(250)
wW = self.mainConf.pxInt(320) wW = self.mainConf.pxInt(320)
wH = self.mainConf.pxInt(340) wH = self.mainConf.pxInt(340)
pOptions = self.theProject.options
self.setMinimumWidth(mS) self.setMinimumWidth(mS)
self.setMinimumHeight(mS) self.setMinimumHeight(mS)
self.resize( self.resize(
self.mainConf.pxInt(self.optState.getInt("GuiWordList", "winWidth", wW)), self.mainConf.pxInt(pOptions.getInt("GuiWordList", "winWidth", wW)),
self.mainConf.pxInt(self.optState.getInt("GuiWordList", "winHeight", wH)) self.mainConf.pxInt(pOptions.getInt("GuiWordList", "winHeight", wH))
) )
# Main Widgets # Main Widgets
@@ -207,8 +207,9 @@ class GuiWordList(QDialog):
winWidth = self.mainConf.rpxInt(self.width()) winWidth = self.mainConf.rpxInt(self.width())
winHeight = self.mainConf.rpxInt(self.height()) winHeight = self.mainConf.rpxInt(self.height())
self.optState.setValue("GuiWordList", "winWidth", winWidth) pOptions = self.theProject.options
self.optState.setValue("GuiWordList", "winHeight", winHeight) pOptions.setValue("GuiWordList", "winWidth", winWidth)
pOptions.setValue("GuiWordList", "winHeight", winHeight)
return return
+18 -1
View File
@@ -32,7 +32,6 @@ class nwItemType(Enum):
ROOT = 1 ROOT = 1
FOLDER = 2 FOLDER = 2
FILE = 3 FILE = 3
TRASH = 4
# END Enum nwItemType # END Enum nwItemType
@@ -63,6 +62,14 @@ class nwItemLayout(Enum):
# END Enum nwItemLayout # END Enum nwItemLayout
class nwDocMode(Enum):
VIEW = 0
EDIT = 1
# END Enum nwDocMode
class nwDocAction(Enum): class nwDocAction(Enum):
NO_ACTION = 0 NO_ACTION = 0
@@ -131,6 +138,16 @@ class nwState(Enum):
# END Enum nwState # END Enum nwState
class nwView(Enum):
EDITOR = 0
PROJECT = 1
NOVEL = 2
OUTLINE = 3
# END Enum nwView
class nwWidget(Enum): class nwWidget(Enum):
TREE = 1 TREE = 1
+2
View File
@@ -87,6 +87,8 @@ class NWErrorMessage(QDialog):
self.mainBox.addWidget(self.btnBox, 2, 0, 1, 2) self.mainBox.addWidget(self.btnBox, 2, 0, 1, 2)
self.mainBox.setSpacing(16) self.mainBox.setSpacing(16)
# Pick a random window title from a set of error messages by
# Hex, the computer, from Discworld
self.setWindowTitle([ self.setWindowTitle([
"+++ Out of Cheese Error +++", "+++ Out of Cheese Error +++",
"+++ Divide by Cucumber Error +++", "+++ Divide by Cucumber Error +++",
+2 -2
View File
@@ -25,10 +25,10 @@ from novelwriter.gui.itemdetails import GuiItemDetails
from novelwriter.gui.mainmenu import GuiMainMenu from novelwriter.gui.mainmenu import GuiMainMenu
from novelwriter.gui.noveltree import GuiNovelTree from novelwriter.gui.noveltree import GuiNovelTree
from novelwriter.gui.outline import GuiOutline from novelwriter.gui.outline import GuiOutline
from novelwriter.gui.outlinedetails import GuiOutlineDetails
from novelwriter.gui.projtree import GuiProjectTree from novelwriter.gui.projtree import GuiProjectTree
from novelwriter.gui.statusbar import GuiMainStatus from novelwriter.gui.statusbar import GuiMainStatus
from novelwriter.gui.theme import GuiTheme from novelwriter.gui.theme import GuiTheme
from novelwriter.gui.viewsbar import GuiViewsBar
__all__ = [ __all__ = [
"GuiDocEditor", "GuiDocEditor",
@@ -39,7 +39,7 @@ __all__ = [
"GuiMainStatus", "GuiMainStatus",
"GuiNovelTree", "GuiNovelTree",
"GuiOutline", "GuiOutline",
"GuiOutlineDetails",
"GuiProjectTree", "GuiProjectTree",
"GuiTheme", "GuiTheme",
"GuiViewsBar",
] ]
+5 -5
View File
@@ -409,10 +409,10 @@ class PagedDialog(QDialog):
return return
def addTab(self, tabWidget, tabLabel): def addTab(self, widget, label):
"""Forwards the adding of tabs to the QTabWidget. """Forwards the adding of tabs to the QTabWidget.
""" """
self._tabBox.addTab(tabWidget, tabLabel) self._tabBox.addTab(widget, label)
return return
def addControls(self, buttonBar): def addControls(self, buttonBar):
@@ -431,15 +431,15 @@ class VerticalTabBar(QTabBar):
self._mW = novelwriter.CONFIG.pxInt(150) self._mW = novelwriter.CONFIG.pxInt(150)
return return
def tabSizeHint(self, theIndex): def tabSizeHint(self, index):
"""Returns a transposed size hint for the rotated bar. """Returns a transposed size hint for the rotated bar.
""" """
tSize = QTabBar.tabSizeHint(self, theIndex) tSize = QTabBar.tabSizeHint(self, index)
tSize.transpose() tSize.transpose()
tSize.setWidth(min(tSize.width(), self._mW)) tSize.setWidth(min(tSize.width(), self._mW))
return tSize return tSize
def paintEvent(self, theEvent): def paintEvent(self, event):
"""Custom implementation of the label painter that rotates the """Custom implementation of the label painter that rotates the
label 90 degrees. label 90 degrees.
""" """
+32 -26
View File
@@ -33,6 +33,7 @@ import bisect
import logging import logging
import novelwriter import novelwriter
from enum import Enum
from time import time from time import time
from PyQt5.QtCore import ( from PyQt5.QtCore import (
@@ -50,7 +51,7 @@ from PyQt5.QtWidgets import (
) )
from novelwriter.core import NWDoc, NWSpellEnchant, countWords from novelwriter.core import NWDoc, NWSpellEnchant, countWords
from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode
from novelwriter.common import transferCase from novelwriter.common import transferCase
from novelwriter.constants import nwConst, nwKeyWords, nwUnicode from novelwriter.constants import nwConst, nwKeyWords, nwUnicode
from novelwriter.gui.dochighlight import GuiDocHighlighter from novelwriter.gui.dochighlight import GuiDocHighlighter
@@ -69,6 +70,7 @@ class GuiDocEditor(QTextEdit):
spellDictionaryChanged = pyqtSignal(str, str) spellDictionaryChanged = pyqtSignal(str, str)
docEditedStatusChanged = pyqtSignal(bool) docEditedStatusChanged = pyqtSignal(bool)
docCountsChanged = pyqtSignal(str, int, int, int) docCountsChanged = pyqtSignal(str, int, int, int)
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, theParent): def __init__(self, theParent):
QTextEdit.__init__(self, theParent) QTextEdit.__init__(self, theParent)
@@ -79,7 +81,6 @@ class GuiDocEditor(QTextEdit):
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.theParent = theParent self.theParent = theParent
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
self.theIndex = theParent.theIndex
self.theProject = theParent.theProject self.theProject = theParent.theProject
self._nwDocument = None self._nwDocument = None
@@ -131,8 +132,9 @@ class GuiDocEditor(QTextEdit):
# Editor Settings # Editor Settings
self.setMinimumWidth(self.mainConf.pxInt(300)) self.setMinimumWidth(self.mainConf.pxInt(300))
self.setAutoFillBackground(True)
self.setAcceptRichText(False) self.setAcceptRichText(False)
self.setAutoFillBackground(True)
self.setFrameStyle(QFrame.NoFrame)
# Custom Shortcuts # Custom Shortcuts
QShortcut( QShortcut(
@@ -400,7 +402,7 @@ class GuiDocEditor(QTextEdit):
self.document().rootFrame().setFrameFormat(docFrame) self.document().rootFrame().setFrameFormat(docFrame)
self.docFooter.updateLineCount() self.docFooter.updateLineCount()
self._docHeaders = self.theIndex.getHandleHeaders(self._docHandle) self._docHeaders = self.theProject.index.getHandleHeaders(self._docHandle)
qApp.processEvents() qApp.processEvents()
self.document().clearUndoRedoStacks() self.document().clearUndoRedoStacks()
@@ -505,9 +507,9 @@ class GuiDocEditor(QTextEdit):
self.setDocumentChanged(False) self.setDocumentChanged(False)
oldHeader = self.theIndex.getHandleHeaderLevel(tHandle) oldHeader = self.theProject.index.getHandleHeaderLevel(tHandle)
self.theIndex.scanText(tHandle, docText) self.theProject.index.scanText(tHandle, docText)
newHeader = self.theIndex.getHandleHeaderLevel(tHandle) newHeader = self.theProject.index.getHandleHeaderLevel(tHandle)
if self._updateHeaders(checkLevel=True): if self._updateHeaders(checkLevel=True):
self.theParent.requestNovelTreeRefresh() self.theParent.requestNovelTreeRefresh()
@@ -567,16 +569,6 @@ class GuiDocEditor(QTextEdit):
return return
def updateDocInfo(self, tHandle):
"""Called when an item label is changed to check if the document
title bar needs updating,
"""
if tHandle == self._docHandle:
self.docHeader.setTitleFromHandle(self._docHandle)
self.docFooter.updateInfo()
self.updateDocMargins()
return
## ##
# Properties # Properties
## ##
@@ -1066,7 +1058,22 @@ class GuiDocEditor(QTextEdit):
return return
## ##
# Slots # Public Slots
##
@pyqtSlot(str)
def updateDocInfo(self, tHandle):
"""Called when an item label is changed to check if the document
title bar needs updating,
"""
if tHandle == self._docHandle:
self.docHeader.setTitleFromHandle(self._docHandle)
self.docFooter.updateInfo()
self.updateDocMargins()
return
##
# Private Slots
## ##
@pyqtSlot(int, int, int) @pyqtSlot(int, int, int)
@@ -1894,7 +1901,7 @@ class GuiDocEditor(QTextEdit):
if loadTag: if loadTag:
logger.verbose("Attempting to follow tag '%s'", theWord) logger.verbose("Attempting to follow tag '%s'", theWord)
self.theParent.docViewer.loadFromTag(theWord) self.loadDocumentTagRequest.emit(theWord, nwDocMode.VIEW)
else: else:
logger.verbose("Potential tag '%s'", theWord) logger.verbose("Potential tag '%s'", theWord)
@@ -2002,7 +2009,7 @@ class GuiDocEditor(QTextEdit):
if self._docHandle is None: if self._docHandle is None:
return False return False
newHeaders = self.theIndex.getHandleHeaders(self._docHandle) newHeaders = self.theProject.index.getHandleHeaders(self._docHandle)
if checkPos: if checkPos:
newPos = [x[0] for x in newHeaders] newPos = [x[0] for x in newHeaders]
oldPos = [x[0] for x in self._docHeaders] oldPos = [x[0] for x in self._docHeaders]
@@ -2701,15 +2708,15 @@ class GuiDocEditHeader(QWidget):
if self.mainConf.showFullPath: if self.mainConf.showFullPath:
tTitle = [] tTitle = []
tTree = self.theProject.projTree.getItemPath(tHandle) tTree = self.theProject.tree.getItemPath(tHandle)
for aHandle in reversed(tTree): for aHandle in reversed(tTree):
nwItem = self.theProject.projTree[aHandle] nwItem = self.theProject.tree[aHandle]
if nwItem is not None: if nwItem is not None:
tTitle.append(nwItem.itemName) tTitle.append(nwItem.itemName)
sSep = " %s " % nwUnicode.U_RSAQUO sSep = " %s " % nwUnicode.U_RSAQUO
self.theTitle.setText(sSep.join(tTitle)) self.theTitle.setText(sSep.join(tTitle))
else: else:
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.tree[tHandle]
if nwItem is None: if nwItem is None:
return False return False
self.theTitle.setText(nwItem.itemName) self.theTitle.setText(nwItem.itemName)
@@ -2795,7 +2802,6 @@ class GuiDocEditFooter(QWidget):
self.theParent = docEditor.theParent self.theParent = docEditor.theParent
self.theProject = docEditor.theProject self.theProject = docEditor.theProject
self.theTheme = docEditor.theTheme self.theTheme = docEditor.theTheme
self.optState = docEditor.theProject.optState
self._theItem = None self._theItem = None
self._docHandle = None self._docHandle = None
@@ -2918,7 +2924,7 @@ class GuiDocEditFooter(QWidget):
logger.verbose("No handle set, so clearing the editor footer") logger.verbose("No handle set, so clearing the editor footer")
self._theItem = None self._theItem = None
else: else:
self._theItem = self.theProject.projTree[self._docHandle] self._theItem = self.theProject.tree[self._docHandle]
self.setHasSelection(False) self.setHasSelection(False)
self.updateInfo() self.updateInfo()
@@ -2942,7 +2948,7 @@ class GuiDocEditFooter(QWidget):
else: else:
theStatus, theIcon = self._theItem.getImportStatus() theStatus, theIcon = self._theItem.getImportStatus()
sIcon = theIcon.pixmap(self.sPx, self.sPx) sIcon = theIcon.pixmap(self.sPx, self.sPx)
hLevel = self.theParent.theIndex.getHandleHeaderLevel(self._docHandle) hLevel = self.theProject.index.getHandleHeaderLevel(self._docHandle)
sText = f"{theStatus} / {self._theItem.describeMe(hLevel)}" sText = f"{theStatus} / {self._theItem.describeMe(hLevel)}"
self.statusIcon.setPixmap(sIcon) self.statusIcon.setPixmap(sIcon)
+5 -4
View File
@@ -55,7 +55,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.spEnchant = spEnchant self.spEnchant = spEnchant
self.theParent = theParent self.theParent = theParent
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
self.theIndex = theParent.theIndex self.theProject = theParent.theProject
self.theHandle = None self.theHandle = None
self.spellCheck = False self.spellCheck = False
self.spellRx = None self.spellRx = None
@@ -287,9 +287,10 @@ class GuiDocHighlighter(QSyntaxHighlighter):
if theText.startswith("@"): # Keywords and commands if theText.startswith("@"): # Keywords and commands
self.setCurrentBlockState(self.BLOCK_META) self.setCurrentBlockState(self.BLOCK_META)
tItem = self.theParent.theProject.projTree[self.theHandle] pIndex = self.theProject.index
isValid, theBits, thePos = self.theIndex.scanThis(theText) tItem = self.theParent.theProject.tree[self.theHandle]
isGood = self.theIndex.checkThese(theBits, tItem) isValid, theBits, thePos = pIndex.scanThis(theText)
isGood = pIndex.checkThese(theBits, tItem)
if isValid: if isValid:
for n, theBit in enumerate(theBits): for n, theBit in enumerate(theBits):
xPos = thePos[n] xPos = thePos[n]
+32 -45
View File
@@ -30,17 +30,19 @@ 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, QUrl, QSize, pyqtSlot from enum import Enum
from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot, pyqtSignal
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor
) )
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
qApp, QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton, qApp, QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton,
QAction, QMenu QAction, QMenu, QFrame
) )
from novelwriter.core import ToHtml from novelwriter.core import ToHtml
from novelwriter.enum import nwAlert, nwItemType, nwDocAction from novelwriter.enum import nwItemType, nwDocAction, nwDocMode
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.constants import nwUnicode from novelwriter.constants import nwUnicode
@@ -49,6 +51,8 @@ logger = logging.getLogger(__name__)
class GuiDocViewer(QTextBrowser): class GuiDocViewer(QTextBrowser):
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, theParent): def __init__(self, theParent):
QTextBrowser.__init__(self, theParent) QTextBrowser.__init__(self, theParent)
@@ -68,6 +72,7 @@ class GuiDocViewer(QTextBrowser):
self.setAutoFillBackground(True) self.setAutoFillBackground(True)
self.setOpenExternalLinks(False) self.setOpenExternalLinks(False)
self.setFocusPolicy(Qt.StrongFocus) self.setFocusPolicy(Qt.StrongFocus)
self.setFrameStyle(QFrame.NoFrame)
# Document Header and Footer # Document Header and Footer
self.docHeader = GuiDocViewHeader(self) self.docHeader = GuiDocViewHeader(self)
@@ -159,7 +164,7 @@ class GuiDocViewer(QTextBrowser):
def loadText(self, tHandle, updateHistory=True): def loadText(self, tHandle, updateHistory=True):
"""Load text into the viewer from an item handle. """Load text into the viewer from an item handle.
""" """
if not self.theProject.projTree.checkType(tHandle, nwItemType.FILE): if not self.theProject.tree.checkType(tHandle, nwItemType.FILE):
logger.warning("Item not found") logger.warning("Item not found")
return False return False
@@ -238,30 +243,6 @@ class GuiDocViewer(QTextBrowser):
self.updateDocMargins() self.updateDocMargins()
return return
def loadFromTag(self, theTag):
"""Load text in the document from a reference given by a meta
tag rather than a known handle. This function depends on the
index being up to date.
"""
logger.debug("Loading document from tag '%s'", theTag)
tHandle, _, sTitle = self.theParent.theIndex.getTagSource(theTag)
if tHandle is None:
self.theParent.makeAlert(self.tr(
"Could not find the reference for tag '{0}'. It either doesn't "
"exist, or the index is out of date. The index can be updated "
"from the Tools menu, or by pressing {1}."
).format(
theTag, "F9"
), nwAlert.ERROR)
return False
else:
# Let the parent handle the opening as it also ensures that
# the doc view panel is visible in case this request comes
# from outside this class.
logger.verbose("Tag points to '%s#%s'", tHandle, sTitle)
self.theParent.viewDocument(tHandle, "#%s" % sTitle)
return True
def docAction(self, theAction): def docAction(self, theAction):
"""Wrapper function for various document actions on the current """Wrapper function for various document actions on the current
document. document.
@@ -341,15 +322,6 @@ class GuiDocViewer(QTextBrowser):
return return
def updateDocInfo(self, tHandle):
"""Called when an item label is changed to check if the document
title bar needs updating,
"""
if tHandle == self._docHandle:
self.docHeader.setTitleFromHandle(self._docHandle)
self.updateDocMargins()
return
## ##
# Properties # Properties
## ##
@@ -408,19 +380,33 @@ class GuiDocViewer(QTextBrowser):
return 0 return 0
## ##
# Slots # Public Slots
##
@pyqtSlot(str)
def updateDocInfo(self, tHandle):
"""Called when an item label is changed to check if the document
title bar needs updating,
"""
if tHandle == self._docHandle:
self.docHeader.setTitleFromHandle(self._docHandle)
self.updateDocMargins()
return
##
# Private Slots
## ##
@pyqtSlot("QUrl") @pyqtSlot("QUrl")
def _linkClicked(self, theURL): def _linkClicked(self, theURL):
"""Slot for a link in the document being clicked. """Process a clicked link internally in the document.
""" """
theLink = theURL.url() theLink = theURL.url()
logger.verbose("Clicked link: '%s'", theLink) logger.verbose("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:
self.loadFromTag(theBits[1]) self.loadDocumentTagRequest.emit(theBits[1], nwDocMode.VIEW)
return return
@pyqtSlot("QPoint") @pyqtSlot("QPoint")
@@ -862,15 +848,15 @@ class GuiDocViewHeader(QWidget):
if self.mainConf.showFullPath: if self.mainConf.showFullPath:
tTitle = [] tTitle = []
tTree = self.theProject.projTree.getItemPath(tHandle) tTree = self.theProject.tree.getItemPath(tHandle)
for aHandle in reversed(tTree): for aHandle in reversed(tTree):
nwItem = self.theProject.projTree[aHandle] nwItem = self.theProject.tree[aHandle]
if nwItem is not None: if nwItem is not None:
tTitle.append(nwItem.itemName) tTitle.append(nwItem.itemName)
sSep = " %s " % nwUnicode.U_RSAQUO sSep = " %s " % nwUnicode.U_RSAQUO
self.theTitle.setText(sSep.join(tTitle)) self.theTitle.setText(sSep.join(tTitle))
else: else:
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.tree[tHandle]
if nwItem is None: if nwItem is None:
return False return False
self.theTitle.setText(nwItem.itemName) self.theTitle.setText(nwItem.itemName)
@@ -1185,6 +1171,7 @@ class GuiDocViewDetails(QScrollArea):
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setWidgetResizable(True) self.setWidgetResizable(True)
self.setMinimumHeight(self.mainConf.pxInt(50)) self.setMinimumHeight(self.mainConf.pxInt(50))
self.setFrameStyle(QFrame.NoFrame)
logger.debug("GuiDocViewDetails initialisation complete") logger.debug("GuiDocViewDetails initialisation complete")
@@ -1197,10 +1184,10 @@ class GuiDocViewDetails(QScrollArea):
if self.theParent.docViewer.stickyRef: if self.theParent.docViewer.stickyRef:
return return
theRefs = self.theParent.theIndex.getBackReferenceList(tHandle) theRefs = self.theProject.index.getBackReferenceList(tHandle)
theList = [] theList = []
for tHandle in theRefs: for tHandle in theRefs:
tItem = self.theProject.projTree[tHandle] tItem = self.theProject.tree[tHandle]
if tItem is not None: if tItem is not None:
theList.append("<a href='%s#%s' %s>%s</a>" % ( theList.append("<a href='%s#%s' %s>%s</a>" % (
tHandle, theRefs[tHandle], self.linkStyle, tItem.itemName tHandle, theRefs[tHandle], self.linkStyle, tItem.itemName
+13 -6
View File
@@ -115,6 +115,7 @@ class GuiItemDetails(QWidget):
self.usageData = QLabel("") self.usageData = QLabel("")
self.usageData.setFont(fntValue) self.usageData.setFont(fntValue)
self.usageData.setAlignment(Qt.AlignLeft) self.usageData.setAlignment(Qt.AlignLeft)
self.usageData.setWordWrap(True)
# Character Count # Character Count
self.cCountName = QLabel(" "+self.tr("Characters")) self.cCountName = QLabel(" "+self.tr("Characters"))
@@ -214,6 +215,16 @@ class GuiItemDetails(QWidget):
return return
def refreshDetails(self):
"""Reload the content of the details panel.
"""
self.updateViewBox(self._itemHandle)
##
# Public Slots
##
@pyqtSlot(str)
def updateViewBox(self, tHandle): def updateViewBox(self, tHandle):
"""Populate the details box from a given handle. """Populate the details box from a given handle.
""" """
@@ -221,7 +232,7 @@ class GuiItemDetails(QWidget):
self.clearDetails() self.clearDetails()
return return
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.tree[tHandle]
if nwItem is None: if nwItem is None:
self.clearDetails() self.clearDetails()
return return
@@ -263,7 +274,7 @@ class GuiItemDetails(QWidget):
# Layout # Layout
# ====== # ======
hLevel = self.theParent.theIndex.getHandleHeaderLevel(tHandle) hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
usageIcon = self.theTheme.getItemIcon( usageIcon = self.theTheme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
) )
@@ -284,10 +295,6 @@ class GuiItemDetails(QWidget):
return return
##
# Slots
##
@pyqtSlot(str, int, int, int) @pyqtSlot(str, int, int, int)
def doUpdateCounts(self, tHandle, cC, wC, pC): def doUpdateCounts(self, tHandle, cC, wC, pC):
"""Update the counts if the handle is the same as the one we're """Update the counts if the handle is the same as the one we're
+2 -45
View File
@@ -71,24 +71,6 @@ class GuiMainMenu(QMenuBar):
return return
##
# Methods
##
def setAvailableRoot(self):
"""Update the list of available root folders and set the ones
that are active.
"""
for itemClass in nwItemClass:
if itemClass == nwItemClass.NO_CLASS:
continue
if itemClass == nwItemClass.TRASH:
continue
self.rootItems[itemClass].setVisible(
self.theProject.projTree.checkRootUnique(itemClass)
)
return
## ##
# Update Menu on Settings Changed # Update Menu on Settings Changed
## ##
@@ -99,12 +81,6 @@ class GuiMainMenu(QMenuBar):
self.aSpellCheck.setChecked(theMode) self.aSpellCheck.setChecked(theMode)
return return
def setAutoOutline(self, theMode):
"""Forward auto outline check state to its action.
"""
self.aAutoOutline.setChecked(theMode)
return
def setFocusMode(self, theMode): def setFocusMode(self, theMode):
"""Forward focus mode check state to its action. """Forward focus mode check state to its action.
""" """
@@ -123,12 +99,6 @@ class GuiMainMenu(QMenuBar):
self.theParent.docEditor.toggleSpellCheck(None) self.theParent.docEditor.toggleSpellCheck(None)
return True return True
def _toggleAutoOutline(self, theMode):
"""Toggle auto outline when the menu entry is checked.
"""
self.theProject.setAutoOutline(theMode)
return True
def _openWebsite(self, theUrl): def _openWebsite(self, theUrl):
"""Open a URL in the system's default browser. """Open a URL in the system's default browser.
""" """
@@ -215,7 +185,7 @@ class GuiMainMenu(QMenuBar):
# Project > New Folder # Project > New Folder
self.aCreateFolder = QAction(self.tr("Create Folder"), self) self.aCreateFolder = QAction(self.tr("Create Folder"), self)
self.aCreateFolder.setShortcut("Ctrl+Shift+N") self.aCreateFolder.setShortcut("Ctrl+Shift+N")
self.aCreateFolder.triggered.connect(lambda: self._newTreeItem(nwItemType.FOLDER, None)) self.aCreateFolder.triggered.connect(lambda: self._newTreeItem(nwItemType.FOLDER))
self.projMenu.addAction(self.aCreateFolder) self.projMenu.addAction(self.aCreateFolder)
# Project > Separator # Project > Separator
@@ -277,7 +247,7 @@ class GuiMainMenu(QMenuBar):
# Document > New # Document > New
self.aNewDoc = QAction(self.tr("New Document"), self) self.aNewDoc = QAction(self.tr("New Document"), self)
self.aNewDoc.setShortcut("Ctrl+N") self.aNewDoc.setShortcut("Ctrl+N")
self.aNewDoc.triggered.connect(lambda: self._newTreeItem(nwItemType.FILE, None)) self.aNewDoc.triggered.connect(lambda: self._newTreeItem(nwItemType.FILE))
self.docuMenu.addAction(self.aNewDoc) self.docuMenu.addAction(self.aNewDoc)
# Document > Open # Document > Open
@@ -907,19 +877,6 @@ class GuiMainMenu(QMenuBar):
self.aRebuildIndex.triggered.connect(lambda: self.theParent.rebuildIndex()) self.aRebuildIndex.triggered.connect(lambda: self.theParent.rebuildIndex())
self.toolsMenu.addAction(self.aRebuildIndex) self.toolsMenu.addAction(self.aRebuildIndex)
# Tools > Rebuild Outline
self.aRebuildOutline = QAction(self.tr("Rebuild Outline"), self)
self.aRebuildOutline.setShortcut("F10")
self.aRebuildOutline.triggered.connect(lambda: self.theParent.rebuildOutline())
self.toolsMenu.addAction(self.aRebuildOutline)
# Tools > Toggle Auto Build Outline
self.aAutoOutline = QAction(self.tr("Auto-Update Outline"), self)
self.aAutoOutline.setCheckable(True)
self.aAutoOutline.toggled.connect(self._toggleAutoOutline)
self.aAutoOutline.setShortcut("Ctrl+F10")
self.toolsMenu.addAction(self.aAutoOutline)
# Tools > Separator # Tools > Separator
self.toolsMenu.addSeparator() self.toolsMenu.addSeparator()
+12 -11
View File
@@ -29,7 +29,9 @@ import novelwriter
from time import time from time import time
from PyQt5.QtCore import Qt, QSize from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView from PyQt5.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QFrame
)
from novelwriter.common import checkInt from novelwriter.common import checkInt
from novelwriter.constants import nwKeyWords from novelwriter.constants import nwKeyWords
@@ -52,7 +54,6 @@ class GuiNovelTree(QTreeWidget):
self.theParent = theParent self.theParent = theParent
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
self.theProject = theParent.theProject self.theProject = theParent.theProject
self.theIndex = theParent.theIndex
# Internal Variables # Internal Variables
self._treeMap = {} self._treeMap = {}
@@ -60,6 +61,7 @@ class GuiNovelTree(QTreeWidget):
# Build GUI # Build GUI
iPx = self.theTheme.baseIconSize iPx = self.theTheme.baseIconSize
self.setFrameStyle(QFrame.NoFrame)
self.setIconSize(QSize(iPx, iPx)) self.setIconSize(QSize(iPx, iPx))
self.setIndentation(iPx) self.setIndentation(iPx)
self.setColumnCount(3) self.setColumnCount(3)
@@ -134,7 +136,7 @@ class GuiNovelTree(QTreeWidget):
""" """
logger.verbose("Requesting refresh of the novel tree") logger.verbose("Requesting refresh of the novel tree")
treeChanged = self.theParent.treeView.changedSince(self._lastBuild) treeChanged = self.theParent.treeView.changedSince(self._lastBuild)
indexChanged = self.theIndex.novelChangedSince(self._lastBuild) indexChanged = self.theProject.index.indexChangedSince(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.verbose("No changes have been made to the novel index")
return return
@@ -144,7 +146,6 @@ class GuiNovelTree(QTreeWidget):
if selItem: if selItem:
titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2] titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2]
self.theParent.treeView.flushTreeOrder()
self._populateTree() self._populateTree()
if titleKey is not None and titleKey in self._treeMap: if titleKey is not None and titleKey in self._treeMap:
@@ -155,7 +156,7 @@ class GuiNovelTree(QTreeWidget):
def updateWordCounts(self, tHandle): def updateWordCounts(self, tHandle):
"""Update the word count for a given handle. """Update the word count for a given handle.
""" """
tHeaders = self.theIndex.getHandleWordCounts(tHandle) tHeaders = self.theProject.index.getHandleWordCounts(tHandle)
for titleKey, wCount in tHeaders: for titleKey, wCount in tHeaders:
if titleKey in self._treeMap: if titleKey in self._treeMap:
self._treeMap[titleKey].setText(self.C_WORDS, f"{wCount:n}") self._treeMap[titleKey].setText(self.C_WORDS, f"{wCount:n}")
@@ -249,12 +250,12 @@ class GuiNovelTree(QTreeWidget):
currChapter = None currChapter = None
currScene = None currScene = None
for tKey, tHandle, sTitle, novIdx in self.theIndex.novelStructure(skipExcluded=True): for tKey, tHandle, sTitle, novIdx in self.theProject.index.novelStructure(skipExcl=True):
tItem = self._createTreeItem(tHandle, sTitle, tKey, novIdx) tItem = self._createTreeItem(tHandle, sTitle, tKey, novIdx)
self._treeMap[tKey] = tItem self._treeMap[tKey] = tItem
tLevel = novIdx["level"] tLevel = novIdx.level
if tLevel == "H1": if tLevel == "H1":
self.addTopLevelItem(tItem) self.addTopLevelItem(tItem)
currTitle = tItem currTitle = tItem
@@ -301,18 +302,18 @@ class GuiNovelTree(QTreeWidget):
"""Populate a tree item with all the column values. """Populate a tree item with all the column values.
""" """
newItem = QTreeWidgetItem() newItem = QTreeWidgetItem()
hIcon = "doc_%s" % novIdx["level"].lower() hIcon = "doc_%s" % novIdx.level.lower()
theData = (tHandle, sTitle[1:].lstrip("0"), titleKey) theData = (tHandle, sTitle[1:].lstrip("0"), titleKey)
wC = int(novIdx["wCount"]) wC = int(novIdx.wordCount)
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, Qt.UserRole, theData)
newItem.setIcon(self.C_TITLE, self.theTheme.getIcon(hIcon)) newItem.setIcon(self.C_TITLE, self.theTheme.getIcon(hIcon))
newItem.setText(self.C_WORDS, f"{wC:n}") newItem.setText(self.C_WORDS, f"{wC:n}")
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
theRefs = self.theIndex.getReferences(tHandle, sTitle) theRefs = self.theProject.index.getReferences(tHandle, sTitle)
newItem.setText(self.C_POV, ", ".join(theRefs[nwKeyWords.POV_KEY])) newItem.setText(self.C_POV, ", ".join(theRefs[nwKeyWords.POV_KEY]))
return newItem return newItem
+626 -66
View File
@@ -4,7 +4,11 @@ novelWriter GUI Project Outline
GUI class for the project outline view GUI class for the project outline view
File History: File History:
Created: 2019-11-16 [0.4.1] Created: 2022-05-15 [1.7b1] GuiOutline
Created: 2022-05-22 [1.7b1] GuiOutlineToolBar
Created: 2019-11-16 [0.4.1] GuiOutlineView
Created: 2019-11-16 [0.4.1] GuiOutlineHeaderMenu
Created: 2020-06-02 [0.7.0] GuiOutlineDetails
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen Copyright 20182022, Veronica Berglyd Olsen
@@ -27,20 +31,244 @@ import logging
import novelwriter import novelwriter
from time import time from time import time
from enum import Enum
from PyQt5.QtCore import Qt, QSize, pyqtSlot from PyQt5.QtCore import (
Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP
)
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QMenu, QAction, QAbstractItemView QAbstractItemView, QAction, QGridLayout, QGroupBox, QHBoxLayout, QLabel,
QMenu, QScrollArea, QSplitter, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
QWidget, QFrame, QToolBar, QSizePolicy, QComboBox, QToolButton
) )
from novelwriter.enum import nwItemLayout, nwItemType, nwOutline from novelwriter.enum import (
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 trConst, nwKeyWords, nwLabels
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GuiOutline(QTreeWidget): class GuiOutline(QWidget):
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, theParent):
QWidget.__init__(self, theParent)
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.outlineBar = GuiOutlineToolBar(self)
self.outlineView = GuiOutlineView(self)
self.outlineData = GuiOutlineDetails(self)
self.splitOutline = QSplitter(Qt.Vertical)
self.splitOutline.addWidget(self.outlineView)
self.splitOutline.addWidget(self.outlineData)
self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
# Assemble
self.outerBox = QVBoxLayout()
self.outerBox.setContentsMargins(0, 0, 0, 0)
self.outerBox.addWidget(self.outlineBar)
self.outerBox.addWidget(self.splitOutline)
self.setLayout(self.outerBox)
# Connect Signals
self.outlineView.hiddenStateChanged.connect(self._updateMenuColumns)
self.outlineView.activeItemChanged.connect(self.outlineData.showItem)
self.outlineData.itemTagClicked.connect(self._tagClicked)
self.outlineBar.loadNovelRootRequest.connect(self._rootItemChanged)
self.outlineBar.viewColumnToggled.connect(self.outlineView.menuColumnToggled)
# Function Mappings
self.getSelectedHandle = self.outlineView.getSelectedHandle
return
##
# Methods
##
def splitSizes(self):
return self.splitOutline.sizes()
def clearOutline(self):
self.outlineData.clearDetails()
return
def initOutline(self):
self.outlineView.initOutline()
self.outlineData.initDetails()
return
def closeOutline(self):
self.outlineView.closeOutline()
self.outlineData.updateClasses()
return
def refreshView(self, overRide=False, novelChanged=False):
self.outlineView.refreshTree(overRide=overRide, novelChanged=novelChanged)
return
def treeFocus(self):
return self.outlineView.hasFocus()
def setTreeFocus(self):
return self.outlineView.setFocus()
##
# Public Slots
##
@pyqtSlot(str)
def updateRootItem(self, tHandle):
"""Should be called whenever a root folders changes.
"""
self.outlineBar.populateNovelList()
self.outlineData.updateClasses()
return
##
# Private Slots
##
@pyqtSlot()
def _updateMenuColumns(self):
"""Trigger an update of the toggled state of the column menu
checkboxes whenever a signal is received that the hidden state
of columns has changed.
"""
self.outlineBar.setColumnHiddenState(self.outlineView.hiddenColumns)
return
@pyqtSlot(str)
def _tagClicked(self, link):
"""Capture the click of a tag in the details panel.
"""
if link:
self.loadDocumentTagRequest.emit(link, nwDocMode.VIEW)
return
@pyqtSlot(str)
def _rootItemChanged(self, handle):
"""The root novel handle has changed or needs to be refreshed.
"""
self.outlineView.refreshTree(rootHandle=(handle or None), overRide=True)
return
# END Class GuiOutline
class GuiOutlineToolBar(QToolBar):
loadNovelRootRequest = pyqtSignal(str)
viewColumnToggled = pyqtSignal(bool, Enum)
def __init__(self, theOutline):
QTreeWidget.__init__(self, theOutline)
logger.debug("Initialising GuiOutlineToolBar ...")
self.mainConf = novelwriter.CONFIG
self.theParent = theOutline.theParent
self.theProject = theOutline.theParent.theProject
self.theTheme = theOutline.theParent.theTheme
iPx = self.mainConf.pxInt(22)
mPx = self.mainConf.pxInt(12)
self.setMovable(False)
self.setIconSize(QSize(iPx, iPx))
self.setContentsMargins(0, 0, 0, 0)
self.setStyleSheet("QToolBar {border: 0px;}")
stretch = QWidget(self)
stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Novel Selector
self.novelLabel = QLabel(self.tr("Outline of"))
self.novelLabel.setContentsMargins(0, 0, mPx, 0)
self.novelValue = QComboBox(self)
self.novelValue.setMinimumWidth(self.mainConf.pxInt(200))
self.novelValue.currentIndexChanged.connect(self._novelValueChanged)
# Actions
self.aRefresh = QAction(self.tr("Refresh"), self)
self.aRefresh.setIcon(self.theTheme.getIcon("refresh"))
self.aRefresh.triggered.connect(self._refreshRequested)
# Column Menu
self.mColumns = GuiOutlineHeaderMenu(self)
self.mColumns.columnToggled.connect(
lambda isChecked, tItem: self.viewColumnToggled.emit(isChecked, tItem)
)
self.tbColumns = QToolButton(self)
self.tbColumns.setIcon(self.theTheme.getIcon("menu"))
self.tbColumns.setMenu(self.mColumns)
self.tbColumns.setPopupMode(QToolButton.InstantPopup)
# Assemble
self.addWidget(self.novelLabel)
self.addWidget(self.novelValue)
self.addSeparator()
self.addAction(self.aRefresh)
self.addWidget(self.tbColumns)
self.addWidget(stretch)
logger.debug("GuiOutlineToolBar initialisation complete")
##
# Methods
##
def populateNovelList(self):
"""Fill the novel combo box with a list of all novel folders.
"""
self.novelValue.clear()
tIcon = self.theTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])
for tHandle, nwItem in self.theProject.tree.iterRoots(nwItemClass.NOVEL):
self.novelValue.addItem(tIcon, nwItem.itemName, tHandle)
self.novelValue.insertSeparator(self.novelValue.count())
self.novelValue.addItem(tIcon, self.tr("All Novel Folders"), "")
return
def setColumnHiddenState(self, hiddenState):
"""Forward the change of column hidden states to the menu.
"""
self.mColumns.setHiddenState(hiddenState)
return
##
# Private Slots
##
@pyqtSlot(int)
def _novelValueChanged(self, index):
"""Emit a signal containing the handle of the selected item.
"""
if index >= 0:
self.loadNovelRootRequest.emit(self.novelValue.currentData())
return
@pyqtSlot()
def _refreshRequested(self):
"""Emit a signal containing the handle of the selected item.
"""
self.loadNovelRootRequest.emit(self.novelValue.currentData())
return
# END Class GuiOutlineToolBar
class GuiOutlineView(QTreeWidget):
DEF_WIDTH = { DEF_WIDTH = {
nwOutline.TITLE: 200, nwOutline.TITLE: 200,
@@ -82,19 +310,20 @@ class GuiOutline(QTreeWidget):
nwOutline.SYNOP: False, nwOutline.SYNOP: False,
} }
def __init__(self, theParent): hiddenStateChanged = pyqtSignal()
QTreeWidget.__init__(self, theParent) activeItemChanged = pyqtSignal(str, str)
logger.debug("Initialising GuiOutline ...") def __init__(self, theOutline):
QTreeWidget.__init__(self, theOutline)
logger.debug("Initialising GuiOutlineView ...")
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.theParent = theParent self.theParent = theOutline.theParent
self.theProject = theParent.theProject self.theProject = theOutline.theParent.theProject
self.theTheme = theParent.theTheme self.theTheme = theOutline.theParent.theTheme
self.theIndex = theParent.theIndex
self.optState = theParent.theProject.optState
self.headerMenu = GuiOutlineHeaderMenu(self)
self.setFrameStyle(QFrame.NoFrame)
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.SingleSelection) self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setExpandsOnDoubleClick(False) self.setExpandsOnDoubleClick(False)
@@ -107,8 +336,6 @@ class GuiOutline(QTreeWidget):
self.setIndentation(iPx) self.setIndentation(iPx)
self.treeHead = self.header() self.treeHead = self.header()
self.treeHead.setContextMenuPolicy(Qt.CustomContextMenu)
self.treeHead.customContextMenuRequested.connect(self._headerRightClick)
self.treeHead.sectionMoved.connect(self._columnMoved) self.treeHead.sectionMoved.connect(self._columnMoved)
# Internals # Internals
@@ -122,12 +349,25 @@ class GuiOutline(QTreeWidget):
self.initOutline() self.initOutline()
self.clearOutline() self.clearOutline()
self.headerMenu.setHiddenState(self._colHidden)
logger.debug("GuiOutline initialisation complete") self.hiddenStateChanged.emit()
logger.debug("GuiOutlineView initialisation complete")
return return
##
# Properties
##
@property
def hiddenColumns(self):
return self._colHidden
##
# Methods
##
def initOutline(self): def initOutline(self):
"""Set or update outline settings. """Set or update outline settings.
""" """
@@ -167,7 +407,7 @@ class GuiOutline(QTreeWidget):
return return
def refreshTree(self, overRide=False, novelChanged=False): def refreshTree(self, rootHandle=None, overRide=False, novelChanged=False):
"""Called whenever the Outline tab is activated and controls """Called whenever the Outline tab is activated and controls
what data to load, and if necessary, force a rebuild of the what data to load, and if necessary, force a rebuild of the
tree. tree.
@@ -175,17 +415,17 @@ class GuiOutline(QTreeWidget):
# If it's the first time, we always build # If it's the first time, we always build
if self._firstView or self._firstView and overRide: if self._firstView or self._firstView and overRide:
self._loadHeaderState() self._loadHeaderState()
self._populateTree() self._populateTree(rootHandle)
self._firstView = False self._firstView = False
return return
# 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.theIndex.novelChangedSince(self._lastBuild) indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline
if doBuild or overRide: if doBuild or overRide:
logger.debug("Rebuilding Project Outline") logger.debug("Rebuilding Project Outline")
self._populateTree() self._populateTree(rootHandle)
return return
@@ -221,7 +461,7 @@ class GuiOutline(QTreeWidget):
document editor. document editor.
""" """
tHandle, tLine = self.getSelectedHandle() tHandle, tLine = self.getSelectedHandle()
self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True) self.theParent.openDocument(tHandle, tLine=tLine - 1, doScroll=True)
return return
@pyqtSlot() @pyqtSlot()
@@ -233,18 +473,10 @@ class GuiOutline(QTreeWidget):
if selItems: if selItems:
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole) tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole) sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole)
self.theParent.projMeta.showItem(tHandle, sTitle) self.activeItemChanged.emit(tHandle, sTitle)
self.theParent.treeView.setSelectedHandle(tHandle)
return return
@pyqtSlot("QPoint")
def _headerRightClick(self, clickPos):
"""Show the header column menu.
"""
self.headerMenu.exec_(self.mapToGlobal(clickPos))
return
@pyqtSlot(int, int, int) @pyqtSlot(int, int, int)
def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx): def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx):
"""Make sure the order array is up to date with the actual order """Make sure the order array is up to date with the actual order
@@ -254,9 +486,10 @@ class GuiOutline(QTreeWidget):
self._saveHeaderState() self._saveHeaderState()
return return
def _menuColumnToggled(self, isChecked, theItem): @pyqtSlot(bool, Enum)
def menuColumnToggled(self, isChecked, theItem):
"""Receive the changes to column visibility forwarded by the """Receive the changes to column visibility forwarded by the
header context menu. column selection menu.
""" """
logger.verbose("User toggled Outline column '%s'", theItem.name) logger.verbose("User toggled Outline column '%s'", theItem.name)
if theItem in self._colIdx: if theItem in self._colIdx:
@@ -273,10 +506,12 @@ class GuiOutline(QTreeWidget):
"""Load the state of the main tree header, that is, column order """Load the state of the main tree header, that is, column order
and column width. and column width.
""" """
pOptions = self.theProject.options
# Load whatever we saved last time, regardless of wether it # Load whatever we saved last time, regardless of wether it
# contains the correct names or number of columns. The names # contains the correct names or number of columns. The names
# must be valid though. # must be valid though.
tempOrder = self.optState.getValue("GuiOutline", "headerOrder", []) tempOrder = pOptions.getValue("GuiOutline", "headerOrder", [])
treeOrder = [] treeOrder = []
for hName in tempOrder: for hName in tempOrder:
try: try:
@@ -299,21 +534,21 @@ class GuiOutline(QTreeWidget):
# We load whatever column widths and hidden states we find in # We load whatever column widths and hidden states we find in
# the file, and leave the rest in their default state. # the file, and leave the rest in their default state.
tmpWidth = self.optState.getValue("GuiOutline", "columnWidth", {}) tmpWidth = pOptions.getValue("GuiOutline", "columnWidth", {})
for hName in tmpWidth: for hName in tmpWidth:
try: try:
self._colWidth[nwOutline[hName]] = self.mainConf.pxInt(tmpWidth[hName]) self._colWidth[nwOutline[hName]] = self.mainConf.pxInt(tmpWidth[hName])
except Exception: except Exception:
logger.warning("Ignored unknown outline column '%s'", str(hName)) logger.warning("Ignored unknown outline column '%s'", str(hName))
tmpHidden = self.optState.getValue("GuiOutline", "columnHidden", {}) tmpHidden = pOptions.getValue("GuiOutline", "columnHidden", {})
for hName in tmpHidden: for hName in tmpHidden:
try: try:
self._colHidden[nwOutline[hName]] = tmpHidden[hName] self._colHidden[nwOutline[hName]] = tmpHidden[hName]
except Exception: except Exception:
logger.warning("Ignored unknown outline column '%s'", str(hName)) logger.warning("Ignored unknown outline column '%s'", str(hName))
self.headerMenu.setHiddenState(self._colHidden) self.hiddenStateChanged.emit()
return return
@@ -347,14 +582,15 @@ class GuiOutline(QTreeWidget):
if not logHidden and logWidth > 0: if not logHidden and logWidth > 0:
colWidth[hName] = logWidth colWidth[hName] = logWidth
self.optState.setValue("GuiOutline", "headerOrder", treeOrder) pOptions = self.theProject.options
self.optState.setValue("GuiOutline", "columnWidth", colWidth) pOptions.setValue("GuiOutline", "headerOrder", treeOrder)
self.optState.setValue("GuiOutline", "columnHidden", colHidden) pOptions.setValue("GuiOutline", "columnWidth", colWidth)
self.optState.saveSettings() pOptions.setValue("GuiOutline", "columnHidden", colHidden)
pOptions.saveSettings()
return return
def _populateTree(self): def _populateTree(self, rootHandle):
"""Build the tree based on the project index, and the header """Build the tree based on the project index, and the header
based on the defined constants, default values and user selected based on the defined constants, default values and user selected
width, order and hidden state. All columns are populated, even width, order and hidden state. All columns are populated, even
@@ -387,11 +623,12 @@ class GuiOutline(QTreeWidget):
currChapter = None currChapter = None
currScene = None currScene = None
for tKey, tHandle, sTitle, novIdx in self.theIndex.novelStructure(skipExcluded=True): novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
for _, tHandle, sTitle, novIdx in novStruct:
tItem = self._createTreeItem(tHandle, sTitle, novIdx) tItem = self._createTreeItem(tHandle, sTitle, novIdx)
tLevel = novIdx["level"] tLevel = novIdx.level
if tLevel == "H1": if tLevel == "H1":
self.addTopLevelItem(tItem) self.addTopLevelItem(tItem)
currTitle = tItem currTitle = tItem
@@ -437,26 +674,26 @@ class GuiOutline(QTreeWidget):
def _createTreeItem(self, tHandle, sTitle, novIdx): def _createTreeItem(self, tHandle, sTitle, novIdx):
"""Populate a tree item with all the column values. """Populate a tree item with all the column values.
""" """
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.tree[tHandle]
newItem = QTreeWidgetItem() newItem = QTreeWidgetItem()
hIcon = "doc_%s" % novIdx["level"].lower() hIcon = "doc_%s" % novIdx.level.lower()
hLevel = self.theIndex.getHandleHeaderLevel(tHandle) hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
dIcon = self.theTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, hLevel) dIcon = self.theTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, hLevel)
cC = int(novIdx["cCount"]) cC = int(novIdx.charCount)
wC = int(novIdx["wCount"]) wC = int(novIdx.wordCount)
pC = int(novIdx["pCount"]) pC = int(novIdx.paraCount)
newItem.setText(self._colIdx[nwOutline.TITLE], novIdx["title"]) newItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title)
newItem.setData(self._colIdx[nwOutline.TITLE], Qt.UserRole, tHandle) newItem.setData(self._colIdx[nwOutline.TITLE], Qt.UserRole, tHandle)
newItem.setIcon(self._colIdx[nwOutline.TITLE], self.theTheme.getIcon(hIcon)) newItem.setIcon(self._colIdx[nwOutline.TITLE], self.theTheme.getIcon(hIcon))
newItem.setText(self._colIdx[nwOutline.LEVEL], novIdx["level"]) newItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level)
newItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName) newItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName)
newItem.setIcon(self._colIdx[nwOutline.LABEL], dIcon) newItem.setIcon(self._colIdx[nwOutline.LABEL], dIcon)
newItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0")) newItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0"))
newItem.setData(self._colIdx[nwOutline.LINE], Qt.UserRole, sTitle) newItem.setData(self._colIdx[nwOutline.LINE], Qt.UserRole, sTitle)
newItem.setText(self._colIdx[nwOutline.SYNOP], novIdx["synopsis"]) newItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis)
newItem.setText(self._colIdx[nwOutline.CCOUNT], f"{cC:n}") newItem.setText(self._colIdx[nwOutline.CCOUNT], f"{cC:n}")
newItem.setText(self._colIdx[nwOutline.WCOUNT], f"{wC:n}") newItem.setText(self._colIdx[nwOutline.WCOUNT], f"{wC:n}")
newItem.setText(self._colIdx[nwOutline.PCOUNT], f"{pC:n}") newItem.setText(self._colIdx[nwOutline.PCOUNT], f"{pC:n}")
@@ -464,7 +701,7 @@ class GuiOutline(QTreeWidget):
newItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight) newItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight) newItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
theRefs = self.theIndex.getReferences(tHandle, sTitle) theRefs = self.theProject.index.getReferences(tHandle, sTitle)
newItem.setText(self._colIdx[nwOutline.POV], ", ".join(theRefs[nwKeyWords.POV_KEY])) 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.FOCUS], ", ".join(theRefs[nwKeyWords.FOCUS_KEY]))
newItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY])) newItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY]))
@@ -477,15 +714,16 @@ class GuiOutline(QTreeWidget):
return newItem return newItem
# END Class GuiOutline # END Class GuiOutlineView
class GuiOutlineHeaderMenu(QMenu): class GuiOutlineHeaderMenu(QMenu):
def __init__(self, theParent): columnToggled = pyqtSignal(bool, Enum)
QMenu.__init__(self, theParent)
def __init__(self, theOutline):
QMenu.__init__(self, theOutline)
self.theParent = theParent
self.acceptToggle = True self.acceptToggle = True
mnuHead = QAction(self.tr("Select Columns"), self) mnuHead = QAction(self.tr("Select Columns"), self)
@@ -499,7 +737,7 @@ class GuiOutlineHeaderMenu(QMenu):
self.actionMap[hItem] = QAction(trConst(nwLabels.OUTLINE_COLS[hItem]), self) self.actionMap[hItem] = QAction(trConst(nwLabels.OUTLINE_COLS[hItem]), self)
self.actionMap[hItem].setCheckable(True) self.actionMap[hItem].setCheckable(True)
self.actionMap[hItem].toggled.connect( self.actionMap[hItem].toggled.connect(
lambda isChecked, tItem=hItem: self._columnToggled(isChecked, tItem) lambda isChecked, tItem=hItem: self.columnToggled.emit(isChecked, tItem)
) )
self.addAction(self.actionMap[hItem]) self.addAction(self.actionMap[hItem])
@@ -520,16 +758,338 @@ class GuiOutlineHeaderMenu(QMenu):
return return
# END Class GuiOutlineHeaderMenu
class GuiOutlineDetails(QScrollArea):
LVL_MAP = {
"H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"),
"H2": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Chapter"),
"H3": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Scene"),
"H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"),
}
itemTagClicked = pyqtSignal(str)
def __init__(self, theOutline):
QScrollArea.__init__(self, theOutline)
logger.debug("Initialising GuiOutlineDetails ...")
self.mainConf = novelwriter.CONFIG
self.theOutline = theOutline
self.theParent = theOutline.theParent
self.theProject = theOutline.theParent.theProject
self.theTheme = theOutline.theParent.theTheme
# Sizes
minTitle = 30*self.theTheme.textNWidth
maxTitle = 40*self.theTheme.textNWidth
wCount = self.theTheme.getTextWidth("999,999")
hSpace = int(self.mainConf.pxInt(10))
vSpace = int(self.mainConf.pxInt(4))
# Details Area
self.titleLabel = QLabel("<b>%s</b>" % self.tr("Title"))
self.fileLabel = QLabel("<b>%s</b>" % self.tr("Document"))
self.itemLabel = QLabel("<b>%s</b>" % self.tr("Status"))
self.titleValue = QLabel("")
self.fileValue = QLabel("")
self.itemValue = QLabel("")
self.titleValue.setMinimumWidth(minTitle)
self.titleValue.setMaximumWidth(maxTitle)
self.fileValue.setMinimumWidth(minTitle)
self.fileValue.setMaximumWidth(maxTitle)
self.itemValue.setMinimumWidth(minTitle)
self.itemValue.setMaximumWidth(maxTitle)
# Stats Area
self.cCLabel = QLabel("<b>%s</b>" % self.tr("Characters"))
self.wCLabel = QLabel("<b>%s</b>" % self.tr("Words"))
self.pCLabel = QLabel("<b>%s</b>" % self.tr("Paragraphs"))
self.cCValue = QLabel("")
self.wCValue = QLabel("")
self.pCValue = QLabel("")
self.cCValue.setMinimumWidth(wCount)
self.wCValue.setMinimumWidth(wCount)
self.pCValue.setMinimumWidth(wCount)
self.cCValue.setAlignment(Qt.AlignRight)
self.wCValue.setAlignment(Qt.AlignRight)
self.pCValue.setAlignment(Qt.AlignRight)
# Synopsis
self.synopLabel = QLabel("<b>%s</b>" % self.tr("Synopsis"))
self.synopValue = QLabel("")
self.synopLWrap = QHBoxLayout()
self.synopValue.setWordWrap(True)
self.synopValue.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self.synopLWrap.addWidget(self.synopValue, 1)
# Tags
self.povKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]))
self.focKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]))
self.chrKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY]))
self.pltKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]))
self.timKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.TIME_KEY]))
self.wldKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY]))
self.objKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY]))
self.entKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY]))
self.cstKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY]))
self.povKeyLWrap = QHBoxLayout()
self.focKeyLWrap = QHBoxLayout()
self.chrKeyLWrap = QHBoxLayout()
self.pltKeyLWrap = QHBoxLayout()
self.timKeyLWrap = QHBoxLayout()
self.wldKeyLWrap = QHBoxLayout()
self.objKeyLWrap = QHBoxLayout()
self.entKeyLWrap = QHBoxLayout()
self.cstKeyLWrap = QHBoxLayout()
self.povKeyValue = QLabel("")
self.focKeyValue = QLabel("")
self.chrKeyValue = QLabel("")
self.pltKeyValue = QLabel("")
self.timKeyValue = QLabel("")
self.wldKeyValue = QLabel("")
self.objKeyValue = QLabel("")
self.entKeyValue = QLabel("")
self.cstKeyValue = QLabel("")
self.povKeyValue.setWordWrap(True)
self.focKeyValue.setWordWrap(True)
self.chrKeyValue.setWordWrap(True)
self.pltKeyValue.setWordWrap(True)
self.timKeyValue.setWordWrap(True)
self.wldKeyValue.setWordWrap(True)
self.objKeyValue.setWordWrap(True)
self.entKeyValue.setWordWrap(True)
self.cstKeyValue.setWordWrap(True)
def tagClicked(link):
self.itemTagClicked.emit(link)
self.povKeyValue.linkActivated.connect(tagClicked)
self.focKeyValue.linkActivated.connect(tagClicked)
self.chrKeyValue.linkActivated.connect(tagClicked)
self.pltKeyValue.linkActivated.connect(tagClicked)
self.timKeyValue.linkActivated.connect(tagClicked)
self.wldKeyValue.linkActivated.connect(tagClicked)
self.objKeyValue.linkActivated.connect(tagClicked)
self.entKeyValue.linkActivated.connect(tagClicked)
self.cstKeyValue.linkActivated.connect(tagClicked)
self.povKeyLWrap.addWidget(self.povKeyValue, 1)
self.focKeyLWrap.addWidget(self.focKeyValue, 1)
self.chrKeyLWrap.addWidget(self.chrKeyValue, 1)
self.pltKeyLWrap.addWidget(self.pltKeyValue, 1)
self.timKeyLWrap.addWidget(self.timKeyValue, 1)
self.wldKeyLWrap.addWidget(self.wldKeyValue, 1)
self.objKeyLWrap.addWidget(self.objKeyValue, 1)
self.entKeyLWrap.addWidget(self.entKeyValue, 1)
self.cstKeyLWrap.addWidget(self.cstKeyValue, 1)
# Selected Item Details
self.mainGroup = QGroupBox(self.tr("Title Details"), self)
self.mainForm = QGridLayout()
self.mainGroup.setLayout(self.mainForm)
self.mainForm.addWidget(self.titleLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.titleValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.cCLabel, 0, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.cCValue, 0, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.fileLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.fileValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.wCLabel, 1, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.wCValue, 1, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.itemLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.itemValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.pCLabel, 2, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.pCValue, 2, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.synopLabel, 3, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addLayout(self.synopLWrap, 4, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.setColumnStretch(1, 1)
self.mainForm.setRowStretch(4, 1)
self.mainForm.setHorizontalSpacing(hSpace)
self.mainForm.setVerticalSpacing(vSpace)
# Selected Item Tags
self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self)
self.tagsForm = QGridLayout()
self.tagsGroup.setLayout(self.tagsForm)
self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.povKeyLWrap, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.focKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.focKeyLWrap, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.chrKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.chrKeyLWrap, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.pltKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.pltKeyLWrap, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.timKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.timKeyLWrap, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.wldKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.wldKeyLWrap, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.objKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.objKeyLWrap, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.entKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.entKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.cstKeyLabel, 8, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.cstKeyLWrap, 8, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.setColumnStretch(1, 1)
self.tagsForm.setRowStretch(8, 1)
self.tagsForm.setHorizontalSpacing(hSpace)
self.tagsForm.setVerticalSpacing(vSpace)
# Assemble
self.outerWidget = QWidget()
self.outerBox = QHBoxLayout()
self.outerBox.addWidget(self.mainGroup, 0)
self.outerBox.addWidget(self.tagsGroup, 1)
self.outerWidget.setLayout(self.outerBox)
self.setWidget(self.outerWidget)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setWidgetResizable(True)
self.setFrameStyle(QFrame.NoFrame)
self.initDetails()
logger.debug("GuiOutlineDetails initialisation complete")
return
def initDetails(self):
"""Set or update outline settings.
"""
# Scroll bars
if self.mainConf.hideVScroll:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
if self.mainConf.hideHScroll:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.updateClasses()
return
def clearDetails(self):
"""Clear all the data labels.
"""
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText("")
self.fileValue.setText("")
self.itemValue.setText("")
self.cCValue.setText("")
self.wCValue.setText("")
self.pCValue.setText("")
self.synopValue.setText("")
self.povKeyValue.setText("")
self.focKeyValue.setText("")
self.chrKeyValue.setText("")
self.pltKeyValue.setText("")
self.timKeyValue.setText("")
self.wldKeyValue.setText("")
self.objKeyValue.setText("")
self.entKeyValue.setText("")
self.cstKeyValue.setText("")
self.updateClasses()
return
## ##
# Slots # Slots
## ##
def _columnToggled(self, isChecked, theItem): @pyqtSlot(str, str)
"""The user has toggled the visibility of a column. Forward the def showItem(self, tHandle, sTitle):
event to the parent class only if we're accepting changes. """Update the content of the tree with the given handle and line
number pointing to a header.
""" """
if self.acceptToggle: pIndex = self.theProject.index
self.theParent._menuColumnToggled(isChecked, theItem) nwItem = self.theProject.tree[tHandle]
novIdx = pIndex.getNovelData(tHandle, sTitle)
theRefs = pIndex.getReferences(tHandle, sTitle)
if nwItem is None or novIdx is None:
return False
if novIdx.level in self.LVL_MAP:
self.titleLabel.setText("<b>%s</b>" % self.tr(self.LVL_MAP[novIdx.level]))
else:
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText(novIdx.title)
itemStatus, _ = nwItem.getImportStatus()
self.fileValue.setText(nwItem.itemName)
self.itemValue.setText(itemStatus)
cC = checkInt(novIdx.charCount, 0)
wC = checkInt(novIdx.wordCount, 0)
pC = checkInt(novIdx.paraCount, 0)
self.cCValue.setText(f"{cC:n}")
self.wCValue.setText(f"{wC:n}")
self.pCValue.setText(f"{pC:n}")
self.synopValue.setText(novIdx.synopsis)
self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY))
self.focKeyValue.setText(self._formatTags(theRefs, nwKeyWords.FOCUS_KEY))
self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY))
self.pltKeyValue.setText(self._formatTags(theRefs, nwKeyWords.PLOT_KEY))
self.timKeyValue.setText(self._formatTags(theRefs, nwKeyWords.TIME_KEY))
self.wldKeyValue.setText(self._formatTags(theRefs, nwKeyWords.WORLD_KEY))
self.objKeyValue.setText(self._formatTags(theRefs, nwKeyWords.OBJECT_KEY))
self.entKeyValue.setText(self._formatTags(theRefs, nwKeyWords.ENTITY_KEY))
self.cstKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CUSTOM_KEY))
return True
@pyqtSlot()
def updateClasses(self):
"""Update the visibility status of class details.
"""
usedClasses = self.theProject.tree.rootClasses()
pltVisible = nwItemClass.PLOT in usedClasses
timVisible = nwItemClass.TIMELINE in usedClasses
wldVisible = nwItemClass.WORLD in usedClasses
objVisible = nwItemClass.OBJECT in usedClasses
entVisible = nwItemClass.ENTITY in usedClasses
cstVisible = nwItemClass.CUSTOM in usedClasses
self.pltKeyLabel.setVisible(pltVisible)
self.pltKeyValue.setVisible(pltVisible)
self.timKeyLabel.setVisible(timVisible)
self.timKeyValue.setVisible(timVisible)
self.wldKeyLabel.setVisible(wldVisible)
self.wldKeyValue.setVisible(wldVisible)
self.objKeyLabel.setVisible(objVisible)
self.objKeyValue.setVisible(objVisible)
self.entKeyLabel.setVisible(entVisible)
self.entKeyValue.setVisible(entVisible)
self.cstKeyLabel.setVisible(cstVisible)
self.cstKeyValue.setVisible(cstVisible)
return return
# END Class GuiOutlineHeaderMenu @staticmethod
def _formatTags(refs, key):
"""Convert a list of tags into a list of clickable tag links.
"""
return ", ".join(
[f"<a href='{tag}'>{tag}</a>" for tag in refs.get(key, [])]
)
# END Class GuiOutlineDetails
-349
View File
@@ -1,349 +0,0 @@
"""
novelWriter GUI Project Outline Details
=========================================
GUI class for the project outline details panel
File History:
Created: 2020-06-02 [0.7.0]
This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import novelwriter
from PyQt5.QtCore import Qt, QT_TRANSLATE_NOOP
from PyQt5.QtWidgets import (
QScrollArea, QWidget, QGridLayout, QHBoxLayout, QGroupBox, QLabel
)
from novelwriter.common import checkInt
from novelwriter.constants import trConst, nwKeyWords, nwLabels
logger = logging.getLogger(__name__)
class GuiOutlineDetails(QScrollArea):
LVL_MAP = {
"H1": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Title"),
"H2": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Chapter"),
"H3": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Scene"),
"H4": QT_TRANSLATE_NOOP("GuiOutlineDetails", "Section"),
}
def __init__(self, theParent):
QScrollArea.__init__(self, theParent)
logger.debug("Initialising GuiOutlineDetails ...")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
self.theTheme = theParent.theTheme
self.theIndex = theParent.theIndex
self.optState = theParent.theProject.optState
# Sizes
minTitle = 30*self.theTheme.textNWidth
maxTitle = 40*self.theTheme.textNWidth
wCount = self.theTheme.getTextWidth("999,999")
hSpace = int(self.mainConf.pxInt(10))
vSpace = int(self.mainConf.pxInt(4))
# Details Area
self.titleLabel = QLabel("<b>%s</b>" % self.tr("Title"))
self.fileLabel = QLabel("<b>%s</b>" % self.tr("Document"))
self.itemLabel = QLabel("<b>%s</b>" % self.tr("Status"))
self.titleValue = QLabel("")
self.fileValue = QLabel("")
self.itemValue = QLabel("")
self.titleValue.setMinimumWidth(minTitle)
self.titleValue.setMaximumWidth(maxTitle)
self.fileValue.setMinimumWidth(minTitle)
self.fileValue.setMaximumWidth(maxTitle)
self.itemValue.setMinimumWidth(minTitle)
self.itemValue.setMaximumWidth(maxTitle)
# Stats Area
self.cCLabel = QLabel("<b>%s</b>" % self.tr("Characters"))
self.wCLabel = QLabel("<b>%s</b>" % self.tr("Words"))
self.pCLabel = QLabel("<b>%s</b>" % self.tr("Paragraphs"))
self.cCValue = QLabel("")
self.wCValue = QLabel("")
self.pCValue = QLabel("")
self.cCValue.setMinimumWidth(wCount)
self.wCValue.setMinimumWidth(wCount)
self.pCValue.setMinimumWidth(wCount)
self.cCValue.setAlignment(Qt.AlignRight)
self.wCValue.setAlignment(Qt.AlignRight)
self.pCValue.setAlignment(Qt.AlignRight)
# Synopsis
self.synopLabel = QLabel("<b>%s</b>" % self.tr("Synopsis"))
self.synopValue = QLabel("")
self.synopLWrap = QHBoxLayout()
self.synopValue.setWordWrap(True)
self.synopValue.setAlignment(Qt.AlignTop | Qt.AlignLeft)
self.synopLWrap.addWidget(self.synopValue, 1)
# Tags
self.povKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]))
self.focKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]))
self.chrKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY]))
self.pltKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]))
self.timKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.TIME_KEY]))
self.wldKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY]))
self.objKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY]))
self.entKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY]))
self.cstKeyLabel = QLabel("<b>%s</b>" % trConst(nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY]))
self.povKeyLWrap = QHBoxLayout()
self.focKeyLWrap = QHBoxLayout()
self.chrKeyLWrap = QHBoxLayout()
self.pltKeyLWrap = QHBoxLayout()
self.timKeyLWrap = QHBoxLayout()
self.wldKeyLWrap = QHBoxLayout()
self.objKeyLWrap = QHBoxLayout()
self.entKeyLWrap = QHBoxLayout()
self.cstKeyLWrap = QHBoxLayout()
self.povKeyValue = QLabel("")
self.focKeyValue = QLabel("")
self.chrKeyValue = QLabel("")
self.pltKeyValue = QLabel("")
self.timKeyValue = QLabel("")
self.wldKeyValue = QLabel("")
self.objKeyValue = QLabel("")
self.entKeyValue = QLabel("")
self.cstKeyValue = QLabel("")
self.povKeyValue.setWordWrap(True)
self.focKeyValue.setWordWrap(True)
self.chrKeyValue.setWordWrap(True)
self.pltKeyValue.setWordWrap(True)
self.timKeyValue.setWordWrap(True)
self.wldKeyValue.setWordWrap(True)
self.objKeyValue.setWordWrap(True)
self.entKeyValue.setWordWrap(True)
self.cstKeyValue.setWordWrap(True)
self.povKeyValue.linkActivated.connect(self._tagClicked)
self.focKeyValue.linkActivated.connect(self._tagClicked)
self.chrKeyValue.linkActivated.connect(self._tagClicked)
self.pltKeyValue.linkActivated.connect(self._tagClicked)
self.timKeyValue.linkActivated.connect(self._tagClicked)
self.wldKeyValue.linkActivated.connect(self._tagClicked)
self.objKeyValue.linkActivated.connect(self._tagClicked)
self.entKeyValue.linkActivated.connect(self._tagClicked)
self.cstKeyValue.linkActivated.connect(self._tagClicked)
self.povKeyLWrap.addWidget(self.povKeyValue, 1)
self.focKeyLWrap.addWidget(self.focKeyValue, 1)
self.chrKeyLWrap.addWidget(self.chrKeyValue, 1)
self.pltKeyLWrap.addWidget(self.pltKeyValue, 1)
self.timKeyLWrap.addWidget(self.timKeyValue, 1)
self.wldKeyLWrap.addWidget(self.wldKeyValue, 1)
self.objKeyLWrap.addWidget(self.objKeyValue, 1)
self.entKeyLWrap.addWidget(self.entKeyValue, 1)
self.cstKeyLWrap.addWidget(self.cstKeyValue, 1)
# Selected Item Details
self.mainGroup = QGroupBox(self.tr("Title Details"), self)
self.mainForm = QGridLayout()
self.mainGroup.setLayout(self.mainForm)
self.mainForm.addWidget(self.titleLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.titleValue, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.cCLabel, 0, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.cCValue, 0, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.fileLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.fileValue, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.wCLabel, 1, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.wCValue, 1, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.itemLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.itemValue, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.pCLabel, 2, 2, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addWidget(self.pCValue, 2, 3, 1, 1, Qt.AlignTop | Qt.AlignRight)
self.mainForm.addWidget(self.synopLabel, 3, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.addLayout(self.synopLWrap, 4, 0, 1, 4, Qt.AlignTop | Qt.AlignLeft)
self.mainForm.setColumnStretch(1, 1)
self.mainForm.setRowStretch(4, 1)
self.mainForm.setHorizontalSpacing(hSpace)
self.mainForm.setVerticalSpacing(vSpace)
# Selected Item Tags
self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self)
self.tagsForm = QGridLayout()
self.tagsGroup.setLayout(self.tagsForm)
self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.povKeyLWrap, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.focKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.focKeyLWrap, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.chrKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.chrKeyLWrap, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.pltKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.pltKeyLWrap, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.timKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.timKeyLWrap, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.wldKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.wldKeyLWrap, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.objKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.objKeyLWrap, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.entKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.entKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.cstKeyLabel, 8, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.cstKeyLWrap, 8, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.setColumnStretch(1, 1)
self.tagsForm.setRowStretch(8, 1)
self.tagsForm.setHorizontalSpacing(hSpace)
self.tagsForm.setVerticalSpacing(vSpace)
# Assemble
self.outerWidget = QWidget()
self.outerBox = QHBoxLayout()
self.outerBox.addWidget(self.mainGroup, 0)
self.outerBox.addWidget(self.tagsGroup, 1)
self.outerWidget.setLayout(self.outerBox)
self.setWidget(self.outerWidget)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setWidgetResizable(True)
self.initDetails()
logger.debug("GuiOutlineDetails initialisation complete")
return
def initDetails(self):
"""Set or update outline settings.
"""
# Scroll bars
if self.mainConf.hideVScroll:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
if self.mainConf.hideHScroll:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
return
def clearDetails(self):
"""Clear all the data labels.
"""
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText("")
self.fileValue.setText("")
self.itemValue.setText("")
self.cCValue.setText("")
self.wCValue.setText("")
self.pCValue.setText("")
self.synopValue.setText("")
self.povKeyValue.setText("")
self.focKeyValue.setText("")
self.chrKeyValue.setText("")
self.pltKeyValue.setText("")
self.timKeyValue.setText("")
self.wldKeyValue.setText("")
self.objKeyValue.setText("")
self.entKeyValue.setText("")
self.cstKeyValue.setText("")
return
def showItem(self, tHandle, sTitle):
"""Update the content of the tree with the given handle and line
number pointing to a header.
"""
nwItem = self.theProject.projTree[tHandle]
novIdx = self.theIndex.getNovelData(tHandle, sTitle)
theRefs = self.theIndex.getReferences(tHandle, sTitle)
if nwItem is None or novIdx is None:
return False
if novIdx["level"] in self.LVL_MAP:
self.titleLabel.setText("<b>%s</b>" % self.tr(self.LVL_MAP[novIdx["level"]]))
else:
self.titleLabel.setText("<b>%s</b>" % self.tr("Title"))
self.titleValue.setText(novIdx["title"])
self.fileValue.setText(nwItem.itemName)
self.itemValue.setText(nwItem.itemStatus)
cC = checkInt(novIdx["cCount"], 0)
wC = checkInt(novIdx["wCount"], 0)
pC = checkInt(novIdx["pCount"], 0)
self.cCValue.setText(f"{cC:n}")
self.wCValue.setText(f"{wC:n}")
self.pCValue.setText(f"{pC:n}")
self.synopValue.setText(novIdx["synopsis"])
self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY))
self.focKeyValue.setText(self._formatTags(theRefs, nwKeyWords.FOCUS_KEY))
self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY))
self.pltKeyValue.setText(self._formatTags(theRefs, nwKeyWords.PLOT_KEY))
self.timKeyValue.setText(self._formatTags(theRefs, nwKeyWords.TIME_KEY))
self.wldKeyValue.setText(self._formatTags(theRefs, nwKeyWords.WORLD_KEY))
self.objKeyValue.setText(self._formatTags(theRefs, nwKeyWords.OBJECT_KEY))
self.entKeyValue.setText(self._formatTags(theRefs, nwKeyWords.ENTITY_KEY))
self.cstKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CUSTOM_KEY))
return True
##
# Slots
##
def _tagClicked(self, theLink):
"""Capture the click of a tag in the right-most column.
"""
logger.verbose("Clicked link: '%s'", theLink)
if len(theLink) > 0:
theBits = theLink.split("=")
if len(theBits) == 2:
self.theParent.docViewer.loadFromTag(theBits[1])
return
##
# Internal Functions
##
def _formatTags(self, theRefs, theKey):
"""Format the tags as clickable links.
"""
if theKey not in theRefs:
return ""
refTags = []
for tTag in theRefs[theKey]:
refTags.append("<a href='#%s=%s'>%s</a>" % (
theKey[1:], tTag, tTag
))
return ", ".join(refTags)
# END Class GuiOutlineDetails
+283 -348
View File
@@ -32,12 +32,14 @@ from time import time
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame,
QDialog
) )
from novelwriter.core import NWDoc from novelwriter.core import NWDoc
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
from novelwriter.constants import nwConst, trConst, nwLists, nwLabels from novelwriter.common import minmax
from novelwriter.dialogs.itemeditor import GuiItemEditor
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -49,8 +51,9 @@ class GuiProjectTree(QTreeWidget):
C_EXPORT = 2 C_EXPORT = 2
C_STATUS = 3 C_STATUS = 3
novelItemChanged = pyqtSignal() treeItemChanged = pyqtSignal(str)
noteItemChanged = pyqtSignal() novelItemChanged = pyqtSignal(str)
rootFolderChanged = pyqtSignal(str)
wordCountsChanged = pyqtSignal() wordCountsChanged = pyqtSignal()
def __init__(self, theParent): def __init__(self, theParent):
@@ -62,13 +65,11 @@ class GuiProjectTree(QTreeWidget):
self.theParent = theParent self.theParent = theParent
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
self.theProject = theParent.theProject self.theProject = theParent.theProject
self.theIndex = theParent.theIndex
# Internal Variables # Internal Variables
self._treeMap = {} self._treeMap = {}
self._treeChanged = False
self._timeChanged = 0
self._lastMove = {} self._lastMove = {}
self._timeChanged = 0
## ##
# Build GUI # Build GUI
@@ -82,7 +83,8 @@ class GuiProjectTree(QTreeWidget):
# Tree Settings # Tree Settings
iPx = self.theTheme.baseIconSize iPx = self.theTheme.baseIconSize
self.setIconSize(QSize(iPx, iPx)) self.setIconSize(QSize(iPx, iPx))
self.setExpandsOnDoubleClick(True) self.setFrameStyle(QFrame.NoFrame)
self.setExpandsOnDoubleClick(False)
self.setIndentation(iPx) self.setIndentation(iPx)
self.setColumnCount(4) self.setColumnCount(4)
self.setHeaderLabels([ self.setHeaderLabels([
@@ -157,147 +159,97 @@ class GuiProjectTree(QTreeWidget):
""" """
self.clear() self.clear()
self._treeMap = {} self._treeMap = {}
self._treeChanged = False self._lastMove = {}
self._timeChanged = 0 self._timeChanged = 0
return return
def newTreeItem(self, itemType, itemClass): def newTreeItem(self, itemType, itemClass=None):
"""Add new item to the tree, with a given itemType and """Add new item to the tree, with a given itemType (and
itemClass, and attach it to the selected handle. Also make sure itemClass if Root), and attach it to the selected handle. Also
the item is added in a place it can be added, and that other make sure the item is added in a place it can be added, and that
meta data is set correctly to ensure a valid project tree. other meta data is set correctly to ensure a valid project tree.
""" """
pHandle = self.getSelectedHandle()
nHandle = None
if not self.theParent.hasProject: if not self.theParent.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
if not isinstance(itemType, nwItemType): nHandle = None
# This would indicate an internal bug tHandle = None
logger.error("No itemType provided")
return False
# The item needs to be assigned an item class, so one must be if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass):
# provided, or it must be possible to extract it from the parent
# item of the new item.
if itemClass is None and pHandle is not None:
pItem = self.theProject.projTree[pHandle]
if pItem is not None:
itemClass = pItem.itemClass
# If class is still not set, alert the user and exit tHandle = self.theProject.newRoot(itemClass)
if itemClass is None:
if itemType == nwItemType.FILE:
self.theParent.makeAlert(self.tr(
"Please select a valid location in the tree to add the document."
), nwAlert.ERROR)
else:
self.theParent.makeAlert(self.tr(
"Please select a valid location in the tree to add the folder."
), nwAlert.ERROR)
return False
# Everything is fine, we have what we need, so we proceed elif itemType in (nwItemType.FILE, nwItemType.FOLDER):
logger.verbose(
"Adding new item of type '%s' and class '%s' to handle '%s'",
itemType.name, itemClass.name, str(pHandle)
)
if itemType == nwItemType.ROOT: sHandle = self.getSelectedHandle()
tHandle = self.theProject.newRoot( if sHandle is None or sHandle not in self.theProject.tree:
trConst(nwLabels.CLASS_NAME[itemClass]), itemClass
)
if tHandle is None:
logger.error("No root item added")
return False
else:
# If no parent has been selected, make the new file under
# the root NOVEL item.
if pHandle is None:
pHandle = self.theProject.projTree.findRoot(nwItemClass.NOVEL)
# If still nothing, give up
if pHandle is None:
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
"Did not find anywhere to add the file or folder!" "Did not find anywhere to add the file or folder!"
), nwAlert.ERROR) ), nwAlert.ERROR)
return False return False
# Now check if the selected item is a file, in which case # If the selected item is a file, the new item will be a
# the new file will be a sibling # sibling if the file has no children, otherwise a child
pItem = self.theProject.projTree[pHandle] pItem = self.theProject.tree[sHandle]
if pItem.itemType == nwItemType.FILE: qItem = self._getTreeItem(sHandle)
nHandle = pHandle if pItem.itemType == nwItemType.FILE and qItem.childCount() == 0:
pHandle = pItem.itemParent nHandle = sHandle
sHandle = pItem.itemParent
# If we again have no home, give up if sHandle is None:
if pHandle is None: logger.error("Internal error") # Bug
self.theParent.makeAlert(self.tr(
"Did not find anywhere to add the file or folder!"
), nwAlert.ERROR)
return False return False
if self.theProject.projTree.isTrashRoot(pHandle): if self.theProject.tree.isTrash(sHandle):
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
"Cannot add new files or folders to the Trash folder." "Cannot add new files or folders to the Trash folder."
), nwAlert.ERROR) ), nwAlert.ERROR)
return False return False
parTree = self.theProject.projTree.getItemPath(pHandle) # Add the file or folder
# If we're still here, add the file or folder
if itemType == nwItemType.FILE: if itemType == nwItemType.FILE:
tHandle = self.theProject.newFile(self.tr("New File"), itemClass, pHandle) if pItem.isNovelLike():
tHandle = self.theProject.newFile(self.tr("New Document"), sHandle)
else:
tHandle = self.theProject.newFile(self.tr("New Note"), sHandle)
elif itemType == nwItemType.FOLDER: elif itemType == nwItemType.FOLDER:
if len(parTree) >= nwConst.MAX_DEPTH - 1: tHandle = self.theProject.newFolder(self.tr("New Folder"), sHandle)
# Folders cannot be deeper than MAX_DEPTH - 1, leaving room
# for one more level of files.
self.theParent.makeAlert(self.tr(
"Cannot add new folder to this item. "
"Maximum folder depth has been reached."
), nwAlert.ERROR)
return False
tHandle = self.theProject.newFolder(self.tr("New Folder"), itemClass, pHandle)
else: else:
logger.error("Failed to add new item") logger.error("Failed to add new item")
return False return False
# If there is no handle set, return here # If there is no handle set, return here. This is a bug
if tHandle is None: if tHandle is None: # pragma: no cover
return True return True
# Add the new item to the tree # Add the new item to the tree
self.revealNewTreeItem(tHandle, nHandle) self.revealNewTreeItem(tHandle, nHandle)
self.theParent.editItem(tHandle) self.theParent.editItem(tHandle)
nwItem = self.theProject.projTree[tHandle]
# If this is a folder, return here # Handle new file creation
nwItem = self.theProject.tree[tHandle]
if nwItem.itemType != nwItemType.FILE: if nwItem.itemType != nwItemType.FILE:
return True return True
# This is a new files, so let's add some content # This is a new file, so let's add some content
newDoc = NWDoc(self.theProject, tHandle) newDoc = NWDoc(self.theProject, tHandle)
curTxt = newDoc.readDocument() if not newDoc.readDocument():
if curTxt is None:
curTxt = ""
if curTxt == "":
if nwItem.itemLayout == nwItemLayout.DOCUMENT: if nwItem.itemLayout == nwItemLayout.DOCUMENT:
newText = f"### {nwItem.itemName}\n\n" iLvl = self.theProject.index.getHandleHeaderIntLevel(sHandle)
hLvl = "#"*minmax(iLvl + 1, 2, 4)
newText = f"{hLvl} {nwItem.itemName}\n\n"
else: else:
newText = f"# {nwItem.itemName}\n\n" newText = f"# {nwItem.itemName}\n\n"
pIndex = self.theProject.index
# Save the text and index it # Save the text and index it
newDoc.writeDocument(newText) newDoc.writeDocument(newText)
self.theIndex.scanText(tHandle, newText) pIndex.scanText(tHandle, newText)
# Get Word Counts # Get Word Counts
cC, wC, pC = self.theIndex.getCounts(tHandle) cC, wC, pC = pIndex.getCounts(tHandle)
nwItem.setCharCount(cC) nwItem.setCharCount(cC)
nwItem.setWordCount(wC) nwItem.setWordCount(wC)
nwItem.setParaCount(pC) nwItem.setParaCount(pC)
@@ -309,7 +261,10 @@ class GuiProjectTree(QTreeWidget):
def revealNewTreeItem(self, tHandle, nHandle=None): def revealNewTreeItem(self, tHandle, nHandle=None):
"""Reveal a newly added project item in the project tree. """Reveal a newly added project item in the project tree.
""" """
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.tree[tHandle]
if nwItem is None:
return False
trItem = self._addTreeItem(nwItem, nHandle) trItem = self._addTreeItem(nwItem, nHandle)
if trItem is None: if trItem is None:
return False return False
@@ -318,7 +273,7 @@ class GuiProjectTree(QTreeWidget):
if pHandle is not None and pHandle in self._treeMap: if pHandle is not None and pHandle in self._treeMap:
self._treeMap[pHandle].setExpanded(True) self._treeMap[pHandle].setExpanded(True)
self._emitItemChange(tHandle) self._alertTreeChange(tHandle=tHandle, flush=True)
self.clearSelection() self.clearSelection()
trItem.setSelected(True) trItem.setSelected(True)
@@ -360,10 +315,31 @@ class GuiProjectTree(QTreeWidget):
pItem.insertChild(nIndex, cItem) pItem.insertChild(nIndex, cItem)
self._recordLastMove(cItem, pItem, tIndex) self._recordLastMove(cItem, pItem, tIndex)
self._alertTreeChange(tHandle=tHandle, flush=True)
self.clearSelection() self.clearSelection()
cItem.setSelected(True) cItem.setSelected(True)
self._setTreeChanged(True)
self._emitItemChange(tHandle) return True
def editTreeItem(self, tHandle=None):
"""Open the edit item dialog.
"""
if tHandle is None:
logger.warning("No item selected")
return False
tItem = self.theProject.tree[tHandle]
if tItem is None:
return False
if tItem.itemType == nwItemType.NO_TYPE:
return False
logger.verbose("Requesting change to item '%s'", tHandle)
dlgProj = GuiItemEditor(self, tHandle)
dlgProj.exec_()
if dlgProj.result() == QDialog.Accepted:
self.setTreeItemValues(tHandle)
self._alertTreeChange(tHandle=tHandle, flush=False)
return True return True
@@ -380,16 +356,6 @@ class GuiProjectTree(QTreeWidget):
self.theProject.setTreeOrder(theList) self.theProject.setTreeOrder(theList)
return True return True
def flushTreeOrder(self):
"""Calls saveTreeOrder if there are unsaved changes, otherwise
does nothing.
"""
if self._treeChanged:
logger.verbose("Flushing project tree to project class")
self.saveTreeOrder()
self._setTreeChanged(False)
return
def getTreeFromHandle(self, tHandle): def getTreeFromHandle(self, tHandle):
"""Recursively return all the children items starting from a """Recursively return all the children items starting from a
given item handle. given item handle.
@@ -400,6 +366,14 @@ class GuiProjectTree(QTreeWidget):
theList = self._scanChildren(theList, theItem, 0) theList = self._scanChildren(theList, theItem, 0)
return theList return theList
def toggleExpanded(self, tHandle):
"""Expand an item based on its handle.
"""
trItem = self._getTreeItem(tHandle)
if trItem is not None:
trItem.setExpanded(not trItem.isExpanded())
return
def getColumnSizes(self): def getColumnSizes(self):
"""Return the column widths for the tree columns. """Return the column widths for the tree columns.
""" """
@@ -419,7 +393,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("No project open") logger.error("No project open")
return False return False
trashHandle = self.theProject.projTree.trashRoot() trashHandle = self.theProject.tree.trashRoot()
logger.debug("Emptying Trash folder") logger.debug("Emptying Trash folder")
if trashHandle is None: if trashHandle is None:
@@ -447,13 +421,13 @@ class GuiProjectTree(QTreeWidget):
return False return False
logger.verbose("Deleting %d file(s) from Trash", nTrash) logger.verbose("Deleting %d file(s) from Trash", nTrash)
for tHandle in self.getTreeFromHandle(trashHandle): for tHandle in reversed(self.getTreeFromHandle(trashHandle)):
if tHandle == trashHandle: if tHandle == trashHandle:
continue continue
self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True) self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True)
if nTrash > 0: if nTrash > 0:
self._setTreeChanged(True) self._alertTreeChange(tHandle=trashHandle, flush=True)
return True return True
@@ -461,8 +435,8 @@ class GuiProjectTree(QTreeWidget):
"""Delete an item from the project tree. As a first step, files are """Delete an item from the project tree. As a first step, files are
moved to the Trash folder. Permanent deletion is a second step. This moved to the Trash folder. Permanent deletion is a second step. This
second step also deletes the item from the project object as well as second step also deletes the item from the project object as well as
delete the files on disk. Folders are deleted if they're empty only, delete the files on disk. Root folders are deleted if they're empty
and the deletion is always permanent. only, and the deletion is always permanent.
""" """
if not self.theParent.hasProject: if not self.theParent.hasProject:
logger.error("No project open") logger.error("No project open")
@@ -480,109 +454,22 @@ class GuiProjectTree(QTreeWidget):
return False return False
trItemS = self._getTreeItem(tHandle) trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.projTree[tHandle] nwItemS = self.theProject.tree[tHandle]
if trItemS is None or nwItemS is None: if trItemS is None or nwItemS is None:
logger.error("Could not find tree item for deletion") logger.error("Could not find tree item for deletion")
return False return False
wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole)) wCount = self._getItemWordCount(tHandle)
if nwItemS.itemType == nwItemType.FILE: autoFlush = not bulkAction
logger.debug("User requested file '%s' deleted", tHandle) if nwItemS.itemType == nwItemType.ROOT:
trItemP = trItemS.parent() # Only an empty ROOT folder can be deleted
trItemT = self._addTrashRoot() logger.debug("User requested a root folder '%s' deleted", tHandle)
if trItemP is None or trItemT is None:
logger.error("Could not delete item")
return False
pHandle = nwItemS.itemParent
if self.theProject.projTree.isTrashRoot(pHandle):
# If the file is in the trash folder already, as the
# user if they want to permanently delete the file.
doPermanent = False
if not alreadyAsked:
msgYes = self.theParent.askQuestion(
self.tr("Delete File"),
self.tr("Permanently delete file '{0}'?").format(nwItemS.itemName)
)
if msgYes:
doPermanent = True
else:
doPermanent = True
if doPermanent:
logger.debug("Permanently deleting file with handle '%s'", tHandle)
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
if self.theParent.docEditor.docHandle() == tHandle:
self.theParent.closeDocument()
delDoc = NWDoc(self.theProject, tHandle)
if not delDoc.deleteDocument():
self.theParent.makeAlert([
self.tr("Could not delete document file."), delDoc.getError()
], nwAlert.ERROR)
return False
self.theIndex.deleteHandle(tHandle)
self._deleteTreeItem(tHandle)
self._setTreeChanged(True)
self.wordCountsChanged.emit()
else:
# The file is not already in the trash folder, so we
# move it there.
msgYes = self.theParent.askQuestion(
self.tr("Delete File"),
self.tr("Move file '{0}' to Trash?").format(nwItemS.itemName),
)
if msgYes:
if pHandle is None:
logger.warning("File has no parent item")
logger.debug("Moving file '%s' to trash", tHandle)
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC)
self._updateItemParent(tHandle)
self.propagateCount(tHandle, wCount)
self.theIndex.deleteHandle(tHandle)
self._recordLastMove(trItemS, trItemP, tIndex)
self._setTreeChanged(True)
elif nwItemS.itemType == nwItemType.FOLDER:
logger.debug("User requested folder '%s' deleted", tHandle)
trItemP = trItemS.parent()
if trItemP is None:
logger.error("Could not delete folder")
return False
tIndex = trItemP.indexOfChild(trItemS)
if trItemS.childCount() == 0:
trItemP.takeChild(tIndex)
self._deleteTreeItem(tHandle)
self._setTreeChanged(True)
else:
self.theParent.makeAlert(self.tr(
"Cannot delete folder. It is not empty. "
"Recursive deletion is not supported. "
"Please delete the content first."
), nwAlert.ERROR)
return False
elif nwItemS.itemType == nwItemType.ROOT:
logger.debug("User requested root folder '%s' deleted", tHandle)
tIndex = self.indexOfTopLevelItem(trItemS) tIndex = self.indexOfTopLevelItem(trItemS)
if trItemS.childCount() == 0: if trItemS.childCount() == 0:
self.takeTopLevelItem(tIndex) self.takeTopLevelItem(tIndex)
self._deleteTreeItem(tHandle) self._deleteTreeItem(tHandle)
self.theParent.mainMenu.setAvailableRoot() self._alertTreeChange(tHandle=tHandle, flush=True)
self._setTreeChanged(True)
else: else:
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
"Cannot delete root folder. It is not empty. " "Cannot delete root folder. It is not empty. "
@@ -591,6 +478,70 @@ class GuiProjectTree(QTreeWidget):
), nwAlert.ERROR) ), nwAlert.ERROR)
return False return False
elif nwItemS.itemType == nwItemType.FOLDER and trItemS.childCount() == 0:
# An empty FOLDER is just deleted without any further checks
logger.debug("User requested an empty folder '%s' deleted", tHandle)
trItemP = trItemS.parent()
tIndex = trItemP.indexOfChild(trItemS)
trItemP.takeChild(tIndex)
self._deleteTreeItem(tHandle)
self._alertTreeChange(tHandle=tHandle, flush=autoFlush)
else:
# A populated FOLDER or a FILE requires confirmtation
logger.debug("User requested a file or folder '%s' deleted", tHandle)
trItemP = trItemS.parent()
trItemT = self._addTrashRoot()
if trItemP is None or trItemT is None:
logger.error("Could not delete item")
return False
if self.theProject.tree.isTrash(tHandle):
# If the file is in the trash folder already, as the
# user if they want to permanently delete the file.
doPermanent = False
if not alreadyAsked:
msgYes = self.theParent.askQuestion(
self.tr("Delete"),
self.tr("Permanently delete '{0}'?").format(nwItemS.itemName)
)
if msgYes:
doPermanent = True
else:
doPermanent = True
if doPermanent:
logger.debug("Permanently deleting item with handle '%s'", tHandle)
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
for dHandle in reversed(self.getTreeFromHandle(tHandle)):
if self.theParent.docEditor.docHandle() == dHandle:
self.theParent.closeDocument()
self._deleteTreeItem(dHandle)
self._alertTreeChange(tHandle=tHandle, flush=autoFlush)
self.wordCountsChanged.emit()
else:
# The item is not already in the trash folder, so we
# move it there.
msgYes = self.theParent.askQuestion(
self.tr("Delete"),
self.tr("Move '{0}' to Trash?").format(nwItemS.itemName),
)
if msgYes:
logger.debug("Moving item '%s' to trash", tHandle)
self.propagateCount(tHandle, 0)
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC)
self._postItemMove(tHandle, wCount)
self._recordLastMove(trItemS, trItemP, tIndex)
self._alertTreeChange(tHandle=tHandle, flush=autoFlush)
return True return True
def setTreeItemValues(self, tHandle): def setTreeItemValues(self, tHandle):
@@ -599,7 +550,7 @@ class GuiProjectTree(QTreeWidget):
already coming from the project tree. already coming from the project tree.
""" """
trItem = self._getTreeItem(tHandle) trItem = self._getTreeItem(tHandle)
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.tree[tHandle]
if trItem is None or nwItem is None: if trItem is None or nwItem is None:
return return
@@ -610,8 +561,8 @@ class GuiProjectTree(QTreeWidget):
else: else:
expIcon = self.theTheme.getIcon("cross") expIcon = self.theTheme.getIcon("cross")
itempStatus, statusIcon = nwItem.getImportStatus() itemStatus, statusIcon = nwItem.getImportStatus()
hLevel = self.theIndex.getHandleHeaderLevel(tHandle) hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
itemIcon = self.theTheme.getItemIcon( itemIcon = self.theTheme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
) )
@@ -620,7 +571,7 @@ class GuiProjectTree(QTreeWidget):
trItem.setText(self.C_NAME, nwItem.itemName) trItem.setText(self.C_NAME, nwItem.itemName)
trItem.setIcon(self.C_EXPORT, expIcon) trItem.setIcon(self.C_EXPORT, expIcon)
trItem.setIcon(self.C_STATUS, statusIcon) trItem.setIcon(self.C_STATUS, statusIcon)
trItem.setToolTip(self.C_STATUS, itempStatus) trItem.setToolTip(self.C_STATUS, itemStatus)
if self.mainConf.emphLabels and nwItem.itemLayout == nwItemLayout.DOCUMENT: if self.mainConf.emphLabels and nwItem.itemLayout == nwItemLayout.DOCUMENT:
trFont = trItem.font(self.C_NAME) trFont = trItem.font(self.C_NAME)
@@ -634,7 +585,7 @@ class GuiProjectTree(QTreeWidget):
return return
def propagateCount(self, tHandle, theCount, nDepth=0): def propagateCount(self, tHandle, newCount, countChildren=False):
"""Recursive function setting the word count for a given item, """Recursive function setting the word count for a given item,
and propagating that count upwards in the tree until reaching a and propagating that count upwards in the tree until reaching a
root item. This function is more efficient than recalculating root item. This function is more efficient than recalculating
@@ -646,20 +597,30 @@ class GuiProjectTree(QTreeWidget):
if tItem is None: if tItem is None:
return return
tItem.setText(self.C_COUNT, f"{theCount:n}") if countChildren:
tItem.setData(self.C_COUNT, Qt.UserRole, int(theCount)) for i in range(tItem.childCount()):
newCount += int(tItem.child(i).data(self.C_COUNT, Qt.UserRole))
tItem.setText(self.C_COUNT, f"{newCount:n}")
tItem.setData(self.C_COUNT, Qt.UserRole, int(newCount))
pItem = tItem.parent() pItem = tItem.parent()
if pItem is None: if pItem is None:
return return
pCount = 0 pCount = 0
pHandle = None
for i in range(pItem.childCount()): for i in range(pItem.childCount()):
pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole)) pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
pHandle = pItem.data(self.C_NAME, Qt.UserRole) pHandle = pItem.data(self.C_NAME, Qt.UserRole)
if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "": if pHandle:
self.propagateCount(pHandle, pCount, nDepth+1) if self.theProject.tree.checkType(pHandle, nwItemType.FILE):
# A file has an internal word count we need to account
# for, but a folder always has 0 words on its own.
pCount += self.theProject.index.getCounts(pHandle)[1]
self.propagateCount(pHandle, pCount, countChildren=False)
return return
@@ -703,20 +664,19 @@ class GuiProjectTree(QTreeWidget):
return False return False
dstIndex = min(max(0, dstIndex), dstItem.childCount()) dstIndex = min(max(0, dstIndex), dstItem.childCount())
wCount = int(srcItem.data(self.C_COUNT, Qt.UserRole))
sHandle = srcItem.data(self.C_NAME, Qt.UserRole) sHandle = srcItem.data(self.C_NAME, Qt.UserRole)
dHandle = dstItem.data(self.C_NAME, Qt.UserRole) dHandle = dstItem.data(self.C_NAME, Qt.UserRole)
logger.debug("Moving item '%s' back to '%s', index %d", sHandle, dHandle, dstIndex) logger.debug("Moving item '%s' back to '%s', index %d", sHandle, dHandle, dstIndex)
wCount = self._getItemWordCount(sHandle)
self.propagateCount(sHandle, 0) self.propagateCount(sHandle, 0)
parItem = srcItem.parent() parItem = srcItem.parent()
srcIndex = parItem.indexOfChild(srcItem) srcIndex = parItem.indexOfChild(srcItem)
movItem = parItem.takeChild(srcIndex) movItem = parItem.takeChild(srcIndex)
dstItem.insertChild(dstIndex, movItem) dstItem.insertChild(dstIndex, movItem)
snItem = self.theProject.projTree[sHandle] self._postItemMove(sHandle, wCount)
dnItem = self.theProject.projTree[dHandle] self._alertTreeChange(tHandle=sHandle, flush=True)
self._postItemMove(sHandle, snItem, dnItem, wCount)
self.clearSelection() self.clearSelection()
movItem.setSelected(True) movItem.setSelected(True)
@@ -771,7 +731,7 @@ class GuiProjectTree(QTreeWidget):
if isinstance(selItem, QTreeWidgetItem): if isinstance(selItem, QTreeWidgetItem):
tHandle = selItem.data(self.C_NAME, Qt.UserRole) tHandle = selItem.data(self.C_NAME, Qt.UserRole)
self.setSelectedHandle(tHandle) # Just to be safe self.setSelectedHandle(tHandle) # Just to be safe
tItem = self.theProject.projTree[tHandle] tItem = self.theProject.tree[tHandle]
if tItem is not None: if tItem is not None:
if self.ctxMenu.filterActions(tItem): if self.ctxMenu.filterActions(tItem):
# Only open menu if any actions remain after filter # Only open menu if any actions remain after filter
@@ -783,7 +743,7 @@ class GuiProjectTree(QTreeWidget):
def doUpdateCounts(self, tHandle, cCount, wCount, pCount): def doUpdateCounts(self, tHandle, cCount, wCount, pCount):
"""Slot for updating the word count of a specific item. """Slot for updating the word count of a specific item.
""" """
self.propagateCount(tHandle, wCount) self.propagateCount(tHandle, wCount, countChildren=True)
self.wordCountsChanged.emit() self.wordCountsChanged.emit()
return return
@@ -809,7 +769,7 @@ class GuiProjectTree(QTreeWidget):
return return
tHandle = selItem.data(self.C_NAME, Qt.UserRole) tHandle = selItem.data(self.C_NAME, Qt.UserRole)
tItem = self.theProject.projTree[tHandle] tItem = self.theProject.tree[tHandle]
if tItem is None: if tItem is None:
return return
@@ -819,61 +779,34 @@ class GuiProjectTree(QTreeWidget):
return return
def dropEvent(self, theEvent): def dropEvent(self, theEvent):
"""Overload the drop of dragged item event to check whether the """Overload the drop item event to ensure relevant data has been
drop is allowed or not. Disallowed drops are cancelled. updated.
""" """
sHandle = self.getSelectedHandle() sHandle = self.getSelectedHandle()
if sHandle is None: if sHandle is None:
logger.error("No handle selected") logger.error("Invalid drag and drop event")
return return
dIndex = self.indexAt(theEvent.pos()) logger.debug("Drag'n'drop of item '%s' accepted", sHandle)
if not dIndex.isValid():
logger.error("Invalid drop index")
return
sItem = self._getTreeItem(sHandle) sItem = self._getTreeItem(sHandle)
dItem = self.itemFromIndex(dIndex) isExpanded = False
dHandle = dItem.data(self.C_NAME, Qt.UserRole) if sItem is not None:
snItem = self.theProject.projTree[sHandle] isExpanded = sItem.isExpanded()
dnItem = self.theProject.projTree[dHandle]
if dnItem is None:
self.theParent.makeAlert(self.tr(
"The item cannot be moved to that location."
), nwAlert.ERROR)
return
pItem = sItem.parent() pItem = sItem.parent()
pIndex = 0 pIndex = 0
if pItem is not None: if pItem is not None:
pIndex = pItem.indexOfChild(sItem) pIndex = pItem.indexOfChild(sItem)
wCount = int(sItem.data(self.C_COUNT, Qt.UserRole)) wCount = self._getItemWordCount(sHandle)
isFile = snItem.itemType == nwItemType.FILE
isRoot = snItem.itemType == nwItemType.ROOT
onFile = dnItem.itemType == nwItemType.FILE
isSame = snItem.itemClass == dnItem.itemClass
isNone = snItem.itemClass == nwItemClass.NO_CLASS
isNote = snItem.itemLayout == nwItemLayout.NOTE
onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile
allowDrop = isSame or isNone or isNote or onFree
allowDrop &= not (self.dropIndicatorPosition() == QAbstractItemView.OnItem and onFile)
if allowDrop and not isRoot:
logger.debug("Drag'n'drop of item '%s' accepted", sHandle)
self.propagateCount(sHandle, 0) self.propagateCount(sHandle, 0)
QTreeWidget.dropEvent(self, theEvent)
self._postItemMove(sHandle, snItem, dnItem, wCount)
self._recordLastMove(sItem, pItem, pIndex)
else: QTreeWidget.dropEvent(self, theEvent)
theEvent.ignore() self._postItemMove(sHandle, wCount)
logger.debug("Drag'n'drop of item '%s' not accepted", sHandle) self._recordLastMove(sItem, pItem, pIndex)
self.theParent.makeAlert(self.tr( self._alertTreeChange(tHandle=sHandle, flush=True)
"The item cannot be moved to that location." sItem.setExpanded(isExpanded)
), nwAlert.ERROR)
return return
@@ -881,64 +814,88 @@ class GuiProjectTree(QTreeWidget):
# Internal Functions # Internal Functions
## ##
def _postItemMove(self, sHandle, snItem, dnItem, wCount): def _postItemMove(self, tHandle, wCount):
"""Run various maintenance tasks for a moved item. """Run various maintenance tasks for a moved item.
""" """
isFile = snItem.itemType == nwItemType.FILE trItemS = self._getTreeItem(tHandle)
isSame = snItem.itemClass == dnItem.itemClass nwItemS = self.theProject.tree[tHandle]
onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile trItemP = trItemS.parent()
if trItemP is None:
logger.error("Failed to find new parent item of '%s'", tHandle)
return False
self._updateItemParent(sHandle) # Update item parent handle in the project, make sure meta data
# is updated accordingly, and update word count
pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
nwItemS.setParent(pHandle)
trItemP.setExpanded(True)
logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle)
# If the item does not have the same class as the target, mHandles = self.getTreeFromHandle(tHandle)
# and the target is not a free root folder, update its class logger.debug("A total of %d item(s) were moved", len(mHandles))
if not (isSame or onFree): for mHandle in mHandles:
logger.debug( logger.debug("Updating item '%s'", mHandle)
"Item '%s' class has been changed from '%s' to '%s'", self.theProject.tree.updateItemData(mHandle)
sHandle, snItem.itemClass.name, dnItem.itemClass.name
)
snItem.setClass(dnItem.itemClass)
self.setTreeItemValues(sHandle)
self.propagateCount(sHandle, wCount) # Update the index
if nwItemS.isInactive():
# The items dropped into archive or trash should be removed self.theProject.index.deleteHandle(mHandle)
# from the project index, for all other items, we rescan the
# file to ensure the index is up to date.
if onFree:
self.theIndex.deleteHandle(sHandle)
else: else:
self.theIndex.reIndexHandle(sHandle) self.theProject.index.reIndexHandle(mHandle)
self.setTreeItemValues(mHandle)
# Trigger dependent updates # Trigger dependent updates
self._setTreeChanged(True) self.propagateCount(tHandle, wCount)
self._emitItemChange(sHandle)
return return True
def _getItemWordCount(self, tHandle):
"""Retrun the word count of a given item handle.
"""
tItem = self._getTreeItem(tHandle)
if tItem is None:
return 0
return int(tItem.data(self.C_COUNT, Qt.UserRole))
def _getTreeItem(self, tHandle): def _getTreeItem(self, tHandle):
"""Returns the QTreeWidgetItem of a given item handle. """Return the QTreeWidgetItem of a given item handle.
""" """
return self._treeMap.get(tHandle, None) return self._treeMap.get(tHandle, None)
def _deleteTreeItem(self, tHandle): def _deleteTreeItem(self, tHandle):
"""Delete a tree item from the project and the map. """Permanently delete a tree item from the project and the map.
""" """
del self.theProject.projTree[tHandle] if self.theProject.tree.checkType(tHandle, nwItemType.FILE):
delDoc = NWDoc(self.theProject, tHandle)
if not delDoc.deleteDocument():
self.theParent.makeAlert([
self.tr("Could not delete document file."), delDoc.getError()
], nwAlert.ERROR)
return False
self.theProject.index.deleteHandle(tHandle)
del self.theProject.tree[tHandle]
self._treeMap.pop(tHandle, None) self._treeMap.pop(tHandle, None)
return
return True
def _scanChildren(self, theList, tItem, tIndex): def _scanChildren(self, theList, tItem, tIndex):
"""This is a recursive function returning all items in a tree """This is a recursive function returning all items in a tree
starting at a given QTreeWidgetItem. starting at a given QTreeWidgetItem.
""" """
tHandle = tItem.data(self.C_NAME, Qt.UserRole) tHandle = tItem.data(self.C_NAME, Qt.UserRole)
nwItem = self.theProject.projTree[tHandle] cCount = tItem.childCount()
nwItem.setExpanded(tItem.isExpanded())
# Update tree-related meta data
nwItem = self.theProject.tree[tHandle]
nwItem.setExpanded(tItem.isExpanded() and cCount > 0)
nwItem.setOrder(tIndex) nwItem.setOrder(tIndex)
theList.append(tHandle) theList.append(tHandle)
for i in range(tItem.childCount()): for i in range(cCount):
self._scanChildren(theList, tItem.child(i), i) self._scanChildren(theList, tItem.child(i), i)
return theList return theList
def _addTreeItem(self, nwItem, nHandle=None): def _addTreeItem(self, nwItem, nHandle=None):
@@ -965,9 +922,7 @@ class GuiProjectTree(QTreeWidget):
self._treeMap[tHandle] = newItem self._treeMap[tHandle] = newItem
if pHandle is None: if pHandle is None:
if nwItem.itemType == nwItemType.ROOT: if nwItem.itemType == nwItemType.ROOT:
self.addTopLevelItem(newItem) newItem.setFlags(newItem.flags() ^ Qt.ItemIsDragEnabled)
self.theParent.mainMenu.setAvailableRoot()
elif nwItem.itemType == nwItemType.TRASH:
self.addTopLevelItem(newItem) self.addTopLevelItem(newItem)
else: else:
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
@@ -984,16 +939,14 @@ class GuiProjectTree(QTreeWidget):
except Exception: except Exception:
logger.error("Failed to get index of item with handle '%s'", nHandle) logger.error("Failed to get index of item with handle '%s'", nHandle)
if byIndex >= 0: if byIndex >= 0:
self._treeMap[pHandle].insertChild(byIndex+1, newItem) self._treeMap[pHandle].insertChild(byIndex + 1, newItem)
else: else:
self._treeMap[pHandle].addChild(newItem) self._treeMap[pHandle].addChild(newItem)
self.propagateCount(tHandle, nwItem.wordCount) self.propagateCount(tHandle, nwItem.wordCount, countChildren=True)
self.setTreeItemValues(tHandle) self.setTreeItemValues(tHandle)
newItem.setExpanded(nwItem.isExpanded) newItem.setExpanded(nwItem.isExpanded)
self._setTreeChanged(True)
return newItem return newItem
def _addTrashRoot(self): def _addTrashRoot(self):
@@ -1006,52 +959,34 @@ class GuiProjectTree(QTreeWidget):
trItem = self._getTreeItem(trashHandle) trItem = self._getTreeItem(trashHandle)
if trItem is None: if trItem is None:
trItem = self._addTreeItem( trItem = self._addTreeItem(self.theProject.tree[trashHandle])
self.theProject.projTree[trashHandle]
)
if trItem is not None: if trItem is not None:
trItem.setExpanded(True) trItem.setExpanded(True)
self._setTreeChanged(True) self._alertTreeChange(tHandle=trashHandle, flush=True)
return trItem return trItem
def _updateItemParent(self, tHandle): def _alertTreeChange(self, tHandle=None, flush=True):
"""Update the parent handle of an item so that the information """Update information on tree change state, and emit necessary
in the project is consistent with the treeView. signals.
""" """
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.projTree[tHandle]
trItemP = trItemS.parent()
if trItemP is None:
logger.error("Failed to find new parent item of '%s'", tHandle)
return False
pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
nwItemS.setParent(pHandle)
self.setTreeItemValues(tHandle)
logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle)
return True
def _setTreeChanged(self, theState):
"""Set the tree change flag, and propagate to the project.
"""
self._treeChanged = theState
if theState:
self._timeChanged = time() self._timeChanged = time()
self.theProject.setProjectChanged(True) self.theProject.setProjectChanged(True)
if flush:
self.saveTreeOrder()
tItem = self.theProject.tree[tHandle]
if tItem is None:
return return
def _emitItemChange(self, tHandle): itemType = tItem.itemType
"""Emit an item change signal for a given handle. if itemType == nwItemType.ROOT:
""" self.rootFolderChanged.emit(tHandle)
if self.theProject.projTree.checkType(tHandle, nwItemType.FILE): elif itemType == nwItemType.FILE and tItem.isNovelLike():
nwItem = self.theProject.projTree[tHandle] self.novelItemChanged.emit(tHandle)
if nwItem.itemClass == nwItemClass.NOVEL:
self.novelItemChanged.emit() self.treeItemChanged.emit(tHandle)
else:
self.noteItemChanged.emit()
return return
def _recordLastMove(self, srcItem, parItem, parIndex): def _recordLastMove(self, srcItem, parItem, parIndex):
@@ -1130,9 +1065,9 @@ class GuiProjectTreeMenu(QMenu):
logger.error("Failed to extract information to build tree context menu") logger.error("Failed to extract information to build tree context menu")
return False return False
trashHandle = self.theTree.theProject.projTree.trashRoot() trashHandle = self.theTree.theProject.tree.trashRoot()
inTrash = theItem.itemParent == trashHandle and trashHandle is not None inTrash = self.theTree.theProject.tree.isTrash(theItem.itemHandle)
isTrash = theItem.itemHandle == trashHandle and trashHandle is not None isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
isFile = theItem.itemType == nwItemType.FILE isFile = theItem.itemType == nwItemType.FILE
@@ -1182,7 +1117,7 @@ class GuiProjectTreeMenu(QMenu):
"""Forward the new file call to the project tree. """Forward the new file call to the project tree.
""" """
if self.theItem is not None: if self.theItem is not None:
self.theTree.newTreeItem(nwItemType.FILE, None) self.theTree.newTreeItem(nwItemType.FILE)
return return
@pyqtSlot() @pyqtSlot()
@@ -1190,7 +1125,7 @@ class GuiProjectTreeMenu(QMenu):
"""Forward the new folder call to the project tree. """Forward the new folder call to the project tree.
""" """
if self.theItem is not None: if self.theItem is not None:
self.theTree.newTreeItem(nwItemType.FOLDER, None) self.theTree.newTreeItem(nwItemType.FOLDER)
return return
@pyqtSlot() @pyqtSlot()
+10 -15
View File
@@ -456,20 +456,18 @@ class GuiIcons:
ICON_KEYS = { ICON_KEYS = {
# Project and GUI icons # Project and GUI icons
"novelwriter", "proj_nwx", "novelwriter", "cls_archive", "cls_character", "cls_custom", "cls_entity", "cls_none",
"cls_none", "cls_novel", "cls_plot", "cls_character", "cls_world", "cls_novel", "cls_object", "cls_plot", "cls_timeline", "cls_trash", "cls_world", "doc_h0",
"cls_timeline", "cls_object", "cls_entity", "cls_custom", "cls_archive", "cls_trash", "doc_h1", "doc_h2", "doc_h3", "doc_h4", "proj_chapter", "proj_details", "proj_document",
"proj_document", "proj_title", "proj_chapter", "proj_scene", "proj_note", "proj_folder", "proj_folder", "proj_note", "proj_nwx", "proj_scene", "proj_stats", "proj_title",
"status_lang", "status_time", "status_idle", "status_stats", "status_lines", "search_cancel", "search_case", "search_loop", "search_preserve", "search_project",
"doc_h0", "doc_h1", "doc_h2", "doc_h3", "doc_h4", "search_regex", "search_word", "status_idle", "status_lang", "status_lines",
"search_case", "search_regex", "search_word", "search_loop", "search_project", "status_stats", "status_time", "view_build", "view_editor", "view_novel", "view_outline",
"search_cancel", "search_preserve",
# General Button Icons # General Button Icons
"delete", "close", "done", "clear", "save", "add", "remove", "add", "backward", "check", "clear", "close", "cross", "delete", "done", "down", "edit",
"search", "search_replace", "edit", "check", "cross", "hash", "forward", "hash", "maximise", "menu", "minimise", "reference", "refresh", "remove",
"maximise", "minimise", "refresh", "reference", "backward", "save", "search_replace", "search", "settings", "up",
"forward", "settings",
# Switches # Switches
"sticky-on", "sticky-off", "sticky-on", "sticky-off",
@@ -639,9 +637,6 @@ class GuiIcons:
iconName = "proj_scene" iconName = "proj_scene"
elif tLayout == nwItemLayout.NOTE: elif tLayout == nwItemLayout.NOTE:
iconName = "proj_note" iconName = "proj_note"
elif tType == nwItemType.TRASH:
iconName = nwLabels.CLASS_ICON[tClass]
if iconName is None: if iconName is None:
return QIcon() return QIcon()
+139
View File
@@ -0,0 +1,139 @@
"""
novelWriter GUI Main Window Views ToolBar
===========================================
GUI class for the main window "Views" toolbar
File History:
Created: 2022-05-10 [1.7b1]
This file is a part of novelWriter
Copyright 20182022, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import novelwriter
from PyQt5.QtCore import Qt, QSize, pyqtSignal
from PyQt5.QtWidgets import (
QToolBar, QWidget, QSizePolicy, QAction, QMenu, QToolButton
)
from novelwriter.enum import nwView
logger = logging.getLogger(__name__)
class GuiViewsBar(QToolBar):
viewChangeRequested = pyqtSignal(nwView)
def __init__(self, theParent):
QToolBar.__init__(self, theParent)
logger.debug("Initialising GuiViewsBar ...")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
# Style
iPx = self.mainConf.pxInt(22)
mPx = self.mainConf.pxInt(60)
lblFont = self.theTheme.guiFont
lblFont.setPointSizeF(0.65*self.theTheme.fontPointSize)
self.setMovable(False)
self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
self.setIconSize(QSize(iPx, iPx))
self.setMaximumWidth(mPx)
self.setContentsMargins(0, 0, 0, 0)
self.setStyleSheet("QToolBar {border: 0px;}")
stretch = QWidget(self)
stretch.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Actions
self.aProject = QAction(self.tr("Project"))
self.aProject.setFont(lblFont)
self.aProject.setToolTip(self.tr("Show project tree and editor"))
self.aProject.setIcon(self.theTheme.getIcon("view_editor"))
self.aProject.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT))
self.aNovel = QAction(self.tr("Novel"))
self.aNovel.setFont(lblFont)
self.aNovel.setToolTip(self.tr("Show novel tree and editor"))
self.aNovel.setIcon(self.theTheme.getIcon("view_novel"))
self.aNovel.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL))
self.aOutline = QAction(self.tr("Outline"))
self.aOutline.setFont(lblFont)
self.aOutline.setToolTip(self.tr("Show novel outline"))
self.aOutline.setIcon(self.theTheme.getIcon("view_outline"))
self.aOutline.triggered.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE))
self.aBuild = QAction(self.tr("Build"))
self.aBuild.setFont(lblFont)
self.aBuild.setToolTip(self.tr("Build novel project"))
self.aBuild.setIcon(self.theTheme.getIcon("view_build"))
self.aBuild.triggered.connect(lambda: self.theParent.showBuildProjectDialog())
self.aDetails = QAction(self.tr("Details"))
self.aDetails.setFont(lblFont)
self.aDetails.setToolTip(self.tr("Show project details"))
self.aDetails.setIcon(self.theTheme.getIcon("proj_details"))
self.aDetails.triggered.connect(lambda: self.theParent.showProjectDetailsDialog())
self.aStats = QAction(self.tr("Stats"))
self.aStats.setFont(lblFont)
self.aStats.setToolTip(self.tr("Show project statistics"))
self.aStats.setIcon(self.theTheme.getIcon("proj_stats"))
self.aStats.triggered.connect(lambda: self.theParent.showWritingStatsDialog())
# Settings Menu
self.mSettings = QMenu()
self.aPrjSettings = QAction(self.tr("Project Settings"))
self.aPrjSettings.triggered.connect(lambda: self.theParent.showProjectSettingsDialog())
self.mSettings.addAction(self.aPrjSettings)
self.aPreferences = QAction(self.tr("Preferences"))
self.aPreferences.triggered.connect(lambda: self.theParent.showPreferencesDialog())
self.mSettings.addAction(self.aPreferences)
self.tbSettings = QToolButton(self)
self.tbSettings.setFont(lblFont)
self.tbSettings.setText(self.tr("Settings"))
self.tbSettings.setIcon(self.theTheme.getIcon("settings"))
self.tbSettings.setMenu(self.mSettings)
self.tbSettings.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
self.tbSettings.setPopupMode(QToolButton.InstantPopup)
# Assemble
self.addAction(self.aProject)
self.addAction(self.aNovel)
self.addAction(self.aOutline)
self.addAction(self.aBuild)
self.addWidget(stretch)
self.addAction(self.aDetails)
self.addAction(self.aStats)
self.addWidget(self.tbSettings)
logger.debug("GuiViewsBar initialisation complete")
return
# END Class GuiViewsBar
+151 -169
View File
@@ -27,35 +27,34 @@ import os
import logging import logging
import novelwriter import novelwriter
from enum import Enum
from time import time from time import time
from datetime import datetime from datetime import datetime
from PyQt5.QtCore import Qt, QTimer, QSize, QThreadPool, pyqtSlot from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot
from PyQt5.QtGui import QIcon, QKeySequence, QCursor from PyQt5.QtGui import QIcon, QKeySequence, QCursor
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut, qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut,
QMessageBox, QDialog, QTabWidget, QToolBar, QAction QMessageBox, QDialog, QStackedWidget
) )
from novelwriter.gui import ( from novelwriter.gui import (
GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu, GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu,
GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails, GuiProjectTree, GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectTree, GuiTheme,
GuiTheme GuiViewsBar
) )
from novelwriter.dialogs import ( from novelwriter.dialogs import (
GuiAbout, GuiDocMerge, GuiDocSplit, GuiItemEditor, GuiPreferences, GuiAbout, GuiDocMerge, GuiDocSplit, GuiPreferences, GuiProjectDetails,
GuiProjectDetails, GuiProjectLoad, GuiProjectSettings, GuiUpdates, GuiProjectLoad, GuiProjectSettings, GuiUpdates, GuiWordList
GuiWordList
) )
from novelwriter.tools import ( from novelwriter.tools import (
GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats
) )
from novelwriter.core import NWProject, NWIndex from novelwriter.core import NWProject
from novelwriter.enum import ( from novelwriter.enum import (
nwItemType, nwItemClass, nwAlert, nwWidget, nwState nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView
) )
from novelwriter.common import getGuiItem, hexToInt from novelwriter.common import getGuiItem, hexToInt
from novelwriter.constants import nwLists
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -87,7 +86,6 @@ class GuiMain(QMainWindow):
# Core Classes and Settings # Core Classes and Settings
self.theTheme = GuiTheme() self.theTheme = GuiTheme()
self.theProject = NWProject(self) self.theProject = NWProject(self)
self.theIndex = NWIndex(self.theProject)
self.hasProject = False self.hasProject = False
self.isFocusMode = False self.isFocusMode = False
self.idleRefTime = time() self.idleRefTime = time()
@@ -103,8 +101,7 @@ class GuiMain(QMainWindow):
# Sizes # Sizes
mPx = self.mainConf.pxInt(4) mPx = self.mainConf.pxInt(4)
fPx = self.theTheme.fontPixelSize hWd = self.mainConf.pxInt(4)
fPt = self.theTheme.fontPointSize
# Main GUI Elements # Main GUI Elements
self.statusBar = GuiMainStatus(self) self.statusBar = GuiMainStatus(self)
@@ -115,62 +112,43 @@ class GuiMain(QMainWindow):
self.docViewer = GuiDocViewer(self) self.docViewer = GuiDocViewer(self)
self.treeMeta = GuiItemDetails(self) self.treeMeta = GuiItemDetails(self)
self.projView = GuiOutline(self) self.projView = GuiOutline(self)
self.projMeta = GuiOutlineDetails(self)
self.mainMenu = GuiMainMenu(self) self.mainMenu = GuiMainMenu(self)
self.viewsBar = GuiViewsBar(self)
# Connect Signals Between Main Elements # Connect Signals Between Main Elements
self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage) self.viewsBar.viewChangeRequested.connect(self._changeView)
self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus)
self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts)
self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts)
self.treeView.itemSelectionChanged.connect(self._treeSingleClick) self.treeView.itemSelectionChanged.connect(self._treeSingleClick)
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.treeView.novelItemChanged.connect(self._treeNovelItemChanged) self.treeView.novelItemChanged.connect(self._treeNovelItemChanged)
self.treeView.wordCountsChanged.connect(self._updateStatusWordCount) self.treeView.wordCountsChanged.connect(self._updateStatusWordCount)
self.treeView.treeItemChanged.connect(self.docEditor.updateDocInfo)
self.treeView.treeItemChanged.connect(self.docViewer.updateDocInfo)
self.treeView.treeItemChanged.connect(self.treeMeta.updateViewBox)
self.treeView.rootFolderChanged.connect(self.projView.updateRootItem)
# Project Tree Tabs self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage)
self.projTabs = QTabWidget() self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus)
self.projTabs.setTabPosition(QTabWidget.South) self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts)
self.projTabs.setStyleSheet("QTabWidget::pane {border: 0;};") self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts)
self.projTabs.addTab(self.treeView, self.tr("Project")) self.docEditor.loadDocumentTagRequest.connect(self._followTag)
self.projTabs.addTab(self.novelView, self.tr("Novel"))
self.projTabs.currentChanged.connect(self._projTabsChanged)
tabFont = self.projTabs.tabBar().font() self.docViewer.loadDocumentTagRequest.connect(self._followTag)
tabFont.setPointSizeF(0.9*fPt)
self.projTabs.tabBar().setFont(tabFont)
# Project Tree Action Buttons self.projView.loadDocumentTagRequest.connect(self._followTag)
btnSize = int(round(0.7*fPx))
self.treeButtons = QToolBar()
self.treeButtons.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.treeButtons.setIconSize(QSize(btnSize, btnSize))
self.treeButtons.setContentsMargins(0, 0, 0, 0)
self.treeButtons.setStyleSheet("QToolBar {padding: 0;}")
self.projTabs.setCornerWidget(self.treeButtons, Qt.BottomRightCorner)
self.projDetailsBtn = QAction(self.tr("Project Details")) # Project Tree Stack
self.projDetailsBtn.setIcon(self.theTheme.getIcon("status_lines")) self.projStack = QStackedWidget()
self.projDetailsBtn.triggered.connect(lambda: self.showProjectDetailsDialog()) self.projStack.addWidget(self.treeView)
self.treeButtons.addAction(self.projDetailsBtn) self.projStack.addWidget(self.novelView)
self.projStack.currentChanged.connect(self._projStackChanged)
self.projStatsBtn = QAction(self.tr("Writing Statistics"))
self.projStatsBtn.setIcon(self.theTheme.getIcon("status_stats"))
self.projStatsBtn.triggered.connect(lambda: self.showWritingStatsDialog())
self.treeButtons.addAction(self.projStatsBtn)
self.projSettingsBtn = QAction(self.tr("Project Settings"))
self.projSettingsBtn.setIcon(self.theTheme.getIcon("settings"))
self.projSettingsBtn.triggered.connect(lambda: self.showProjectSettingsDialog())
self.treeButtons.addAction(self.projSettingsBtn)
# Project Tree View # Project Tree View
self.treePane = QWidget() self.treePane = QWidget()
self.treeBox = QVBoxLayout() self.treeBox = QVBoxLayout()
self.treeBox.setContentsMargins(0, 0, 0, 0) self.treeBox.setContentsMargins(0, 0, 0, 0)
self.treeBox.setSpacing(mPx) self.treeBox.setSpacing(mPx)
self.treeBox.addWidget(self.projTabs) self.treeBox.addWidget(self.projStack)
self.treeBox.addWidget(self.treeMeta) self.treeBox.addWidget(self.treeMeta)
self.treePane.setLayout(self.treeBox) self.treePane.setLayout(self.treeBox)
@@ -178,47 +156,42 @@ class GuiMain(QMainWindow):
self.splitView = QSplitter(Qt.Vertical) self.splitView = QSplitter(Qt.Vertical)
self.splitView.addWidget(self.docViewer) self.splitView.addWidget(self.docViewer)
self.splitView.addWidget(self.viewMeta) self.splitView.addWidget(self.viewMeta)
self.splitView.setHandleWidth(hWd)
self.splitView.setSizes(self.mainConf.getViewPanePos()) self.splitView.setSizes(self.mainConf.getViewPanePos())
# Splitter : Document Editor / Document Viewer # Splitter : Document Editor / Document Viewer
self.splitDocs = QSplitter(Qt.Horizontal) self.splitDocs = QSplitter(Qt.Horizontal)
self.splitDocs.addWidget(self.docEditor) self.splitDocs.addWidget(self.docEditor)
self.splitDocs.addWidget(self.splitView) self.splitDocs.addWidget(self.splitView)
self.splitDocs.setHandleWidth(hWd)
# Splitter : Project Outlie / Outline Details
self.splitOutline = QSplitter(Qt.Vertical)
self.splitOutline.addWidget(self.projView)
self.splitOutline.addWidget(self.projMeta)
self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
# Main Tabs : Editor / Outline
self.mainTabs = QTabWidget()
self.mainTabs.setTabPosition(QTabWidget.East)
self.mainTabs.setStyleSheet("QTabWidget::pane {border: 0;}")
self.mainTabs.addTab(self.splitDocs, self.tr("Editor"))
self.mainTabs.addTab(self.splitOutline, self.tr("Outline"))
self.mainTabs.currentChanged.connect(self._mainTabChanged)
# Splitter : Project Tree / Main Tabs # Splitter : Project Tree / Main Tabs
self.splitMain = QSplitter(Qt.Horizontal) self.splitMain = QSplitter(Qt.Horizontal)
self.splitMain.setContentsMargins(mPx, mPx, mPx, mPx) self.splitMain.setContentsMargins(0, 0, 0, 0)
self.splitMain.addWidget(self.treePane) self.splitMain.addWidget(self.treePane)
self.splitMain.addWidget(self.mainTabs) self.splitMain.addWidget(self.splitDocs)
self.splitMain.setHandleWidth(hWd)
self.splitMain.setSizes(self.mainConf.getMainPanePos()) self.splitMain.setSizes(self.mainConf.getMainPanePos())
# Main Stack : Editor / Outline
self.mainStack = QStackedWidget()
self.mainStack.addWidget(self.splitMain)
self.mainStack.addWidget(self.projView)
self.mainStack.currentChanged.connect(self._mainStackChanged)
# Indices of Splitter Widgets # Indices of Splitter Widgets
self.idxTree = self.splitMain.indexOf(self.treePane) self.idxTree = self.splitMain.indexOf(self.treePane)
self.idxMain = self.splitMain.indexOf(self.mainTabs) self.idxMain = self.splitMain.indexOf(self.splitDocs)
self.idxEditor = self.splitDocs.indexOf(self.docEditor) self.idxEditor = self.splitDocs.indexOf(self.docEditor)
self.idxViewer = self.splitDocs.indexOf(self.splitView) self.idxViewer = self.splitDocs.indexOf(self.splitView)
self.idxViewDoc = self.splitView.indexOf(self.docViewer) self.idxViewDoc = self.splitView.indexOf(self.docViewer)
self.idxViewMeta = self.splitView.indexOf(self.viewMeta) self.idxViewMeta = self.splitView.indexOf(self.viewMeta)
# Indices of Tab Widgets # Indices of Tab Widgets
self.idxTabEdit = self.mainTabs.indexOf(self.splitDocs) self.idxEditorView = self.mainStack.indexOf(self.splitMain)
self.idxTabProj = self.mainTabs.indexOf(self.splitOutline) self.idxOutlineView = self.mainStack.indexOf(self.projView)
self.idxTreeView = self.projTabs.indexOf(self.treeView) self.idxTreeView = self.projStack.indexOf(self.treeView)
self.idxNovelView = self.projTabs.indexOf(self.novelView) self.idxNovelView = self.projStack.indexOf(self.novelView)
# Splitter Behaviour # Splitter Behaviour
self.splitMain.setCollapsible(self.idxTree, False) self.splitMain.setCollapsible(self.idxTree, False)
@@ -237,8 +210,9 @@ class GuiMain(QMainWindow):
# Set Main Window Elements # Set Main Window Elements
self.setMenuBar(self.mainMenu) self.setMenuBar(self.mainMenu)
self.setCentralWidget(self.splitMain) self.setCentralWidget(self.mainStack)
self.setStatusBar(self.statusBar) self.setStatusBar(self.statusBar)
self.addToolBar(Qt.LeftToolBarArea, self.viewsBar)
# Finalise Initialisation # Finalise Initialisation
# ======================= # =======================
@@ -323,7 +297,7 @@ class GuiMain(QMainWindow):
self.docEditor.clearEditor() self.docEditor.clearEditor()
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.closeDocViewer() self.closeDocViewer()
self.projMeta.clearDetails() self.projView.clearOutline()
# General # General
self.statusBar.clearStatus() self.statusBar.clearStatus()
@@ -385,6 +359,7 @@ class GuiMain(QMainWindow):
self.rebuildTrees() self.rebuildTrees()
self.saveProject() self.saveProject()
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.projView.updateRootItem(None)
self.rebuildIndex(beQuiet=True) self.rebuildIndex(beQuiet=True)
self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setRefTime(self.theProject.projOpened)
self.statusBar.setProjectStatus(nwState.GOOD) self.statusBar.setProjectStatus(nwState.GOOD)
@@ -446,10 +421,10 @@ class GuiMain(QMainWindow):
self.idleRefTime = time() self.idleRefTime = time()
self.idleTime = 0.0 self.idleTime = 0.0
self.theIndex.clearIndex() self.theProject.index.clearIndex()
self.clearGUI() self.clearGUI()
self.hasProject = False self.hasProject = False
self.mainTabs.setCurrentWidget(self.splitDocs) self._changeView(nwView.PROJECT)
return saveOK return saveOK
@@ -465,7 +440,7 @@ class GuiMain(QMainWindow):
return False return False
# Switch main tab to editor view # Switch main tab to editor view
self.mainTabs.setCurrentWidget(self.splitDocs) self._changeView(nwView.PROJECT)
# Try to open the project # Try to open the project
if not self.theProject.openProject(projFile): if not self.theProject.openProject(projFile):
@@ -523,15 +498,15 @@ class GuiMain(QMainWindow):
self.idleTime = 0.0 self.idleTime = 0.0
# Load the tag index # Load the tag index
self.theIndex.loadIndex() self.theProject.index.loadIndex()
# Update GUI # Update GUI
self._updateWindowTitle(self.theProject.projName) self._updateWindowTitle(self.theProject.projName)
self.rebuildTrees() self.rebuildTrees()
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.docEditor.toggleSpellCheck(self.theProject.spellCheck) self.docEditor.toggleSpellCheck(self.theProject.spellCheck)
self.mainMenu.setAutoOutline(self.theProject.autoOutline)
self.statusBar.setRefTime(self.theProject.projOpened) self.statusBar.setRefTime(self.theProject.projOpened)
self.projView.updateRootItem(None)
self._updateStatusWordCount() self._updateStatusWordCount()
# Restore previously open documents, if any # Restore previously open documents, if any
@@ -542,10 +517,10 @@ class GuiMain(QMainWindow):
self.viewDocument(self.theProject.lastViewed) self.viewDocument(self.theProject.lastViewed)
# Check if we need to rebuild the index # Check if we need to rebuild the index
if self.theIndex.indexBroken: if self.theProject.index.indexBroken:
self.makeAlert(self.tr( self.makeAlert(self.tr(
"The project index is outdated or broken. Rebuilding index." "The project index is outdated or broken. Rebuilding index."
), nwAlert.WARN) ), nwAlert.INFO)
self.rebuildIndex() self.rebuildIndex()
# Make sure the changed status is set to false on things opened # Make sure the changed status is set to false on things opened
@@ -566,7 +541,7 @@ class GuiMain(QMainWindow):
self.treeView.saveTreeOrder() self.treeView.saveTreeOrder()
if self.theProject.saveProject(autoSave=autoSave): if self.theProject.saveProject(autoSave=autoSave):
self.theIndex.saveIndex() self.theProject.index.saveIndex()
return True return True
@@ -599,12 +574,12 @@ class GuiMain(QMainWindow):
logger.error("No project open") logger.error("No project open")
return False return False
if not self.theProject.projTree.checkType(tHandle, nwItemType.FILE): if not self.theProject.tree.checkType(tHandle, nwItemType.FILE):
logger.debug("Requested item '%s' is not a document", tHandle) logger.debug("Requested item '%s' is not a document", tHandle)
return False return False
self.closeDocument() self.closeDocument()
self.mainTabs.setCurrentWidget(self.splitDocs) self._changeView(nwView.EDITOR)
if self.docEditor.loadText(tHandle, tLine): if self.docEditor.loadText(tHandle, tLine):
if changeFocus: if changeFocus:
self.docEditor.setFocus() self.docEditor.setFocus()
@@ -623,12 +598,11 @@ class GuiMain(QMainWindow):
logger.error("No project open") logger.error("No project open")
return False return False
self.treeView.flushTreeOrder()
nHandle = None # The next handle after tHandle nHandle = None # The next handle after tHandle
fHandle = None # The first file handle we encounter fHandle = None # The first file handle we encounter
foundIt = False # We've found tHandle, pick the next we see foundIt = False # We've found tHandle, pick the next we see
for tItem in self.theProject.projTree: for tItem in self.theProject.tree:
if not self.theProject.projTree.checkType(tItem.itemHandle, nwItemType.FILE): if not self.theProject.tree.checkType(tItem.itemHandle, nwItemType.FILE):
continue continue
if fHandle is None: if fHandle is None:
fHandle = tItem.itemHandle fHandle = tItem.itemHandle
@@ -687,7 +661,7 @@ class GuiMain(QMainWindow):
return False return False
# Make sure main tab is in Editor view # Make sure main tab is in Editor view
self.mainTabs.setCurrentWidget(self.splitDocs) self._changeView(nwView.EDITOR)
logger.debug("Viewing document with handle '%s'", tHandle) logger.debug("Viewing document with handle '%s'", tHandle)
if self.docViewer.loadText(tHandle): if self.docViewer.loadText(tHandle):
@@ -817,7 +791,7 @@ class GuiMain(QMainWindow):
tHandle = self.treeView.getSelectedHandle() tHandle = self.treeView.getSelectedHandle()
elif self.novelView.hasFocus(): elif self.novelView.hasFocus():
tHandle, tLine = self.novelView.getSelectedHandle() tHandle, tLine = self.novelView.getSelectedHandle()
elif self.projView.hasFocus(): elif self.projView.treeFocus():
tHandle, tLine = self.projView.getSelectedHandle() tHandle, tLine = self.projView.getSelectedHandle()
else: else:
logger.warning("No item selected") logger.warning("No item selected")
@@ -840,28 +814,11 @@ class GuiMain(QMainWindow):
tHandle = self.docEditor.docHandle() tHandle = self.docEditor.docHandle()
else: else:
tHandle = self.treeView.getSelectedHandle() tHandle = self.treeView.getSelectedHandle()
if tHandle:
return self.treeView.editTreeItem(tHandle)
if tHandle is None:
logger.warning("No item selected")
return False return False
tItem = self.theProject.projTree[tHandle]
if tItem is None:
return False
if tItem.itemType not in nwLists.REG_TYPES:
return False
logger.verbose("Requesting change to item '%s'", tHandle)
dlgProj = GuiItemEditor(self, tHandle)
dlgProj.exec_()
if dlgProj.result() == QDialog.Accepted:
self.treeView.setTreeItemValues(tHandle)
self.treeMeta.updateViewBox(tHandle)
self.docEditor.updateDocInfo(tHandle)
self.docViewer.updateDocInfo(tHandle)
return True
def rebuildTrees(self): def rebuildTrees(self):
"""Rebuild the project tree. """Rebuild the project tree.
""" """
@@ -872,7 +829,7 @@ class GuiMain(QMainWindow):
def requestNovelTreeRefresh(self): def requestNovelTreeRefresh(self):
"""Update the novel tree, but only if it is visible. """Update the novel tree, but only if it is visible.
""" """
if self.projTabs.currentIndex() == self.idxNovelView and self.hasProject: if self.projStack.currentIndex() == self.idxNovelView and self.hasProject:
self.novelView.refreshTree() self.novelView.refreshTree()
return True return True
return False return False
@@ -889,25 +846,16 @@ class GuiMain(QMainWindow):
tStart = time() tStart = time()
self.treeView.saveTreeOrder() self.treeView.saveTreeOrder()
self.theIndex.clearIndex() self.theProject.index.clearIndex()
for tItem in self.theProject.projTree: for tItem in self.theProject.tree:
if tItem is None: # pragma: no cover
continue # This is a bug trap
if tItem is not None: logger.verbose("Indexing '%s'", tItem.itemName)
self.setStatus(self.tr("Indexing: '{0}'").format(tItem.itemName)) if self.theProject.index.reIndexHandle(tItem.itemHandle):
else: # Update Word Counts
self.setStatus(self.tr("Indexing: '{0}'").format(self.tr("Unknown item"))) self.treeView.propagateCount(tItem.itemHandle, tItem.wordCount, countChildren=True)
if tItem is not None and tItem.itemType == nwItemType.FILE:
logger.verbose("Scanning '%s'", tItem.itemName)
self.theIndex.reIndexHandle(tItem.itemHandle)
# Get Word Counts
cC, wC, pC = self.theIndex.getCounts(tItem.itemHandle)
tItem.setCharCount(cC)
tItem.setWordCount(wC)
tItem.setParaCount(pC)
self.treeView.propagateCount(tItem.itemHandle, wC)
self.treeView.setTreeItemValues(tItem.itemHandle) self.treeView.setTreeItemValues(tItem.itemHandle)
tEnd = time() tEnd = time()
@@ -925,19 +873,6 @@ class GuiMain(QMainWindow):
return True return True
def rebuildOutline(self):
"""Force a rebuild of the Outline view.
"""
if not self.hasProject:
logger.error("No project open")
return False
logger.verbose("Forcing a rebuild of the Project Outline")
self.mainTabs.setCurrentWidget(self.splitOutline)
self.projView.refreshTree(overRide=True)
return True
## ##
# Main Dialogs # Main Dialogs
## ##
@@ -985,7 +920,6 @@ class GuiMain(QMainWindow):
self.treeView.initTree() self.treeView.initTree()
self.novelView.initTree() self.novelView.initTree()
self.projView.initOutline() self.projView.initOutline()
self.projMeta.initDetails()
self._updateStatusWordCount() self._updateStatusWordCount()
return return
@@ -1002,7 +936,9 @@ class GuiMain(QMainWindow):
if dlgProj.result() == QDialog.Accepted: if dlgProj.result() == QDialog.Accepted:
logger.debug("Applying new project settings") logger.debug("Applying new project settings")
if dlgProj.spellChanged:
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.treeMeta.refreshDetails()
self._updateWindowTitle(self.theProject.projName) self._updateWindowTitle(self.theProject.projName)
return True return True
@@ -1014,8 +950,6 @@ class GuiMain(QMainWindow):
logger.error("No project open") logger.error("No project open")
return False return False
self.treeView.flushTreeOrder()
dlgDetails = getGuiItem("GuiProjectDetails") dlgDetails = getGuiItem("GuiProjectDetails")
if dlgDetails is None: if dlgDetails is None:
dlgDetails = GuiProjectDetails(self) dlgDetails = GuiProjectDetails(self)
@@ -1220,7 +1154,7 @@ class GuiMain(QMainWindow):
if not self.isFocusMode: if not self.isFocusMode:
self.mainConf.setMainPanePos(self.splitMain.sizes()) self.mainConf.setMainPanePos(self.splitMain.sizes())
self.mainConf.setDocPanePos(self.splitDocs.sizes()) self.mainConf.setDocPanePos(self.splitDocs.sizes())
self.mainConf.setOutlinePanePos(self.splitOutline.sizes()) self.mainConf.setOutlinePanePos(self.projView.splitSizes())
if self.viewMeta.isVisible(): if self.viewMeta.isVisible():
self.mainConf.setViewPanePos(self.splitView.sizes()) self.mainConf.setViewPanePos(self.splitView.sizes())
@@ -1244,20 +1178,20 @@ class GuiMain(QMainWindow):
"""Switch focus between main GUI views. """Switch focus between main GUI views.
""" """
if paneNo == nwWidget.TREE: if paneNo == nwWidget.TREE:
tabIdx = self.projTabs.currentIndex() tabIdx = self.projStack.currentIndex()
if tabIdx == self.idxTreeView: if tabIdx == self.idxTreeView:
self.treeView.setFocus() self.treeView.setFocus()
elif tabIdx == self.idxNovelView: elif tabIdx == self.idxNovelView:
self.novelView.setFocus() self.novelView.setFocus()
elif paneNo == nwWidget.EDITOR: elif paneNo == nwWidget.EDITOR:
self.mainTabs.setCurrentWidget(self.splitDocs) self._changeView(nwView.EDITOR)
self.docEditor.setFocus() self.docEditor.setFocus()
elif paneNo == nwWidget.VIEWER: elif paneNo == nwWidget.VIEWER:
self.mainTabs.setCurrentWidget(self.splitDocs) self._changeView(nwView.EDITOR)
self.docViewer.setFocus() self.docViewer.setFocus()
elif paneNo == nwWidget.OUTLINE: elif paneNo == nwWidget.OUTLINE:
self.mainTabs.setCurrentWidget(self.splitOutline) self._changeView(nwView.OUTLINE)
self.projView.setFocus() self.projView.setTreeFocus()
return return
def closeDocEditor(self): def closeDocEditor(self):
@@ -1289,7 +1223,6 @@ class GuiMain(QMainWindow):
self.mainMenu.setFocusMode(self.isFocusMode) self.mainMenu.setFocusMode(self.isFocusMode)
if self.isFocusMode: if self.isFocusMode:
logger.debug("Activating Focus Mode") logger.debug("Activating Focus Mode")
self.mainTabs.setCurrentWidget(self.splitDocs)
self.switchFocus(nwWidget.EDITOR) self.switchFocus(nwWidget.EDITOR)
else: else:
logger.debug("Deactivating Focus Mode") logger.debug("Deactivating Focus Mode")
@@ -1298,7 +1231,7 @@ class GuiMain(QMainWindow):
self.treePane.setVisible(isVisible) self.treePane.setVisible(isVisible)
self.statusBar.setVisible(isVisible) self.statusBar.setVisible(isVisible)
self.mainMenu.setVisible(isVisible) self.mainMenu.setVisible(isVisible)
self.mainTabs.tabBar().setVisible(isVisible) self.viewsBar.setVisible(isVisible)
hideDocFooter = self.isFocusMode and self.mainConf.hideFocusFooter hideDocFooter = self.isFocusMode and self.mainConf.hideFocusFooter
self.docEditor.docFooter.setVisible(not hideDocFooter) self.docEditor.docFooter.setVisible(not hideDocFooter)
@@ -1426,7 +1359,7 @@ class GuiMain(QMainWindow):
return True return True
def _updateWindowTitle(self, projName=None): def _updateWindowTitle(self, projName=None):
"""Set the window title and add the project's working title. """Set the window title and add the project's name.
""" """
winTitle = self.mainConf.appName winTitle = self.mainConf.appName
if projName is not None: if projName is not None:
@@ -1469,9 +1402,9 @@ class GuiMain(QMainWindow):
"popMinimal": newProj.field("popMinimal"), "popMinimal": newProj.field("popMinimal"),
"popCustom": newProj.field("popCustom"), "popCustom": newProj.field("popCustom"),
"addRoots": [], "addRoots": [],
"addNotes": False,
"numChapters": 0, "numChapters": 0,
"numScenes": 0, "numScenes": 0,
"chFolders": False,
} }
if newProj.field("popCustom"): if newProj.field("popCustom"):
addRoots = [] addRoots = []
@@ -1481,19 +1414,30 @@ class GuiMain(QMainWindow):
addRoots.append(nwItemClass.CHARACTER) addRoots.append(nwItemClass.CHARACTER)
if newProj.field("addWorld"): if newProj.field("addWorld"):
addRoots.append(nwItemClass.WORLD) addRoots.append(nwItemClass.WORLD)
if newProj.field("addTime"):
addRoots.append(nwItemClass.TIMELINE)
if newProj.field("addObject"):
addRoots.append(nwItemClass.OBJECT)
if newProj.field("addEntity"):
addRoots.append(nwItemClass.ENTITY)
projData["addRoots"] = addRoots projData["addRoots"] = addRoots
projData["addNotes"] = newProj.field("addNotes")
projData["numChapters"] = newProj.field("numChapters") projData["numChapters"] = newProj.field("numChapters")
projData["numScenes"] = newProj.field("numScenes") projData["numScenes"] = newProj.field("numScenes")
projData["chFolders"] = newProj.field("chFolders")
return projData return projData
def _getTagSource(self, tTag):
"""A wrapper function for the index lookup of a tag that will
display an alert if the tag cannot be found.
"""
tHandle, sTitle = self.theProject.index.getTagSource(tTag)
if tHandle is None:
self.makeAlert(self.tr(
"Could not find the reference for tag '{0}'. It either doesn't "
"exist, or the index is out of date. The index can be updated "
"from the Tools menu, or by pressing {1}."
).format(
tTag, "F9"
), nwAlert.ERROR)
return None, None
return tHandle, sTitle
## ##
# Events # Events
## ##
@@ -1509,9 +1453,42 @@ class GuiMain(QMainWindow):
return return
## ##
# Slots # Private Slots
## ##
@pyqtSlot(str, Enum)
def _followTag(self, tTag, tMode):
"""Follow a tag after user interaction with a link.
"""
tHandle, sTitle = self._getTagSource(tTag)
if tHandle is not None:
if tMode == nwDocMode.EDIT:
self.openDocument(tHandle)
elif tMode == nwDocMode.VIEW:
self.viewDocument(tHandle=tHandle, tAnchor=f"#{sTitle}")
return
@pyqtSlot(nwView)
def _changeView(self, view):
"""Handle the requested change of view from the GuiViewBar.
"""
if view == nwView.EDITOR:
# Only change the main stack, but not the project stack
self.mainStack.setCurrentWidget(self.splitMain)
elif view == nwView.PROJECT:
self.mainStack.setCurrentWidget(self.splitMain)
self.projStack.setCurrentWidget(self.treeView)
elif view == nwView.NOVEL:
self.mainStack.setCurrentWidget(self.splitMain)
self.projStack.setCurrentWidget(self.novelView)
elif view == nwView.OUTLINE:
self.mainStack.setCurrentWidget(self.projView)
return
@pyqtSlot() @pyqtSlot()
def _timeTick(self): def _timeTick(self):
"""Triggered on every tick of the main timer. """Triggered on every tick of the main timer.
@@ -1567,11 +1544,17 @@ class GuiMain(QMainWindow):
@pyqtSlot("QTreeWidgetItem*", int) @pyqtSlot("QTreeWidgetItem*", int)
def _treeDoubleClick(self, tItem, colNo): def _treeDoubleClick(self, tItem, colNo):
"""The user double-clicked an item in the tree. If it is a file, """The user double-clicked an item in the tree. If it is a file,
we open it. Otherwise, we do nothing. we open it. Otherwise, we toggle the expanded status.
""" """
tHandle = self.treeView.getSelectedHandle() tHandle = self.treeView.getSelectedHandle()
if tHandle is not None: if tHandle is not None:
tItem = self.theProject.tree[tHandle]
if tItem is None:
return
if tItem.itemType == nwItemType.FILE:
self.openDocument(tHandle, changeFocus=False, doScroll=False) self.openDocument(tHandle, changeFocus=False, doScroll=False)
else:
self.treeView.toggleExpanded(tHandle)
return return
@pyqtSlot() @pyqtSlot()
@@ -1579,11 +1562,10 @@ class GuiMain(QMainWindow):
"""Triggered when there is a change to a novel item in the """Triggered when there is a change to a novel item in the
project tree. project tree.
""" """
if self.mainTabs.currentIndex() == self.idxTabProj: if self.mainStack.currentIndex() == self.idxOutlineView:
logger.verbose("Novel tree changed while Outline tab active") logger.verbose("Novel tree changed while Outline tab active")
if self.hasProject: if self.hasProject:
self.treeView.flushTreeOrder() self.projView.refreshView(novelChanged=True)
self.projView.refreshTree(novelChanged=True)
return return
@@ -1608,20 +1590,20 @@ class GuiMain(QMainWindow):
return return
@pyqtSlot(int) @pyqtSlot(int)
def _mainTabChanged(self, tabIndex): def _mainStackChanged(self, tabIndex):
"""Activated when the main window tab is changed. """Activated when the main window tab is changed.
""" """
if tabIndex == self.idxTabEdit: if tabIndex == self.idxEditorView:
logger.verbose("Editor tab activated") logger.verbose("Editor tab activated")
elif tabIndex == self.idxTabProj: elif tabIndex == self.idxOutlineView:
logger.verbose("Project outline tab activated") logger.verbose("Project outline tab activated")
if self.hasProject: if self.hasProject:
self.projView.refreshTree() self.projView.refreshView()
return return
@pyqtSlot(int) @pyqtSlot(int)
def _projTabsChanged(self, tabIndex): def _projStackChanged(self, tabIndex):
"""Activated when the project view tab is changed. """Activated when the project view tab is changed.
""" """
sHandle = None sHandle = None
+50 -55
View File
@@ -75,7 +75,6 @@ class GuiBuildNovel(QDialog):
self.theParent = theParent self.theParent = theParent
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
self.theProject = theParent.theProject self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.htmlText = [] # List of html documents self.htmlText = [] # List of html documents
self.htmlStyle = [] # List of html styles self.htmlStyle = [] # List of html styles
@@ -86,9 +85,10 @@ class GuiBuildNovel(QDialog):
self.setMinimumWidth(self.mainConf.pxInt(700)) self.setMinimumWidth(self.mainConf.pxInt(700))
self.setMinimumHeight(self.mainConf.pxInt(600)) self.setMinimumHeight(self.mainConf.pxInt(600))
pOptions = self.theProject.options
self.resize( self.resize(
self.mainConf.pxInt(self.optState.getInt("GuiBuildNovel", "winWidth", 900)), self.mainConf.pxInt(pOptions.getInt("GuiBuildNovel", "winWidth", 900)),
self.mainConf.pxInt(self.optState.getInt("GuiBuildNovel", "winHeight", 800)) self.mainConf.pxInt(pOptions.getInt("GuiBuildNovel", "winHeight", 800))
) )
self.docView = GuiBuildNovelDocView(self, self.theProject) self.docView = GuiBuildNovelDocView(self, self.theProject)
@@ -174,12 +174,12 @@ class GuiBuildNovel(QDialog):
self.hideScene = QSwitch(width=wS, height=hS) self.hideScene = QSwitch(width=wS, height=hS)
self.hideScene.setChecked( self.hideScene.setChecked(
self.optState.getBool("GuiBuildNovel", "hideScene", False) pOptions.getBool("GuiBuildNovel", "hideScene", False)
) )
self.hideSection = QSwitch(width=wS, height=hS) self.hideSection = QSwitch(width=wS, height=hS)
self.hideSection.setChecked( self.hideSection.setChecked(
self.optState.getBool("GuiBuildNovel", "hideSection", True) pOptions.getBool("GuiBuildNovel", "hideSection", True)
) )
# Wrapper boxes due to QGridView and QLineEdit expand bug # Wrapper boxes due to QGridView and QLineEdit expand bug
@@ -235,7 +235,7 @@ class GuiBuildNovel(QDialog):
self.textFont.setReadOnly(True) self.textFont.setReadOnly(True)
self.textFont.setMinimumWidth(xFmt) self.textFont.setMinimumWidth(xFmt)
self.textFont.setText( self.textFont.setText(
self.optState.getString("GuiBuildNovel", "textFont", self.mainConf.textFont) pOptions.getString("GuiBuildNovel", "textFont", self.mainConf.textFont)
) )
self.fontButton = QPushButton("...") self.fontButton = QPushButton("...")
self.fontButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("..."))) self.fontButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("...")))
@@ -247,7 +247,7 @@ class GuiBuildNovel(QDialog):
self.textSize.setMaximum(72) self.textSize.setMaximum(72)
self.textSize.setSingleStep(1) self.textSize.setSingleStep(1)
self.textSize.setValue( self.textSize.setValue(
self.optState.getInt("GuiBuildNovel", "textSize", self.mainConf.textSize) pOptions.getInt("GuiBuildNovel", "textSize", self.mainConf.textSize)
) )
self.lineHeight = QDoubleSpinBox(self) self.lineHeight = QDoubleSpinBox(self)
@@ -257,7 +257,7 @@ class GuiBuildNovel(QDialog):
self.lineHeight.setSingleStep(0.05) self.lineHeight.setSingleStep(0.05)
self.lineHeight.setDecimals(2) self.lineHeight.setDecimals(2)
self.lineHeight.setValue( self.lineHeight.setValue(
self.optState.getFloat("GuiBuildNovel", "lineHeight", 1.15) pOptions.getFloat("GuiBuildNovel", "lineHeight", 1.15)
) )
# Wrapper box due to QGridView and QLineEdit expand bug # Wrapper box due to QGridView and QLineEdit expand bug
@@ -291,12 +291,12 @@ class GuiBuildNovel(QDialog):
self.justifyText = QSwitch(width=wS, height=hS) self.justifyText = QSwitch(width=wS, height=hS)
self.justifyText.setChecked( self.justifyText.setChecked(
self.optState.getBool("GuiBuildNovel", "justifyText", False) pOptions.getBool("GuiBuildNovel", "justifyText", False)
) )
self.noStyling = QSwitch(width=wS, height=hS) self.noStyling = QSwitch(width=wS, height=hS)
self.noStyling.setChecked( self.noStyling.setChecked(
self.optState.getBool("GuiBuildNovel", "noStyling", False) pOptions.getBool("GuiBuildNovel", "noStyling", False)
) )
self.styleForm.addWidget(justifyLabel, 1, 0, 1, 1, Qt.AlignLeft) self.styleForm.addWidget(justifyLabel, 1, 0, 1, 1, Qt.AlignLeft)
@@ -316,22 +316,22 @@ class GuiBuildNovel(QDialog):
self.includeSynopsis = QSwitch(width=wS, height=hS) self.includeSynopsis = QSwitch(width=wS, height=hS)
self.includeSynopsis.setChecked( self.includeSynopsis.setChecked(
self.optState.getBool("GuiBuildNovel", "incSynopsis", False) pOptions.getBool("GuiBuildNovel", "incSynopsis", False)
) )
self.includeComments = QSwitch(width=wS, height=hS) self.includeComments = QSwitch(width=wS, height=hS)
self.includeComments.setChecked( self.includeComments.setChecked(
self.optState.getBool("GuiBuildNovel", "incComments", False) pOptions.getBool("GuiBuildNovel", "incComments", False)
) )
self.includeKeywords = QSwitch(width=wS, height=hS) self.includeKeywords = QSwitch(width=wS, height=hS)
self.includeKeywords.setChecked( self.includeKeywords.setChecked(
self.optState.getBool("GuiBuildNovel", "incKeywords", False) pOptions.getBool("GuiBuildNovel", "incKeywords", False)
) )
self.includeBody = QSwitch(width=wS, height=hS) self.includeBody = QSwitch(width=wS, height=hS)
self.includeBody.setChecked( self.includeBody.setChecked(
self.optState.getBool("GuiBuildNovel", "incBodyText", True) pOptions.getBool("GuiBuildNovel", "incBodyText", True)
) )
synopsisLabel = QLabel(self.tr("Include synopsis")) synopsisLabel = QLabel(self.tr("Include synopsis"))
@@ -360,17 +360,17 @@ class GuiBuildNovel(QDialog):
self.novelFiles = QSwitch(width=wS, height=hS) self.novelFiles = QSwitch(width=wS, height=hS)
self.novelFiles.setChecked( self.novelFiles.setChecked(
self.optState.getBool("GuiBuildNovel", "addNovel", True) pOptions.getBool("GuiBuildNovel", "addNovel", True)
) )
self.noteFiles = QSwitch(width=wS, height=hS) self.noteFiles = QSwitch(width=wS, height=hS)
self.noteFiles.setChecked( self.noteFiles.setChecked(
self.optState.getBool("GuiBuildNovel", "addNotes", False) pOptions.getBool("GuiBuildNovel", "addNotes", False)
) )
self.ignoreFlag = QSwitch(width=wS, height=hS) self.ignoreFlag = QSwitch(width=wS, height=hS)
self.ignoreFlag.setChecked( self.ignoreFlag.setChecked(
self.optState.getBool("GuiBuildNovel", "ignoreFlag", False) pOptions.getBool("GuiBuildNovel", "ignoreFlag", False)
) )
novelLabel = QLabel(self.tr("Include novel files")) novelLabel = QLabel(self.tr("Include novel files"))
@@ -396,12 +396,12 @@ class GuiBuildNovel(QDialog):
self.replaceTabs = QSwitch(width=wS, height=hS) self.replaceTabs = QSwitch(width=wS, height=hS)
self.replaceTabs.setChecked( self.replaceTabs.setChecked(
self.optState.getBool("GuiBuildNovel", "replaceTabs", False) pOptions.getBool("GuiBuildNovel", "replaceTabs", False)
) )
self.replaceUCode = QSwitch(width=wS, height=hS) self.replaceUCode = QSwitch(width=wS, height=hS)
self.replaceUCode.setChecked( self.replaceUCode.setChecked(
self.optState.getBool("GuiBuildNovel", "replaceUCode", False) pOptions.getBool("GuiBuildNovel", "replaceUCode", False)
) )
tabsLabel = QLabel(self.tr("Replace tabs with spaces")) tabsLabel = QLabel(self.tr("Replace tabs with spaces"))
@@ -493,9 +493,9 @@ class GuiBuildNovel(QDialog):
# Splitter Position # Splitter Position
boxWidth = self.mainConf.pxInt(350) boxWidth = self.mainConf.pxInt(350)
boxWidth = self.optState.getInt("GuiBuildNovel", "boxWidth", boxWidth) boxWidth = pOptions.getInt("GuiBuildNovel", "boxWidth", boxWidth)
docWidth = max(self.width() - boxWidth, 100) docWidth = max(self.width() - boxWidth, 100)
docWidth = self.optState.getInt("GuiBuildNovel", "docWidth", docWidth) docWidth = pOptions.getInt("GuiBuildNovel", "docWidth", docWidth)
# The Tool Box # The Tool Box
self.toolsBox = QVBoxLayout() self.toolsBox = QVBoxLayout()
@@ -659,6 +659,7 @@ class GuiBuildNovel(QDialog):
fmtUnnumbered = self.fmtUnnumbered.text() fmtUnnumbered = self.fmtUnnumbered.text()
fmtScene = self.fmtScene.text() fmtScene = self.fmtScene.text()
fmtSection = self.fmtSection.text() fmtSection = self.fmtSection.text()
buildLang = self.buildLang.currentData()
hideScene = self.hideScene.isChecked() hideScene = self.hideScene.isChecked()
hideSection = self.hideSection.isChecked() hideSection = self.hideSection.isChecked()
textFont = self.textFont.text() textFont = self.textFont.text()
@@ -676,7 +677,7 @@ class GuiBuildNovel(QDialog):
replaceUCode = self.replaceUCode.isChecked() replaceUCode = self.replaceUCode.isChecked()
# The language lookup dict is reloaded if needed # The language lookup dict is reloaded if needed
self.theProject.setProjectLang(self.buildLang.currentData()) self.theProject.setProjectLang(buildLang)
# Get font information # Get font information
fontInfo = QFontInfo(QFont(textFont, textSize)) fontInfo = QFontInfo(QFont(textFont, textSize))
@@ -706,16 +707,16 @@ class GuiBuildNovel(QDialog):
if isOdt: if isOdt:
bldObj.setColourHeaders(not noStyling) bldObj.setColourHeaders(not noStyling)
bldObj.setLanguage(buildLang)
bldObj.initDocument() bldObj.initDocument()
# Make sure the project and document is up to date # Make sure the project and document is up to date
self.theParent.treeView.flushTreeOrder()
self.theParent.saveDocument() self.theParent.saveDocument()
self.buildProgress.setMaximum(len(self.theProject.projTree)) self.buildProgress.setMaximum(len(self.theProject.tree))
self.buildProgress.setValue(0) self.buildProgress.setValue(0)
for nItt, tItem in enumerate(self.theProject.projTree): for nItt, tItem in enumerate(self.theProject.tree):
noteRoot = noteFiles noteRoot = noteFiles
noteRoot &= tItem.itemType == nwItemType.ROOT noteRoot &= tItem.itemType == nwItemType.ROOT
@@ -780,14 +781,12 @@ class GuiBuildNovel(QDialog):
if theItem is None: if theItem is None:
return False return False
if not theItem.isExported and not ignoreFlag: if not (theItem.isExported or ignoreFlag):
return False return False
isNone = theItem.itemType != nwItemType.FILE isNone = theItem.itemType != nwItemType.FILE
isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT
isNone |= theItem.itemClass == nwItemClass.NO_CLASS isNone |= theItem.isInactive()
isNone |= theItem.itemClass == nwItemClass.TRASH
isNone |= theItem.itemParent == self.theProject.projTree.trashRoot()
isNone |= theItem.itemParent is None isNone |= theItem.itemParent is None
isNote = theItem.itemLayout == nwItemLayout.NOTE isNote = theItem.itemLayout == nwItemLayout.NOTE
isNovel = not isNone and not isNote isNovel = not isNone and not isNote
@@ -799,10 +798,6 @@ class GuiBuildNovel(QDialog):
if isNovel and not novelFiles: if isNovel and not novelFiles:
return False return False
rootItem = self.theProject.projTree.getRootItem(theItem.itemHandle)
if rootItem.itemClass == nwItemClass.ARCHIVE:
return False
return True return True
def _saveDocument(self, theFmt): def _saveDocument(self, theFmt):
@@ -1159,28 +1154,28 @@ class GuiBuildNovel(QDialog):
self.theProject.setProjectLang(buildLang) self.theProject.setProjectLang(buildLang)
# GUI Settings # GUI Settings
self.optState.setValue("GuiBuildNovel", "hideScene", hideScene) pOptions = self.theProject.options
self.optState.setValue("GuiBuildNovel", "hideSection", hideSection) pOptions.setValue("GuiBuildNovel", "hideScene", hideScene)
self.optState.setValue("GuiBuildNovel", "winWidth", winWidth) pOptions.setValue("GuiBuildNovel", "hideSection", hideSection)
self.optState.setValue("GuiBuildNovel", "winHeight", winHeight) pOptions.setValue("GuiBuildNovel", "winWidth", winWidth)
self.optState.setValue("GuiBuildNovel", "boxWidth", boxWidth) pOptions.setValue("GuiBuildNovel", "winHeight", winHeight)
self.optState.setValue("GuiBuildNovel", "docWidth", docWidth) pOptions.setValue("GuiBuildNovel", "boxWidth", boxWidth)
self.optState.setValue("GuiBuildNovel", "justifyText", justifyText) pOptions.setValue("GuiBuildNovel", "docWidth", docWidth)
self.optState.setValue("GuiBuildNovel", "noStyling", noStyling) pOptions.setValue("GuiBuildNovel", "justifyText", justifyText)
self.optState.setValue("GuiBuildNovel", "textFont", textFont) pOptions.setValue("GuiBuildNovel", "noStyling", noStyling)
self.optState.setValue("GuiBuildNovel", "textSize", textSize) pOptions.setValue("GuiBuildNovel", "textFont", textFont)
self.optState.setValue("GuiBuildNovel", "lineHeight", lineHeight) pOptions.setValue("GuiBuildNovel", "textSize", textSize)
self.optState.setValue("GuiBuildNovel", "addNovel", novelFiles) pOptions.setValue("GuiBuildNovel", "lineHeight", lineHeight)
self.optState.setValue("GuiBuildNovel", "addNotes", noteFiles) pOptions.setValue("GuiBuildNovel", "addNovel", novelFiles)
self.optState.setValue("GuiBuildNovel", "ignoreFlag", ignoreFlag) pOptions.setValue("GuiBuildNovel", "addNotes", noteFiles)
self.optState.setValue("GuiBuildNovel", "incSynopsis", incSynopsis) pOptions.setValue("GuiBuildNovel", "ignoreFlag", ignoreFlag)
self.optState.setValue("GuiBuildNovel", "incComments", incComments) pOptions.setValue("GuiBuildNovel", "incSynopsis", incSynopsis)
self.optState.setValue("GuiBuildNovel", "incKeywords", incKeywords) pOptions.setValue("GuiBuildNovel", "incComments", incComments)
self.optState.setValue("GuiBuildNovel", "incBodyText", incBodyText) pOptions.setValue("GuiBuildNovel", "incKeywords", incKeywords)
self.optState.setValue("GuiBuildNovel", "replaceTabs", replaceTabs) pOptions.setValue("GuiBuildNovel", "incBodyText", incBodyText)
self.optState.setValue("GuiBuildNovel", "replaceUCode", replaceUCode) pOptions.setValue("GuiBuildNovel", "replaceTabs", replaceTabs)
pOptions.setValue("GuiBuildNovel", "replaceUCode", replaceUCode)
self.optState.saveSettings() pOptions.saveSettings()
return return
+1 -1
View File
@@ -72,7 +72,7 @@ class GuiLipsum(QDialog):
# Form # Form
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Insert Lorem Ipsum Text"))) self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Insert Lorem Ipsum Text")))
self.paraLabel = QLabel(self.tr("Number of pragraphs")) self.paraLabel = QLabel(self.tr("Number of paragraphs"))
self.paraCount = QSpinBox() self.paraCount = QSpinBox()
self.paraCount.setMinimum(1) self.paraCount.setMinimum(1)
self.paraCount.setMaximum(100) self.paraCount.setMaximum(100)
+116 -87
View File
@@ -31,12 +31,10 @@ from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QWizard, QWizardPage, QLabel, QVBoxLayout, QLineEdit, QPlainTextEdit, QWizard, QWizardPage, QLabel, QVBoxLayout, QLineEdit, QPlainTextEdit,
QPushButton, QFileDialog, QHBoxLayout, QRadioButton, QFormLayout, QPushButton, QFileDialog, QHBoxLayout, QRadioButton, QFormLayout,
QGroupBox, QGridLayout, QSpinBox QGridLayout, QSpinBox
) )
from novelwriter.enum import nwItemClass
from novelwriter.common import makeFileNameSafe from novelwriter.common import makeFileNameSafe
from novelwriter.constants import trConst, nwLabels
from novelwriter.gui.custom import QSwitch from novelwriter.gui.custom import QSwitch
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -98,10 +96,10 @@ class ProjWizardIntroPage(QWizardPage):
self.setTitle(self.tr("Create New Project")) self.setTitle(self.tr("Create New Project"))
self.theText = QLabel(self.tr( self.theText = QLabel(self.tr(
"Provide at least a working title. The working title should not " "Provide at least a project name. The project name should not "
"be change beyond this point as it is used by the application for " "be changed beyond this point as it is used for generating file "
"generating file names for for instance backups. The other fields " "names for for instance backups. The other fields are optional "
"are optional and can be changed at any time in Project Settings." "and can be changed at any time in Project Settings."
)) ))
self.theText.setWordWrap(True) self.theText.setWordWrap(True)
@@ -134,7 +132,7 @@ class ProjWizardIntroPage(QWizardPage):
self.projAuthors.setPlaceholderText(self.tr("Optional. One name per line.")) self.projAuthors.setPlaceholderText(self.tr("Optional. One name per line."))
self.mainForm = QFormLayout() self.mainForm = QFormLayout()
self.mainForm.addRow(self.tr("Working Title"), self.projName) self.mainForm.addRow(self.tr("Project Name"), self.projName)
self.mainForm.addRow(self.tr("Novel Title"), self.projTitle) self.mainForm.addRow(self.tr("Novel Title"), self.projTitle)
self.mainForm.addRow(self.tr("Author(s)"), self.projAuthors) self.mainForm.addRow(self.tr("Author(s)"), self.projAuthors)
self.mainForm.setVerticalSpacing(fS) self.mainForm.setVerticalSpacing(fS)
@@ -185,6 +183,9 @@ class ProjWizardFolderPage(QWizardPage):
self.browseButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("..."))) self.browseButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("...")))
self.browseButton.clicked.connect(self._doBrowse) self.browseButton.clicked.connect(self._doBrowse)
self.errLabel = QLabel("")
self.errLabel.setWordWrap(True)
self.mainForm = QHBoxLayout() self.mainForm = QHBoxLayout()
self.mainForm.addWidget(QLabel(self.tr("Project Path")), 0) self.mainForm.addWidget(QLabel(self.tr("Project Path")), 0)
self.mainForm.addWidget(self.projPath, 1) self.mainForm.addWidget(self.projPath, 1)
@@ -198,11 +199,36 @@ class ProjWizardFolderPage(QWizardPage):
self.outerBox.setSpacing(vS) self.outerBox.setSpacing(vS)
self.outerBox.addWidget(self.theText) self.outerBox.addWidget(self.theText)
self.outerBox.addLayout(self.mainForm) self.outerBox.addLayout(self.mainForm)
self.outerBox.addWidget(self.errLabel)
self.outerBox.addStretch(1) self.outerBox.addStretch(1)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
return return
def isComplete(self):
"""Check that the selected path isn't already being used.
"""
self.errLabel.setText("")
if not QWizardPage.isComplete(self):
return False
setPath = os.path.abspath(os.path.expanduser(self.projPath.text()))
parPath = os.path.dirname(setPath)
logger.verbose("Path is: %s", setPath)
if parPath and not os.path.isdir(parPath):
self.errLabel.setText(self.tr(
"Error: A project folder cannot be created using this path."
))
return False
if os.path.exists(setPath):
self.errLabel.setText(self.tr(
"Error: The selected path already exists."
))
return False
return True
## ##
# Slots # Slots
## ##
@@ -296,68 +322,28 @@ class ProjWizardCustomPage(QWizardPage):
self.setTitle(self.tr("Custom Project Options")) self.setTitle(self.tr("Custom Project Options"))
self.theText = QLabel(self.tr( self.theText = QLabel(self.tr(
"Select which additional root folders to make, and how to populate " "Select which additional elements to populate the project with. "
"the Novel folder. If you don't want to add chapters or scenes, set " "You can skip making chapters and add only scenes by setting the "
"the values to 0. You can add scenes without chapters." "number of chapters to 0."
)) ))
self.theText.setWordWrap(True) self.theText.setWordWrap(True)
vS = self.mainConf.pxInt(12) cM = self.mainConf.pxInt(12)
mH = self.mainConf.pxInt(26)
fS = self.mainConf.pxInt(4)
# Root Folders # Root Folders
self.rootGroup = QGroupBox(self.tr("Additional Root Folders"))
self.rootForm = QGridLayout()
self.rootGroup.setLayout(self.rootForm)
self.lblPlot = QLabel(self.tr("{0} folder").format(
trConst(nwLabels.CLASS_NAME[nwItemClass.PLOT]))
)
self.lblChar = QLabel(self.tr("{0} folder").format(
trConst(nwLabels.CLASS_NAME[nwItemClass.CHARACTER]))
)
self.lblWorld = QLabel(self.tr("{0} folder").format(
trConst(nwLabels.CLASS_NAME[nwItemClass.WORLD]))
)
self.lblTime = QLabel(self.tr("{0} folder").format(
trConst(nwLabels.CLASS_NAME[nwItemClass.TIMELINE]))
)
self.lblObject = QLabel(self.tr("{0} folder").format(
trConst(nwLabels.CLASS_NAME[nwItemClass.OBJECT]))
)
self.lblEntity = QLabel(self.tr("{0} folder").format(
trConst(nwLabels.CLASS_NAME[nwItemClass.ENTITY]))
)
self.addPlot = QSwitch() self.addPlot = QSwitch()
self.addChar = QSwitch() self.addChar = QSwitch()
self.addWorld = QSwitch() self.addWorld = QSwitch()
self.addTime = QSwitch() self.addNotes = QSwitch()
self.addObject = QSwitch()
self.addEntity = QSwitch()
self.addPlot.setChecked(True) self.addPlot.setChecked(True)
self.addChar.setChecked(True) self.addChar.setChecked(True)
self.addWorld.setChecked(True) self.addWorld.setChecked(False)
self.addNotes.setChecked(False)
self.rootForm.addWidget(self.lblPlot, 0, 0)
self.rootForm.addWidget(self.lblChar, 1, 0)
self.rootForm.addWidget(self.lblWorld, 2, 0)
self.rootForm.addWidget(self.lblTime, 3, 0)
self.rootForm.addWidget(self.lblObject, 4, 0)
self.rootForm.addWidget(self.lblEntity, 5, 0)
self.rootForm.addWidget(self.addPlot, 0, 1, 1, 1, Qt.AlignRight)
self.rootForm.addWidget(self.addChar, 1, 1, 1, 1, Qt.AlignRight)
self.rootForm.addWidget(self.addWorld, 2, 1, 1, 1, Qt.AlignRight)
self.rootForm.addWidget(self.addTime, 3, 1, 1, 1, Qt.AlignRight)
self.rootForm.addWidget(self.addObject, 4, 1, 1, 1, Qt.AlignRight)
self.rootForm.addWidget(self.addEntity, 5, 1, 1, 1, Qt.AlignRight)
self.rootForm.setRowStretch(6, 1)
# Novel Options
self.novelGroup = QGroupBox(self.tr("Populate Novel Folder"))
self.novelForm = QGridLayout()
self.novelGroup.setLayout(self.novelForm)
# Generate Content
self.numChapters = QSpinBox() self.numChapters = QSpinBox()
self.numChapters.setRange(0, 100) self.numChapters.setRange(0, 100)
self.numChapters.setValue(5) self.numChapters.setValue(5)
@@ -366,37 +352,40 @@ class ProjWizardCustomPage(QWizardPage):
self.numScenes.setRange(0, 200) self.numScenes.setRange(0, 200)
self.numScenes.setValue(5) self.numScenes.setValue(5)
self.chFolders = QSwitch() # Grid Form
self.chFolders.setChecked(True) self.addBox = QGridLayout()
self.addBox.addWidget(QLabel(self.tr("Add a folder for plot notes")), 0, 0)
self.novelForm.addWidget(QLabel(self.tr("Add chapters")), 0, 0) self.addBox.addWidget(QLabel(self.tr("Add a folder for character notes")), 1, 0)
self.novelForm.addWidget(QLabel(self.tr("Scenes (per chapter)")), 1, 0) self.addBox.addWidget(QLabel(self.tr("Add a folder for location notes")), 2, 0)
self.novelForm.addWidget(QLabel(self.tr("Add chapter folders")), 2, 0) self.addBox.addWidget(QLabel(self.tr("Add example notes to the above")), 3, 0)
self.novelForm.addWidget(self.numChapters, 0, 1, 1, 1, Qt.AlignRight) self.addBox.addWidget(QLabel(self.tr("Add chapters to the novel folder")), 4, 0)
self.novelForm.addWidget(self.numScenes, 1, 1, 1, 1, Qt.AlignRight) self.addBox.addWidget(QLabel(self.tr("Add scenes to each chapter")), 5, 0)
self.novelForm.addWidget(self.chFolders, 2, 1, 1, 1, Qt.AlignRight) self.addBox.addWidget(self.addPlot, 0, 1, 1, 1, Qt.AlignRight)
self.novelForm.setRowStretch(3, 1) self.addBox.addWidget(self.addChar, 1, 1, 1, 1, Qt.AlignRight)
self.addBox.addWidget(self.addWorld, 2, 1, 1, 1, Qt.AlignRight)
self.addBox.addWidget(self.addNotes, 3, 1, 1, 1, Qt.AlignRight)
self.addBox.addWidget(self.numChapters, 4, 1, 1, 1, Qt.AlignRight)
self.addBox.addWidget(self.numScenes, 5, 1, 1, 1, Qt.AlignRight)
self.addBox.setVerticalSpacing(fS)
self.addBox.setHorizontalSpacing(cM)
self.addBox.setContentsMargins(cM, 0, cM, 0)
self.addBox.setColumnStretch(2, 1)
for i in range(6):
self.addBox.setRowMinimumHeight(i, mH)
# Wizard Fields # Wizard Fields
self.registerField("addPlot", self.addPlot) self.registerField("addPlot", self.addPlot)
self.registerField("addChar", self.addChar) self.registerField("addChar", self.addChar)
self.registerField("addWorld", self.addWorld) self.registerField("addWorld", self.addWorld)
self.registerField("addTime", self.addTime) self.registerField("addNotes", self.addNotes)
self.registerField("addObject", self.addObject)
self.registerField("addEntity", self.addEntity)
self.registerField("numChapters", self.numChapters) self.registerField("numChapters", self.numChapters)
self.registerField("numScenes", self.numScenes) self.registerField("numScenes", self.numScenes)
self.registerField("chFolders", self.chFolders)
# Assemble # Assemble
self.innerBox = QHBoxLayout()
self.innerBox.addWidget(self.rootGroup)
self.innerBox.addWidget(self.novelGroup)
self.outerBox = QVBoxLayout() self.outerBox = QVBoxLayout()
self.outerBox.setSpacing(vS) self.outerBox.setSpacing(cM)
self.outerBox.addWidget(self.theText) self.outerBox.addWidget(self.theText)
self.outerBox.addLayout(self.innerBox) self.outerBox.addLayout(self.addBox)
self.outerBox.addStretch(1) self.outerBox.addStretch(1)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
@@ -413,15 +402,8 @@ class ProjWizardFinalPage(QWizardPage):
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.theWizard = theWizard self.theWizard = theWizard
self.setTitle(self.tr("Finished")) self.setTitle(self.tr("Summary"))
self.theText = QLabel( self.theText = QLabel("")
"<p>%s</p><p>%s</p>" % (
self.tr("All done."),
self.tr("Press '{0}' to create the new project.").format(
self.tr("Done") if self.mainConf.osDarwin else self.tr("Finish")
)
)
)
self.theText.setWordWrap(True) self.theText.setWordWrap(True)
# Assemble # Assemble
@@ -433,4 +415,51 @@ class ProjWizardFinalPage(QWizardPage):
return return
def initializePage(self):
"""Update the summary information on the final page.
"""
QWizardPage.initializePage(self)
sumList = []
sumList.append(self.tr("Project Name: {0}").format(self.field("projName")))
sumList.append(self.tr("Project Path: {0}").format(self.field("projPath")))
if self.field("popMinimal"):
sumList.append(self.tr("Fill the project with a minimal set of items"))
elif self.field("popSample"):
sumList.append(self.tr("Fill the project with example files"))
elif self.field("popCustom"):
if self.field("addPlot"):
sumList.append(self.tr("Add a folder for plot notes"))
if self.field("addChar"):
sumList.append(self.tr("Add a folder for character notes"))
if self.field("addWorld"):
sumList.append(self.tr("Add a folder for location notes"))
if self.field("addNotes"):
sumList.append(self.tr("Add example notes to the above"))
if self.field("numChapters") > 0:
sumList.append(self.tr("Add {0} chapters to the novel folder").format(
self.field("numChapters")
))
if self.field("numScenes") > 0:
sumList.append(self.tr("Add {0} scenes to each chapter").format(
self.field("numScenes")
))
else:
if self.field("numScenes") > 0:
sumList.append(self.tr("Add {0} scenes").format(
self.field("numScenes")
))
self.theText.setText(
"<p>%s</p><p>&nbsp;&bull;&nbsp;%s</p><p>%s</p>" % (
self.tr("You have selected the following:"),
"<br>&nbsp;&bull;&nbsp;".join(sumList),
self.tr("Press '{0}' to create the new project.").format(
self.tr("Done") if self.mainConf.osDarwin else self.tr("Finish")
)
)
)
return
# END Class ProjWizardFinalPage # END Class ProjWizardFinalPage
+34 -33
View File
@@ -67,33 +67,34 @@ class GuiWritingStats(QDialog):
self.theParent = theParent self.theParent = theParent
self.theTheme = theParent.theTheme self.theTheme = theParent.theTheme
self.theProject = theParent.theProject self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.logData = [] self.logData = []
self.filterData = [] self.filterData = []
self.timeFilter = 0.0 self.timeFilter = 0.0
self.wordOffset = 0 self.wordOffset = 0
pOptions = self.theProject.options
self.setWindowTitle(self.tr("Writing Statistics")) self.setWindowTitle(self.tr("Writing Statistics"))
self.setMinimumWidth(self.mainConf.pxInt(420)) self.setMinimumWidth(self.mainConf.pxInt(420))
self.setMinimumHeight(self.mainConf.pxInt(400)) self.setMinimumHeight(self.mainConf.pxInt(400))
self.resize( self.resize(
self.mainConf.pxInt(self.optState.getInt("GuiWritingStats", "winWidth", 550)), self.mainConf.pxInt(pOptions.getInt("GuiWritingStats", "winWidth", 550)),
self.mainConf.pxInt(self.optState.getInt("GuiWritingStats", "winHeight", 500)) self.mainConf.pxInt(pOptions.getInt("GuiWritingStats", "winHeight", 500))
) )
# List Box # List Box
wCol0 = self.mainConf.pxInt( wCol0 = self.mainConf.pxInt(
self.optState.getInt("GuiWritingStats", "widthCol0", 180) pOptions.getInt("GuiWritingStats", "widthCol0", 180)
) )
wCol1 = self.mainConf.pxInt( wCol1 = self.mainConf.pxInt(
self.optState.getInt("GuiWritingStats", "widthCol1", 80) pOptions.getInt("GuiWritingStats", "widthCol1", 80)
) )
wCol2 = self.mainConf.pxInt( wCol2 = self.mainConf.pxInt(
self.optState.getInt("GuiWritingStats", "widthCol2", 80) pOptions.getInt("GuiWritingStats", "widthCol2", 80)
) )
wCol3 = self.mainConf.pxInt( wCol3 = self.mainConf.pxInt(
self.optState.getInt("GuiWritingStats", "widthCol3", 80) pOptions.getInt("GuiWritingStats", "widthCol3", 80)
) )
self.listBox = QTreeWidget() self.listBox = QTreeWidget()
@@ -115,9 +116,9 @@ class GuiWritingStats(QDialog):
hHeader.setTextAlignment(self.C_IDLE, Qt.AlignRight) hHeader.setTextAlignment(self.C_IDLE, Qt.AlignRight)
hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight) hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight)
sortCol = checkIntRange(self.optState.getInt("GuiWritingStats", "sortCol", 0), 0, 2, 0) sortCol = checkIntRange(pOptions.getInt("GuiWritingStats", "sortCol", 0), 0, 2, 0)
sortOrder = checkIntTuple( sortOrder = checkIntTuple(
self.optState.getInt("GuiWritingStats", "sortOrder", Qt.DescendingOrder), pOptions.getInt("GuiWritingStats", "sortOrder", Qt.DescendingOrder),
(Qt.AscendingOrder, Qt.DescendingOrder), Qt.DescendingOrder (Qt.AscendingOrder, Qt.DescendingOrder), Qt.DescendingOrder
) )
self.listBox.sortByColumn(sortCol, sortOrder) self.listBox.sortByColumn(sortCol, sortOrder)
@@ -190,37 +191,37 @@ class GuiWritingStats(QDialog):
self.incNovel = QSwitch(width=2*sPx, height=sPx) self.incNovel = QSwitch(width=2*sPx, height=sPx)
self.incNovel.setChecked( self.incNovel.setChecked(
self.optState.getBool("GuiWritingStats", "incNovel", True) pOptions.getBool("GuiWritingStats", "incNovel", True)
) )
self.incNovel.clicked.connect(self._updateListBox) self.incNovel.clicked.connect(self._updateListBox)
self.incNotes = QSwitch(width=2*sPx, height=sPx) self.incNotes = QSwitch(width=2*sPx, height=sPx)
self.incNotes.setChecked( self.incNotes.setChecked(
self.optState.getBool("GuiWritingStats", "incNotes", True) pOptions.getBool("GuiWritingStats", "incNotes", True)
) )
self.incNotes.clicked.connect(self._updateListBox) self.incNotes.clicked.connect(self._updateListBox)
self.hideZeros = QSwitch(width=2*sPx, height=sPx) self.hideZeros = QSwitch(width=2*sPx, height=sPx)
self.hideZeros.setChecked( self.hideZeros.setChecked(
self.optState.getBool("GuiWritingStats", "hideZeros", True) pOptions.getBool("GuiWritingStats", "hideZeros", True)
) )
self.hideZeros.clicked.connect(self._updateListBox) self.hideZeros.clicked.connect(self._updateListBox)
self.hideNegative = QSwitch(width=2*sPx, height=sPx) self.hideNegative = QSwitch(width=2*sPx, height=sPx)
self.hideNegative.setChecked( self.hideNegative.setChecked(
self.optState.getBool("GuiWritingStats", "hideNegative", False) pOptions.getBool("GuiWritingStats", "hideNegative", False)
) )
self.hideNegative.clicked.connect(self._updateListBox) self.hideNegative.clicked.connect(self._updateListBox)
self.groupByDay = QSwitch(width=2*sPx, height=sPx) self.groupByDay = QSwitch(width=2*sPx, height=sPx)
self.groupByDay.setChecked( self.groupByDay.setChecked(
self.optState.getBool("GuiWritingStats", "groupByDay", False) pOptions.getBool("GuiWritingStats", "groupByDay", False)
) )
self.groupByDay.clicked.connect(self._updateListBox) self.groupByDay.clicked.connect(self._updateListBox)
self.showIdleTime = QSwitch(width=2*sPx, height=sPx) self.showIdleTime = QSwitch(width=2*sPx, height=sPx)
self.showIdleTime.setChecked( self.showIdleTime.setChecked(
self.optState.getBool("GuiWritingStats", "showIdleTime", False) pOptions.getBool("GuiWritingStats", "showIdleTime", False)
) )
self.showIdleTime.clicked.connect(self._updateListBox) self.showIdleTime.clicked.connect(self._updateListBox)
@@ -244,7 +245,7 @@ class GuiWritingStats(QDialog):
self.histMax.setMaximum(100000) self.histMax.setMaximum(100000)
self.histMax.setSingleStep(100) self.histMax.setSingleStep(100)
self.histMax.setValue( self.histMax.setValue(
self.optState.getInt("GuiWritingStats", "histMax", 2000) pOptions.getInt("GuiWritingStats", "histMax", 2000)
) )
self.histMax.valueChanged.connect(self._updateListBox) self.histMax.valueChanged.connect(self._updateListBox)
@@ -323,23 +324,23 @@ class GuiWritingStats(QDialog):
showIdleTime = self.showIdleTime.isChecked() showIdleTime = self.showIdleTime.isChecked()
histMax = self.histMax.value() histMax = self.histMax.value()
self.optState.setValue("GuiWritingStats", "winWidth", winWidth) pOptions = self.theProject.options
self.optState.setValue("GuiWritingStats", "winHeight", winHeight) pOptions.setValue("GuiWritingStats", "winWidth", winWidth)
self.optState.setValue("GuiWritingStats", "widthCol0", widthCol0) pOptions.setValue("GuiWritingStats", "winHeight", winHeight)
self.optState.setValue("GuiWritingStats", "widthCol1", widthCol1) pOptions.setValue("GuiWritingStats", "widthCol0", widthCol0)
self.optState.setValue("GuiWritingStats", "widthCol2", widthCol2) pOptions.setValue("GuiWritingStats", "widthCol1", widthCol1)
self.optState.setValue("GuiWritingStats", "widthCol3", widthCol3) pOptions.setValue("GuiWritingStats", "widthCol2", widthCol2)
self.optState.setValue("GuiWritingStats", "sortCol", sortCol) pOptions.setValue("GuiWritingStats", "widthCol3", widthCol3)
self.optState.setValue("GuiWritingStats", "sortOrder", sortOrder) pOptions.setValue("GuiWritingStats", "sortCol", sortCol)
self.optState.setValue("GuiWritingStats", "incNovel", incNovel) pOptions.setValue("GuiWritingStats", "sortOrder", sortOrder)
self.optState.setValue("GuiWritingStats", "incNotes", incNotes) pOptions.setValue("GuiWritingStats", "incNovel", incNovel)
self.optState.setValue("GuiWritingStats", "hideZeros", hideZeros) pOptions.setValue("GuiWritingStats", "incNotes", incNotes)
self.optState.setValue("GuiWritingStats", "hideNegative", hideNegative) pOptions.setValue("GuiWritingStats", "hideZeros", hideZeros)
self.optState.setValue("GuiWritingStats", "groupByDay", groupByDay) pOptions.setValue("GuiWritingStats", "hideNegative", hideNegative)
self.optState.setValue("GuiWritingStats", "showIdleTime", showIdleTime) pOptions.setValue("GuiWritingStats", "groupByDay", groupByDay)
self.optState.setValue("GuiWritingStats", "histMax", histMax) pOptions.setValue("GuiWritingStats", "showIdleTime", showIdleTime)
pOptions.setValue("GuiWritingStats", "histMax", histMax)
self.optState.saveSettings() pOptions.saveSettings()
self.close() self.close()
return return
+1
View File
@@ -4,5 +4,6 @@
# Mars # Mars
@tag: Mars @tag: Mars
@location: Space
Its red. Dusty and red. Its red. Dusty and red.
+6 -8
View File
@@ -1,19 +1,19 @@
%%~name: Making a Scene %%~name: Making a Scene
%%~path: e7ded148d6e4a/636b6aa9b697b %%~path: 6a2d6d5f4f401/636b6aa9b697b
%%~kind: NOVEL/DOCUMENT %%~kind: NOVEL/DOCUMENT
### Making a Scene ### Making a Scene
@pov: Jane @pov: Jane
@char: John @char: John, Jane
@location: Earth @location: Earth
A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree. A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree. The scene document can be sorted after the chapter document, or as a child of the chapter. Both result in the same output in the end, so it is a matter of preference.
Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but there are some known limitations. If the syntax highlighter doesnt show it correctly, the export tool will not either. Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but there are some known limitations. If the syntax highlighter doesnt show it correctly, the export tool will not either.
In addition, the editor supports automatic formatting of “quotes”, both double and single. Depending on the syntax highlighter, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.” In addition, the editor supports automatic formatting of “quotes”, both double and single. Depending on the syntax highlighter settings and colour theme, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.”
If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, lets auto-replace this A with <A>, and this C with <C>. While <E> is just <E>. Press Ctrl+R to see what this looks like in the view pane. If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, lets auto-replace this A with <A>, and this C with <C>. While <E> is just <E>. Press Ctrl+R to see what this looks like in the view pane. The list of auto-replaced text is sett in Project Settings.
The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported and can be automatically inserted when typing two hyphens. The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported and can be automatically inserted when typing two hyphens.
@@ -25,7 +25,7 @@ If you need to split a scene file up into further pieces, you can do so with the
Both scene and section titles can be left out of the final exported document. The formatting of titles can be selected from the Build Novel Project dialog. You can also have them replaced with scene separators like “* * *”. Both scene and section titles can be left out of the final exported document. The formatting of titles can be selected from the Build Novel Project dialog. You can also have them replaced with scene separators like “* * *”.
#### Text Alignment #### Text Alignment and Indentation
The text by default will have the left or justified alignment in the main text files in your project. You can also specify alignment for a specific paragraph by “pushing” it away from an edge with a set of >> or << symbols, like so: The text by default will have the left or justified alignment in the main text files in your project. You can also specify alignment for a specific paragraph by “pushing” it away from an edge with a set of >> or << symbols, like so:
@@ -35,8 +35,6 @@ This text is left-aligned. <<
>> This text is centred. << >> This text is centred. <<
#### Text Indent
You can indent a paragraph from both the left and right margin with > and < symbols. You can indent a paragraph from both the left and right margin with > and < symbols.
> This paragraph is indented from both the left margin and the right margin. This is useful for when you want to quote a large chunk of text for instance. < > This paragraph is indented from both the left margin and the right margin. This is useful for when you want to quote a large chunk of text for instance. <
+3 -3
View File
@@ -1,11 +1,11 @@
%%~name: Chapter One %%~name: Chapter One
%%~path: e7ded148d6e4a/6a2d6d5f4f401 %%~path: 7031beac91f75/6a2d6d5f4f401
%%~kind: NOVEL/DOCUMENT %%~kind: NOVEL/DOCUMENT
## So it Begins ## So it Begins
@pov: Jane @pov: Jane
@location: Earth @location: Earth
% Synopsis: We can add a chapter file, but keep the scene files separate. In the chapter file we can set the meta data that applies to the whole chapter if we wish to. % Synopsis: We can add a chapter document, but keep the scene files separate. In the chapter document we can set the meta data that applies to the whole chapter if we wish to. You can add the scenes as child documents directly under the chapter.
A chapter can also contain leading text before the first scene. A chapter can contain leading text before the first scene, like this piece of text.
+2 -1
View File
@@ -1,5 +1,5 @@
%%~name: Chapter Two %%~name: Chapter Two
%%~path: e7ded148d6e4a/88706ddc78b1b %%~path: 7031beac91f75/88706ddc78b1b
%%~kind: NOVEL/DOCUMENT %%~kind: NOVEL/DOCUMENT
## Where has John Gone? ## Where has John Gone?
@@ -11,6 +11,7 @@
### Jane Cannot Find John ### Jane Cannot Find John
@pov: Jane @pov: Jane
@focus: John
@location: Space @location: Space
Jane has been looking all over for John. Hes nowhere to be found on Earth, so Jane goes to space. Jane has been looking all over for John. Hes nowhere to be found on Earth, so Jane goes to space.
+2 -2
View File
@@ -1,6 +1,6 @@
%%~name: Old File %%~name: Old File
%%~path: ae9bf3c3ea159/8a5deb88c0e97 %%~path: ae9bf3c3ea159/8a5deb88c0e97
%%~kind: NOVEL/DOCUMENT %%~kind: ARCHIVE/DOCUMENT
### Discarded Scene ### Discarded Scene
If you have files you no longer want in your main project, you can move them to the “Archive” folder. This is equivalent to turning off the “Include when building project” switch, just that you also put the file away, although the switch can be ignored when building the project, this folder cannot. If you have files you no longer want in your main project, you can move them to the “Archive” folder. This is equivalent to turning off the “Include when building project” switch, just that you also put the file away.
+5 -3
View File
@@ -1,11 +1,11 @@
%%~name: A Note on Structure %%~name: A Note on Structure
%%~path: e7ded148d6e4a/96b68994dfa3d %%~path: 7031beac91f75/96b68994dfa3d
%%~kind: NOVEL/NOTE %%~kind: NOVEL/NOTE
# A Note on Structure # A Note on Structure
This file is just a note. You can save notes anywhere you like in the project tree. Notes can be filtered out when you export the project. This file is just a note. You can save notes anywhere you like in the project tree. Notes can be filtered out when you export the project.
In root folders that isnt the Novel root folder, you can _only_ add notes. In the Novel root folder, you can choose between a number of layouts. Some of them are just to let yourself know what each file is for. In root folders that arent the Novel or Archive root folders, you can _only_ add notes. In the Novel and Archive folder you can also add Project Documents, which are the documents that make up your actual story.
## Headers in Notes ## Headers in Notes
@@ -15,8 +15,10 @@ Unlike in novel files, headers in notes have no particular meaning other than vi
The folders in the tree view have no structural meaning other than theyre a way for you to organise your files into groups in whatever way suits you. They are not intended to represent chapters, but you can of course use them for that. If you do, know that you still need to define chapter headers in your structure so novelWriter knows where you want them. The folders in the tree view have no structural meaning other than theyre a way for you to organise your files into groups in whatever way suits you. They are not intended to represent chapters, but you can of course use them for that. If you do, know that you still need to define chapter headers in your structure so novelWriter knows where you want them.
If you do have separate chapter documents, you can always add scene documents as child document of the chapter instead of using folders.
## Linking Files and Notes ## Linking Files and Notes
You can link files and notes together by assigning tags to them, and then reference them from other files. The file class of a file determines which reference keywords apply to each file. For instance a file in the Characters root folder can be referenced using either the @char keyword or the @pov keyword. You can link project documents and notes together by assigning tags to the notes with the @tag keyword, and then reference them from other files using one of the many reference keywords. The file class of a file determines which reference keywords apply to each file. For instance a file in the Characters root folder can be referenced using either the @char keyword or the @pov keyword.
If you want to see the content of the file the reference points to, you can click Ctrl+Enter with the cursor on top of the reference, and the view pane will show you the file. In the view pane, all references are clickable, so you can navigate further. At the bottom of the view pane, a list of files referencing the one your viewing will appear. This panel updates when you navigate, unless you make it sticky by clicking the sticky checkbox. If you want to see the content of the file the reference points to, you can click Ctrl+Enter with the cursor on top of the reference, and the view pane will show you the file. In the view pane, all references are clickable, so you can navigate further. At the bottom of the view pane, a list of files referencing the one your viewing will appear. This panel updates when you navigate, unless you make it sticky by clicking the sticky checkbox.
+1 -1
View File
@@ -6,4 +6,4 @@
This is a plain page with some text on it. This is a plain page with some text on it.
If you want the text to start on a fresh page, add the [NEW PAGE] code above the text. You can also add empty paragraphs with the [VSPACE] code. If you want the text to start on a fresh page, add the [NEW PAGE] code above the text. You can also add empty paragraphs with the [VSPACE] code. The above code adds two empty paragraphs before the text starts.
+17
View File
@@ -0,0 +1,17 @@
%%~name: Chapter One
%%~path: e5e47ebf63b1c/a520879ca0b45
%%~kind: NOVEL/DOCUMENT
### Chapter One
@pov: Jane
% Synopsis: Remember Jane and John?
### Scene One
@pov: Jane
@focus: John
A project can have multiple novel root folders for multiple novels. This is the first scene of a sequel to the first novel.
In this way, the writer can keep the same notes for multiple novels. This can be especially useful if the writer is planning a multi-novel story in advance.
+2 -1
View File
@@ -1,9 +1,10 @@
%%~name: We Found John! %%~name: We Found John!
%%~path: e7ded148d6e4a/ae7339df26ded %%~path: 88706ddc78b1b/ae7339df26ded
%%~kind: NOVEL/DOCUMENT %%~kind: NOVEL/DOCUMENT
### We Found John! ### We Found John!
@pov: John @pov: John
@focus: John
@location: Mars @location: Mars
Jane has been searching for a while, and she finally found John on Mars. He was indeed in space! What was he doing on Mars anyway? Well, it turns out, he was farming potatoes. Jane has been searching for a while, and she finally found John on Mars. He was indeed in space! What was he doing on Mars anyway? Well, it turns out, he was farming potatoes.
+1
View File
@@ -4,5 +4,6 @@
# Earth # Earth
@tag: Earth @tag: Earth
@location: Space
Third planet from the sun, fairly dense, and with lots of people on it. Third planet from the sun, fairly dense, and with lots of people on it.
+1 -1
View File
@@ -1,6 +1,6 @@
%%~name: Delete Me! %%~name: Delete Me!
%%~path: 98acd8c76c93a/b8136a5a774a0 %%~path: 98acd8c76c93a/b8136a5a774a0
%%~kind: NOVEL/DOCUMENT %%~kind: TRASH/DOCUMENT
### Delete Me! ### Delete Me!
This scene is trash. This scene is trash.
+2 -2
View File
@@ -1,9 +1,9 @@
%%~name: Interlude %%~name: Interlude
%%~path: e7ded148d6e4a/ba8a28a246524 %%~path: 7031beac91f75/ba8a28a246524
%%~kind: NOVEL/DOCUMENT %%~kind: NOVEL/DOCUMENT
##! Interlude ##! Interlude
% Notice that this is a file with the flag N.Un. The N means its a novel file, and the Un means its an unnumbered chapter. Unnumbered chapters can be treated separately from numbered chapters during export. Perfect for when you want to add an interlude, or for a prologue or epilogue. % Notice that this document has a title with a ! in it in addition to the two hash symbols. This means it is an unnumbered chapter. Unnumbered chapters can be treated separately from numbered chapters during export. Perfect for when you want to add an interlude, or for a prologue or epilogue.
I am the very model of a modern Major-General I am the very model of a modern Major-General
I've information vegetable, animal, and mineral I've information vegetable, animal, and mineral
+8
View File
@@ -0,0 +1,8 @@
%%~name: Title Page
%%~path: e5e47ebf63b1c/bacb7059e3083
%%~kind: NOVEL/DOCUMENT
#! Sequel Novel
>> **By Jane Doh** <<
% Synopsis: Jane and John are back in a sequel to My Novel!
+4 -4
View File
@@ -1,5 +1,5 @@
%%~name: Another Scene %%~name: Another Scene
%%~path: e7ded148d6e4a/bc0cbd2a407f3 %%~path: 6a2d6d5f4f401/bc0cbd2a407f3
%%~kind: NOVEL/DOCUMENT %%~kind: NOVEL/DOCUMENT
### Another Scene ### Another Scene
@@ -7,9 +7,9 @@
@focus: Jane @focus: Jane
@location: Earth @location: Earth
Adding more scenes to a chapter is as easy as adding more scene files, with a level three heading, or just adding another level three heading in the same file if that works for the way you want to structure your files. Adding more scenes to a chapter is as easy as adding more scene files with a level three heading. You can of course also just add another level three heading in the same file if that works for the way you want to structure your files.
In fact, if you wish, you can add all the scenes in the chapter file too. All novelWriter cares about is the level of the headings. In fact, if you wish, you can add all the scenes in the chapter file too. All novelWriter cares about is the level of the headings and the order in which they appear.
### More Scenes ### More Scenes
@@ -17,4 +17,4 @@ In fact, if you wish, you can add all the scenes in the chapter file too. All no
@focus: John @focus: John
@location: Earth @location: Earth
This is a second scene in the same file as the previous scene. You can always split the files up later. This is a second scene in the same file as the previous scene. You can always split the files up later using the split tool.
+100 -92
View File
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-03 22:32:30"> <novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-06-05 15:03:00">
<project> <project>
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
<author>Jane Smith</author> <author>Jane Smith</author>
<author>Jay Doh</author> <author>Jay Doh</author>
<saveCount>1303</saveCount> <saveCount>1334</saveCount>
<autoCount>199</autoCount> <autoCount>225</autoCount>
<editTime>65005</editTime> <editTime>67746</editTime>
</project> </project>
<settings> <settings>
<doBackup>False</doBackup> <doBackup>False</doBackup>
@@ -15,11 +15,11 @@
<spellCheck>True</spellCheck> <spellCheck>True</spellCheck>
<spellLang>None</spellLang> <spellLang>None</spellLang>
<autoOutline>True</autoOutline> <autoOutline>True</autoOutline>
<lastEdited>636b6aa9b697b</lastEdited> <lastEdited>a520879ca0b45</lastEdited>
<lastViewed>636b6aa9b697b</lastViewed> <lastViewed>636b6aa9b697b</lastViewed>
<lastWordCount>1206</lastWordCount> <lastWordCount>1363</lastWordCount>
<novelWordCount>830</novelWordCount> <novelWordCount>954</novelWordCount>
<notesWordCount>376</notesWordCount> <notesWordCount>409</notesWordCount>
<autoReplace> <autoReplace>
<entry key="A">B</entry> <entry key="A">B</entry>
<entry key="B">E</entry> <entry key="B">E</entry>
@@ -33,121 +33,129 @@
<section></section> <section></section>
</titleFormat> </titleFormat>
<status> <status>
<entry red="100" green="100" blue="100">New</entry> <entry key="sf12341" count="7" red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Notes</entry> <entry key="sf24ce6" count="1" red="200" green="50" blue="0">Notes</entry>
<entry red="182" green="60" blue="0">Started</entry> <entry key="sc24b8f" count="2" red="182" green="60" blue="0">Started</entry>
<entry red="193" green="129" blue="0">1st Draft</entry> <entry key="s90e6c9" count="6" red="193" green="129" blue="0">1st Draft</entry>
<entry red="193" green="129" blue="0">2nd Draft</entry> <entry key="sd51c5b" count="1" red="193" green="129" blue="0">2nd Draft</entry>
<entry red="193" green="129" blue="0">3rd Draft</entry> <entry key="s8ae72a" count="0" red="193" green="129" blue="0">3rd Draft</entry>
<entry red="58" green="180" blue="58">Finished</entry> <entry key="s78ea90" count="0" red="58" green="180" blue="58">Finished</entry>
</status> </status>
<importance> <importance>
<entry red="100" green="100" blue="100">None</entry> <entry key="ia857f0" count="5" red="100" green="100" blue="100">None</entry>
<entry red="0" green="122" blue="188">Minor</entry> <entry key="icfb3a5" count="2" red="0" green="122" blue="188">Minor</entry>
<entry red="21" green="0" blue="180">Major</entry> <entry key="i2d7a54" count="2" red="21" green="0" blue="180">Major</entry>
<entry red="117" green="0" blue="175">Main</entry> <entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="25"> <content count="27">
<item handle="7031beac91f75" parent="None" order="0" type="ROOT" class="NOVEL"> <item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="True"/> <meta expanded="True"/>
<name status="Started" import="None">Novel</name> <name status="sc24b8f" import="ia857f0">Novel</name>
</item> </item>
<item handle="53b69b83cdafc" parent="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="53b69b83cdafc" parent="7031beac91f75" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="93" wordCount="19" paraCount="2" cursorPos="2"/> <meta expanded="False" charCount="93" wordCount="19" paraCount="2" cursorPos="119"/>
<name status="Started" import="None" exported="True">Title Page</name> <name status="sc24b8f" import="ia857f0" exported="True">Title Page</name>
</item> </item>
<item handle="974e400180a99" parent="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="186" wordCount="39" paraCount="2" cursorPos="212"/> <meta expanded="False" charCount="251" wordCount="50" paraCount="2" cursorPos="277"/>
<name status="New" import="None" exported="True">Page</name> <name status="sf12341" import="ia857f0" exported="True">Page</name>
</item> </item>
<item handle="edca4be2fcaf8" parent="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="edca4be2fcaf8" parent="7031beac91f75" root="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="26" wordCount="6" paraCount="1" cursorPos="33"/> <meta expanded="False" charCount="26" wordCount="6" paraCount="1" cursorPos="36"/>
<name status="New" import="None" exported="True">Part One</name> <name status="sf12341" import="ia857f0" exported="True">Part One</name>
</item> </item>
<item handle="e7ded148d6e4a" parent="7031beac91f75" order="3" type="FOLDER" class="NOVEL"> <item handle="6a2d6d5f4f401" parent="7031beac91f75" root="7031beac91f75" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="True" charCount="95" wordCount="18" paraCount="1" cursorPos="291"/>
<name status="sf24ce6" import="ia857f0" exported="True">Chapter One</name>
</item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="2687" wordCount="479" paraCount="14" cursorPos="61"/>
<name status="s90e6c9" import="ia857f0" exported="True">Making a Scene</name>
</item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="548" wordCount="108" paraCount="3" cursorPos="649"/>
<name status="s90e6c9" import="ia857f0" exported="True">Another Scene</name>
</item>
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="617" wordCount="101" paraCount="3" cursorPos="310"/>
<name status="sf12341" import="ia857f0" exported="True">Interlude</name>
</item>
<item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE">
<meta expanded="False" charCount="1909" wordCount="346" paraCount="7" cursorPos="0"/>
<name status="sd51c5b" import="ia857f0" exported="False">A Note on Structure</name>
</item>
<item handle="88706ddc78b1b" parent="7031beac91f75" root="7031beac91f75" order="6" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="True" charCount="139" wordCount="28" paraCount="1" cursorPos="343"/>
<name status="s90e6c9" import="ia857f0" exported="True">Chapter Two</name>
</item>
<item handle="ae7339df26ded" parent="88706ddc78b1b" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="189" wordCount="37" paraCount="1" cursorPos="224"/>
<name status="s90e6c9" import="ia857f0" exported="True">We Found John!</name>
</item>
<item handle="e5e47ebf63b1c" parent="None" root="e5e47ebf63b1c" order="1" type="ROOT" class="NOVEL">
<meta expanded="True"/> <meta expanded="True"/>
<name status="1st Draft" import="None">A Folder</name> <name status="sf12341" import="ia857f0">Sequel</name>
</item> </item>
<item handle="6a2d6d5f4f401" parent="e7ded148d6e4a" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="bacb7059e3083" parent="e5e47ebf63b1c" root="e5e47ebf63b1c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="75" wordCount="14" paraCount="1" cursorPos="279"/> <meta expanded="False" charCount="27" wordCount="5" paraCount="1" cursorPos="100"/>
<name status="Notes" import="None" exported="True">Chapter One</name> <name status="sf12341" import="ia857f0" exported="True">Title Page</name>
</item> </item>
<item handle="636b6aa9b697b" parent="e7ded148d6e4a" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="a520879ca0b45" parent="e5e47ebf63b1c" root="e5e47ebf63b1c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="2429" wordCount="432" paraCount="14" cursorPos="219"/> <meta expanded="False" charCount="299" wordCount="55" paraCount="2" cursorPos="104"/>
<name status="1st Draft" import="None" exported="True">Making a Scene</name> <name status="s90e6c9" import="ia857f0" exported="True">Chapter One</name>
</item> </item>
<item handle="bc0cbd2a407f3" parent="e7ded148d6e4a" order="2" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="f6622b4617424" parent="None" root="f6622b4617424" order="2" type="ROOT" class="CHARACTER">
<meta charCount="476" wordCount="93" paraCount="3" cursorPos="577"/>
<name status="1st Draft" import="None" exported="True">Another Scene</name>
</item>
<item handle="ba8a28a246524" parent="e7ded148d6e4a" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="617" wordCount="101" paraCount="3" cursorPos="4"/>
<name status="New" import="None" exported="True">Interlude</name>
</item>
<item handle="96b68994dfa3d" parent="e7ded148d6e4a" order="4" type="FILE" class="NOVEL" layout="NOTE">
<meta charCount="1692" wordCount="313" paraCount="6" cursorPos="1110"/>
<name status="2nd Draft" import="None" exported="False">A Note on Structure</name>
</item>
<item handle="88706ddc78b1b" parent="e7ded148d6e4a" order="5" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="139" wordCount="28" paraCount="1" cursorPos="343"/>
<name status="1st Draft" import="None" exported="True">Chapter Two</name>
</item>
<item handle="ae7339df26ded" parent="e7ded148d6e4a" order="6" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="189" wordCount="37" paraCount="1" cursorPos="224"/>
<name status="1st Draft" import="None" exported="True">We Found John!</name>
</item>
<item handle="f6622b4617424" parent="None" order="1" type="ROOT" class="CHARACTER">
<meta expanded="True"/> <meta expanded="True"/>
<name status="New" import="None">Characters</name> <name status="sf12341" import="ia857f0">Characters</name>
</item> </item>
<item handle="f7e2d9f330615" parent="f6622b4617424" order="0" type="FOLDER" class="CHARACTER"> <item handle="f7e2d9f330615" parent="f6622b4617424" root="f6622b4617424" order="0" type="FOLDER" class="CHARACTER">
<meta expanded="True"/> <meta expanded="True"/>
<name status="New" import="None">Main Characters</name> <name status="sf12341" import="ia857f0">Main Characters</name>
</item> </item>
<item handle="14298de4d9524" parent="f7e2d9f330615" order="0" type="FILE" class="CHARACTER" layout="NOTE"> <item handle="14298de4d9524" parent="f7e2d9f330615" root="f6622b4617424" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta charCount="49" wordCount="9" paraCount="1" cursorPos="24"/> <meta expanded="False" charCount="49" wordCount="9" paraCount="1" cursorPos="24"/>
<name status="New" import="Minor" exported="True">John Smith</name> <name status="sf12341" import="icfb3a5" exported="True">John Smith</name>
</item> </item>
<item handle="bb2c23b3c42cc" parent="f7e2d9f330615" order="1" type="FILE" class="CHARACTER" layout="NOTE"> <item handle="bb2c23b3c42cc" parent="f7e2d9f330615" root="f6622b4617424" order="1" type="FILE" class="CHARACTER" layout="NOTE">
<meta charCount="55" wordCount="9" paraCount="1" cursorPos="25"/> <meta expanded="False" charCount="55" wordCount="9" paraCount="1" cursorPos="25"/>
<name status="New" import="Major" exported="True">Jane Smith</name> <name status="sf12341" import="i2d7a54" exported="True">Jane Smith</name>
</item> </item>
<item handle="15c4492bd5107" parent="None" order="2" type="ROOT" class="WORLD"> <item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="3" type="ROOT" class="WORLD">
<meta expanded="True"/> <meta expanded="True"/>
<name status="New" import="None">Locations</name> <name status="sf12341" import="ia857f0">Locations</name>
</item> </item>
<item handle="b3e74dbc1f584" parent="15c4492bd5107" order="0" type="FILE" class="WORLD" layout="NOTE"> <item handle="b3e74dbc1f584" parent="15c4492bd5107" root="15c4492bd5107" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta charCount="76" wordCount="15" paraCount="1" cursorPos="20"/> <meta expanded="False" charCount="76" wordCount="15" paraCount="1" cursorPos="20"/>
<name status="New" import="Main" exported="True">Earth</name> <name status="sf12341" import="i56be10" exported="True">Earth</name>
</item> </item>
<item handle="f1471bef9f2ae" parent="15c4492bd5107" order="1" type="FILE" class="WORLD" layout="NOTE"> <item handle="f1471bef9f2ae" parent="15c4492bd5107" root="15c4492bd5107" order="1" type="FILE" class="WORLD" layout="NOTE">
<meta charCount="115" wordCount="24" paraCount="1" cursorPos="133"/> <meta expanded="False" charCount="115" wordCount="24" paraCount="1" cursorPos="133"/>
<name status="New" import="Minor" exported="True">Space</name> <name status="sf12341" import="icfb3a5" exported="True">Space</name>
</item> </item>
<item handle="5eaea4e8cdee8" parent="15c4492bd5107" order="2" type="FILE" class="WORLD" layout="NOTE"> <item handle="5eaea4e8cdee8" parent="15c4492bd5107" root="15c4492bd5107" order="2" type="FILE" class="WORLD" layout="NOTE">
<meta charCount="28" wordCount="6" paraCount="1" cursorPos="45"/> <meta expanded="False" charCount="28" wordCount="6" paraCount="1" cursorPos="45"/>
<name status="New" import="Major" exported="True">Mars</name> <name status="sf12341" import="i2d7a54" exported="True">Mars</name>
</item> </item>
<item handle="6827118336ac1" parent="None" order="3" type="ROOT" class="ARCHIVE"> <item handle="6827118336ac1" parent="None" root="6827118336ac1" order="4" type="ROOT" class="ARCHIVE">
<meta expanded="True"/> <meta expanded="True"/>
<name status="New" import="None">Archive</name> <name status="sf12341" import="ia857f0">Archive</name>
</item> </item>
<item handle="ae9bf3c3ea159" parent="6827118336ac1" order="0" type="FOLDER" class="ARCHIVE"> <item handle="ae9bf3c3ea159" parent="6827118336ac1" root="6827118336ac1" order="0" type="FOLDER" class="ARCHIVE">
<meta expanded="True"/> <meta expanded="True"/>
<name status="New" import="None">Scenes</name> <name status="sf12341" import="ia857f0">Scenes</name>
</item> </item>
<item handle="8a5deb88c0e97" parent="ae9bf3c3ea159" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="8a5deb88c0e97" parent="ae9bf3c3ea159" root="6827118336ac1" order="0" type="FILE" class="ARCHIVE" layout="DOCUMENT">
<meta charCount="315" wordCount="55" paraCount="1" cursorPos="322"/> <meta expanded="False" charCount="232" wordCount="42" paraCount="1" cursorPos="239"/>
<name status="1st Draft" import="None" exported="True">Old File</name> <name status="s90e6c9" import="ia857f0" exported="True">Old File</name>
</item> </item>
<item handle="98acd8c76c93a" parent="None" order="4" type="TRASH" class="TRASH"> <item handle="98acd8c76c93a" parent="None" root="98acd8c76c93a" order="5" type="ROOT" class="TRASH">
<meta expanded="True"/> <meta expanded="True"/>
<name status="New" import="None">Trash</name> <name status="sf12341" import="ia857f0">Trash</name>
</item> </item>
<item handle="b8136a5a774a0" parent="98acd8c76c93a" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="b8136a5a774a0" parent="98acd8c76c93a" root="98acd8c76c93a" order="0" type="FILE" class="TRASH" layout="DOCUMENT">
<meta charCount="30" wordCount="6" paraCount="1" cursorPos="36"/> <meta expanded="False" charCount="30" wordCount="6" paraCount="1" cursorPos="36"/>
<name status="New" import="None" exported="True">Delete Me!</name> <name status="sf12341" import="ia857f0" exported="True">Delete Me!</name>
</item> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>
+1
View File
@@ -4,5 +4,6 @@
<comment>novelWriter Project</comment> <comment>novelWriter Project</comment>
<sub-class-of type="application/xml"/> <sub-class-of type="application/xml"/>
<glob pattern="*.nwx"/> <glob pattern="*.nwx"/>
<icon name="application-x-novelwriter-project"/>
</mime-type> </mime-type>
</mime-info> </mime-info>
+20 -4
View File
@@ -180,6 +180,26 @@ def nwGUI(qtbot, monkeypatch, fncDir, fncConf):
return return
##
# Python Objects
##
@pytest.fixture(scope="function")
def mockRnd(monkeypatch):
"""Create a mock random number generator that just counts upwards
from 0. This one will generate status/importance flags and handles
in a predictable sequence.
"""
def rnd(n):
for x in range(n):
yield x
gen = rnd(1000)
monkeypatch.setattr("random.getrandbits", lambda *a: next(gen))
return
## ##
# Temp Project Folders # Temp Project Folders
## ##
@@ -248,10 +268,6 @@ def nwOldProj(tmpDir):
return return
##
# Useful Fixtures
##
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def ipsumText(): def ipsumText():
"""Return five paragraphs of Lorem Ipsum text. """Return five paragraphs of Lorem Ipsum text.
+68 -68
View File
@@ -1,12 +1,12 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-03 21:36:12"> <novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-23 15:34:13">
<project> <project>
<name>Lorem Ipsum</name> <name>Lorem Ipsum</name>
<title>Lorem Ipsum</title> <title>Lorem Ipsum</title>
<author>lipsum.com</author> <author>lipsum.com</author>
<saveCount>23</saveCount> <saveCount>26</saveCount>
<autoCount>24</autoCount> <autoCount>24</autoCount>
<editTime>1854</editTime> <editTime>1863</editTime>
</project> </project>
<settings> <settings>
<doBackup>False</doBackup> <doBackup>False</doBackup>
@@ -31,102 +31,102 @@
<section></section> <section></section>
</titleFormat> </titleFormat>
<status> <status>
<entry red="100" green="100" blue="100">New</entry> <entry key="sbaa94f" count="3" red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Note</entry> <entry key="s27bf7c" count="0" red="200" green="50" blue="0">Note</entry>
<entry red="200" green="150" blue="0">Draft</entry> <entry key="s92a87b" count="5" red="200" green="150" blue="0">Draft</entry>
<entry red="50" green="200" blue="0">Finished</entry> <entry key="sedd043" count="7" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <importance>
<entry red="100" green="100" blue="100">New</entry> <entry key="i613591" count="6" red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Minor</entry> <entry key="i560cbf" count="0" red="200" green="50" blue="0">Minor</entry>
<entry red="200" green="150" blue="0">Major</entry> <entry key="i37861c" count="0" red="200" green="150" blue="0">Major</entry>
<entry red="50" green="200" blue="0">Main</entry> <entry key="id6b1d0" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="21"> <content count="21">
<item handle="b3643d0f92e32" parent="None" order="0" type="ROOT" class="NOVEL"> <item handle="b3643d0f92e32" parent="None" root="b3643d0f92e32" order="0" type="ROOT" class="NOVEL">
<meta expanded="True"/> <meta expanded="True"/>
<name status="New" import="New">Novel</name> <name status="sbaa94f" import="i613591">Novel</name>
</item> </item>
<item handle="7a992350f3eb6" parent="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="7a992350f3eb6" parent="b3643d0f92e32" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="230" wordCount="40" paraCount="3" cursorPos="148"/> <meta expanded="False" charCount="230" wordCount="40" paraCount="3" cursorPos="148"/>
<name status="Finished" import="New" exported="True">Lorem Ipsum</name> <name status="sedd043" import="i613591" exported="True">Lorem Ipsum</name>
</item> </item>
<item handle="8c58a65414c23" parent="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="8c58a65414c23" parent="b3643d0f92e32" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="1058" wordCount="176" paraCount="2" cursorPos="43"/> <meta expanded="False" charCount="1058" wordCount="176" paraCount="2" cursorPos="43"/>
<name status="Finished" import="New" exported="True">Front Matter</name> <name status="sedd043" import="i613591" exported="True">Front Matter</name>
</item> </item>
<item handle="88d59a277361b" parent="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="88d59a277361b" parent="b3643d0f92e32" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="584" wordCount="92" paraCount="1" cursorPos="4"/> <meta expanded="False" charCount="584" wordCount="92" paraCount="1" cursorPos="4"/>
<name status="Draft" import="New" exported="True">Prologue</name> <name status="s92a87b" import="i613591" exported="True">Prologue</name>
</item> </item>
<item handle="db7e733775d4d" parent="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="db7e733775d4d" parent="b3643d0f92e32" root="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="35" wordCount="6" paraCount="1" cursorPos="42"/> <meta expanded="False" charCount="35" wordCount="6" paraCount="1" cursorPos="42"/>
<name status="New" import="New" exported="True">Act One</name> <name status="sbaa94f" import="i613591" exported="True">Act One</name>
</item> </item>
<item handle="45e6b01ca35c1" parent="b3643d0f92e32" order="4" type="FOLDER" class="NOVEL"> <item handle="45e6b01ca35c1" parent="b3643d0f92e32" root="b3643d0f92e32" order="4" type="FOLDER" class="NOVEL">
<meta expanded="True"/> <meta expanded="True"/>
<name status="Draft" import="New">Chapter One</name> <name status="s92a87b" import="i613591">Chapter One</name>
</item> </item>
<item handle="fb609cd8319dc" parent="45e6b01ca35c1" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="fb609cd8319dc" parent="45e6b01ca35c1" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="419" wordCount="67" paraCount="1" cursorPos="56"/> <meta expanded="False" charCount="419" wordCount="67" paraCount="1" cursorPos="56"/>
<name status="Draft" import="New" exported="True">Chapter One</name> <name status="s92a87b" import="i613591" exported="True">Chapter One</name>
</item> </item>
<item handle="88243afbe5ed8" parent="45e6b01ca35c1" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="88243afbe5ed8" parent="45e6b01ca35c1" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="2758" wordCount="404" paraCount="4" cursorPos="1528"/> <meta expanded="False" charCount="2758" wordCount="404" paraCount="4" cursorPos="1528"/>
<name status="Finished" import="New" exported="True">Scene One</name> <name status="sedd043" import="i613591" exported="True">Scene One</name>
</item> </item>
<item handle="f96ec11c6a3da" parent="45e6b01ca35c1" order="2" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="f96ec11c6a3da" parent="45e6b01ca35c1" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="4043" wordCount="600" paraCount="6" cursorPos="2335"/> <meta expanded="False" charCount="4043" wordCount="600" paraCount="6" cursorPos="2335"/>
<name status="Finished" import="New" exported="True">Scene Two</name> <name status="sedd043" import="i613591" exported="True">Scene Two</name>
</item> </item>
<item handle="846352075de7d" parent="b3643d0f92e32" order="5" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="846352075de7d" parent="b3643d0f92e32" root="b3643d0f92e32" order="5" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="631" wordCount="109" paraCount="3" cursorPos="376"/> <meta expanded="False" charCount="631" wordCount="109" paraCount="3" cursorPos="376"/>
<name status="New" import="New" exported="False">Interlude</name> <name status="sbaa94f" import="i613591" exported="False">Interlude</name>
</item> </item>
<item handle="6bd935d2490cd" parent="b3643d0f92e32" order="6" type="FOLDER" class="NOVEL"> <item handle="6bd935d2490cd" parent="b3643d0f92e32" root="b3643d0f92e32" order="6" type="FOLDER" class="NOVEL">
<meta expanded="True"/> <meta expanded="True"/>
<name status="Draft" import="New">Chapter Two</name> <name status="s92a87b" import="i613591">Chapter Two</name>
</item> </item>
<item handle="441420a886d82" parent="6bd935d2490cd" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="441420a886d82" parent="6bd935d2490cd" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="477" wordCount="70" paraCount="1" cursorPos="56"/> <meta expanded="False" charCount="477" wordCount="70" paraCount="1" cursorPos="56"/>
<name status="Draft" import="New" exported="True">Chapter Two</name> <name status="s92a87b" import="i613591" exported="True">Chapter Two</name>
</item> </item>
<item handle="eb103bc70c90c" parent="6bd935d2490cd" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="eb103bc70c90c" parent="6bd935d2490cd" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="3006" wordCount="439" paraCount="4" cursorPos="57"/> <meta expanded="False" charCount="3006" wordCount="439" paraCount="4" cursorPos="57"/>
<name status="Finished" import="New" exported="True">Scene Three</name> <name status="sedd043" import="i613591" exported="True">Scene Three</name>
</item> </item>
<item handle="f8c0562e50f1b" parent="6bd935d2490cd" order="2" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="f8c0562e50f1b" parent="6bd935d2490cd" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="3839" wordCount="563" paraCount="6" cursorPos="56"/> <meta expanded="False" charCount="3839" wordCount="563" paraCount="6" cursorPos="56"/>
<name status="Finished" import="New" exported="True">Scene Four</name> <name status="sedd043" import="i613591" exported="True">Scene Four</name>
</item> </item>
<item handle="47666c91c7ccf" parent="6bd935d2490cd" order="3" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="47666c91c7ccf" parent="6bd935d2490cd" root="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="3644" wordCount="543" paraCount="5" cursorPos="351"/> <meta expanded="False" charCount="3644" wordCount="543" paraCount="5" cursorPos="351"/>
<name status="Finished" import="New" exported="True">Scene Five</name> <name status="sedd043" import="i613591" exported="True">Scene Five</name>
</item> </item>
<item handle="67a8707f2f249" parent="None" order="1" type="ROOT" class="CHARACTER"> <item handle="67a8707f2f249" parent="None" root="67a8707f2f249" order="1" type="ROOT" class="CHARACTER">
<meta expanded="True"/> <meta expanded="True"/>
<name status="New" import="New">Characters</name> <name status="sbaa94f" import="i613591">Characters</name>
</item> </item>
<item handle="4c4f28287af27" parent="67a8707f2f249" order="0" type="FILE" class="CHARACTER" layout="NOTE"> <item handle="4c4f28287af27" parent="67a8707f2f249" root="67a8707f2f249" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta charCount="1864" wordCount="284" paraCount="3" cursorPos="1883"/> <meta expanded="False" charCount="1864" wordCount="284" paraCount="3" cursorPos="1883"/>
<name status="New" import="New" exported="True">Mr. Nobody</name> <name status="sbaa94f" import="i613591" exported="True">Mr. Nobody</name>
</item> </item>
<item handle="6c6afb1247750" parent="None" order="2" type="ROOT" class="PLOT"> <item handle="6c6afb1247750" parent="None" root="6c6afb1247750" order="2" type="ROOT" class="PLOT">
<meta expanded="True"/> <meta expanded="True"/>
<name status="New" import="New">Plot</name> <name status="sbaa94f" import="i613591">Plot</name>
</item> </item>
<item handle="2426c6f0ca922" parent="6c6afb1247750" order="0" type="FILE" class="PLOT" layout="NOTE"> <item handle="2426c6f0ca922" parent="6c6afb1247750" root="6c6afb1247750" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta charCount="1369" wordCount="195" paraCount="2" cursorPos="1387"/> <meta expanded="False" charCount="1369" wordCount="195" paraCount="2" cursorPos="1387"/>
<name status="New" import="New" exported="True">Main</name> <name status="sbaa94f" import="i613591" exported="True">Main</name>
</item> </item>
<item handle="60bdf227455cc" parent="None" order="3" type="ROOT" class="WORLD"> <item handle="60bdf227455cc" parent="None" root="60bdf227455cc" order="3" type="ROOT" class="WORLD">
<meta expanded="True"/> <meta expanded="True"/>
<name status="New" import="New">World</name> <name status="sbaa94f" import="i613591">World</name>
</item> </item>
<item handle="04468803b92e1" parent="60bdf227455cc" order="0" type="FILE" class="WORLD" layout="NOTE"> <item handle="04468803b92e1" parent="60bdf227455cc" root="60bdf227455cc" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta charCount="1770" wordCount="259" paraCount="3" cursorPos="1792"/> <meta expanded="False" charCount="1770" wordCount="259" paraCount="3" cursorPos="1792"/>
<name status="New" import="New" exported="True">Ancient Europe</name> <name status="sbaa94f" import="i613591" exported="True">Ancient Europe</name>
</item> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>
+30 -30
View File
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-03 21:36:18"> <novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-23 15:34:21">
<project> <project>
<name>Test Minimal</name> <name>Test Minimal</name>
<title>Minimal</title> <title>Minimal</title>
<author>Jane Doe</author> <author>Jane Doe</author>
<author>John Doh</author> <author>John Doh</author>
<saveCount>14</saveCount> <saveCount>17</saveCount>
<autoCount>2</autoCount> <autoCount>2</autoCount>
<editTime>135</editTime> <editTime>150</editTime>
</project> </project>
<settings> <settings>
<doBackup>True</doBackup> <doBackup>True</doBackup>
@@ -29,50 +29,50 @@
<section></section> <section></section>
</titleFormat> </titleFormat>
<status> <status>
<entry red="100" green="100" blue="100">New</entry> <entry key="s72322d" count="5" red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Note</entry> <entry key="sb6adc7" count="0" red="200" green="50" blue="0">Note</entry>
<entry red="200" green="150" blue="0">Draft</entry> <entry key="s2ae76d" count="0" red="200" green="150" blue="0">Draft</entry>
<entry red="50" green="200" blue="0">Finished</entry> <entry key="s541953" count="0" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <importance>
<entry red="100" green="100" blue="100">New</entry> <entry key="iffcacb" count="3" red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Minor</entry> <entry key="i6b130b" count="0" red="200" green="50" blue="0">Minor</entry>
<entry red="200" green="150" blue="0">Major</entry> <entry key="iece803" count="0" red="200" green="150" blue="0">Major</entry>
<entry red="50" green="200" blue="0">Main</entry> <entry key="i5ba06e" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="8"> <content count="8">
<item handle="a508bb932959c" parent="None" order="0" type="ROOT" class="NOVEL"> <item handle="a508bb932959c" parent="None" root="a508bb932959c" order="0" type="ROOT" class="NOVEL">
<meta expanded="True"/> <meta expanded="True"/>
<name status="New" import="New">Novel</name> <name status="s72322d" import="iffcacb">Novel</name>
</item> </item>
<item handle="a35baf2e93843" parent="a508bb932959c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="a35baf2e93843" parent="a508bb932959c" root="a508bb932959c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="28" wordCount="6" paraCount="1" cursorPos="33"/> <meta expanded="False" charCount="28" wordCount="6" paraCount="1" cursorPos="33"/>
<name status="New" import="New" exported="True">Title Page</name> <name status="s72322d" import="iffcacb" exported="True">Title Page</name>
</item> </item>
<item handle="a6d311a93600a" parent="a508bb932959c" order="1" type="FOLDER" class="NOVEL"> <item handle="a6d311a93600a" parent="a508bb932959c" root="a508bb932959c" order="1" type="FOLDER" class="NOVEL">
<meta expanded="True"/> <meta expanded="True"/>
<name status="New" import="New">New Chapter</name> <name status="s72322d" import="iffcacb">New Chapter</name>
</item> </item>
<item handle="f5ab3e30151e1" parent="a6d311a93600a" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="f5ab3e30151e1" parent="a6d311a93600a" root="a508bb932959c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="11" wordCount="2" paraCount="0" cursorPos="16"/> <meta expanded="False" charCount="11" wordCount="2" paraCount="0" cursorPos="16"/>
<name status="New" import="New" exported="True">New Chapter</name> <name status="s72322d" import="iffcacb" exported="True">New Chapter</name>
</item> </item>
<item handle="8c659a11cd429" parent="a6d311a93600a" order="1" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="8c659a11cd429" parent="a6d311a93600a" root="a508bb932959c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="9" wordCount="2" paraCount="0" cursorPos="15"/> <meta expanded="False" charCount="9" wordCount="2" paraCount="0" cursorPos="15"/>
<name status="New" import="New" exported="True">New Scene</name> <name status="s72322d" import="iffcacb" exported="True">New Scene</name>
</item> </item>
<item handle="7695ce551d265" parent="None" order="1" type="ROOT" class="PLOT"> <item handle="7695ce551d265" parent="None" root="7695ce551d265" order="1" type="ROOT" class="PLOT">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">Plot</name> <name status="s72322d" import="iffcacb">Plot</name>
</item> </item>
<item handle="afb3043c7b2b3" parent="None" order="2" type="ROOT" class="CHARACTER"> <item handle="afb3043c7b2b3" parent="None" root="afb3043c7b2b3" order="2" type="ROOT" class="CHARACTER">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">Characters</name> <name status="s72322d" import="iffcacb">Characters</name>
</item> </item>
<item handle="9d5247ab588e0" parent="None" order="3" type="ROOT" class="WORLD"> <item handle="9d5247ab588e0" parent="None" root="9d5247ab588e0" order="3" type="ROOT" class="WORLD">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">World</name> <name status="s72322d" import="iffcacb">World</name>
</item> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>
-1
View File
@@ -29,7 +29,6 @@ class MockGuiMain():
def __init__(self): def __init__(self):
self.mainConf = None self.mainConf = None
self.hasProject = True self.hasProject = True
self.theIndex = None
self.theProject = None self.theProject = None
self.statusBar = MockStatusBar() self.statusBar = MockStatusBar()
@@ -1,99 +1,125 @@
{ {
"tagIndex": { "tagsIndex": {
"Bod": [3, "4c4f28287af27", "CHARACTER", "T000001"], "Bod": {"handle": "4c4f28287af27", "heading": "T000001", "class": "CHARACTER"},
"Main": [3, "2426c6f0ca922", "PLOT", "T000001"], "Main": {"handle": "2426c6f0ca922", "heading": "T000001", "class": "PLOT"},
"Europe": [3, "04468803b92e1", "WORLD", "T000001"] "Europe": {"handle": "04468803b92e1", "heading": "T000001", "class": "WORLD"}
},
"refIndex": {
"fb609cd8319dc": {
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
}, },
"88243afbe5ed8": { "itemIndex": {
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
},
"f96ec11c6a3da": {
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
},
"441420a886d82": {
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
},
"eb103bc70c90c": {
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
},
"f8c0562e50f1b": {
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
},
"47666c91c7ccf": {
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
},
"4c4f28287af27": {
"T000001": [[4, "@plot", "Main"]]
}
},
"fileIndex": {
"7a992350f3eb6": { "7a992350f3eb6": {
"T000001": {"level": "H1", "title": "Lorem Ipsum", "layout": "DOCUMENT", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""} "level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Lorem Ipsum", "tag": "", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""}
}
}, },
"8c58a65414c23": { "8c58a65414c23": {
"T000000": {"level": "H0", "title": "", "layout": "DOCUMENT", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""} "level": "H0",
"headings": {
"T000000": {"level": "H0", "title": "", "tag": "", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""}
}
}, },
"88d59a277361b": { "88d59a277361b": {
"T000001": {"level": "H2", "title": "Prologue", "layout": "DOCUMENT", "cCount": 584, "wCount": 92, "pCount": 1, "synopsis": "Explanation from the lipsum.com website."} "level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Prologue", "tag": "", "cCount": 584, "wCount": 92, "pCount": 1, "synopsis": "Explanation from the lipsum.com website."}
}
}, },
"db7e733775d4d": { "db7e733775d4d": {
"T000001": {"level": "H1", "title": "Act One", "layout": "DOCUMENT", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""} "level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Act One", "tag": "", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""}
}
}, },
"fb609cd8319dc": { "fb609cd8319dc": {
"T000001": {"level": "H2", "title": "Chapter One", "layout": "DOCUMENT", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."} "level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Chapter One", "tag": "", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
}, },
"88243afbe5ed8": { "88243afbe5ed8": {
"T000001": {"level": "H3", "title": "Scene One", "layout": "DOCUMENT", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."}, "level": "H3",
"T000013": {"level": "H4", "title": "Scene One, Section Two", "layout": "DOCUMENT", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""} "headings": {
"T000001": {"level": "H3", "title": "Scene One", "tag": "", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."},
"T000013": {"level": "H4", "title": "Scene One, Section Two", "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
}, },
"f96ec11c6a3da": { "f96ec11c6a3da": {
"T000001": {"level": "H3", "title": "Scene Two", "layout": "DOCUMENT", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."}, "level": "H3",
"T000015": {"level": "H4", "title": "Scene Two, Section Two", "layout": "DOCUMENT", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""} "headings": {
"T000001": {"level": "H3", "title": "Scene Two", "tag": "", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."},
"T000015": {"level": "H4", "title": "Scene Two, Section Two", "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
}, },
"846352075de7d": { "846352075de7d": {
"T000001": {"level": "H2", "title": "Why do we use it?", "layout": "DOCUMENT", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""} "level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Why do we use it?", "tag": "", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""}
}
}, },
"441420a886d82": { "441420a886d82": {
"T000001": {"level": "H2", "title": "Chapter Two", "layout": "DOCUMENT", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."} "level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Chapter Two", "tag": "", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
}, },
"eb103bc70c90c": { "eb103bc70c90c": {
"T000001": {"level": "H3", "title": "Scene Three", "layout": "DOCUMENT", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."} "level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene Three", "tag": "", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
}, },
"f8c0562e50f1b": { "f8c0562e50f1b": {
"T000001": {"level": "H3", "title": "Scene Four", "layout": "DOCUMENT", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."} "level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene Four", "tag": "", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
}, },
"47666c91c7ccf": { "47666c91c7ccf": {
"T000001": {"level": "H3", "title": "Scene Five", "layout": "DOCUMENT", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."} "level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene Five", "tag": "", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
}, },
"4c4f28287af27": { "4c4f28287af27": {
"T000001": {"level": "H1", "title": "Nobody Owens", "layout": "NOTE", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""} "level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Nobody Owens", "tag": "Bod", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""}
},
"references": {
"T000001": {"Main": "@plot"}
}
}, },
"2426c6f0ca922": { "2426c6f0ca922": {
"T000001": {"level": "H1", "title": "Main Plot", "layout": "NOTE", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""} "level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Main Plot", "tag": "Main", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""}
}
}, },
"04468803b92e1": { "04468803b92e1": {
"T000001": {"level": "H1", "title": "Ancient Europe", "layout": "NOTE", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""} "level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Ancient Europe", "tag": "Europe", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""}
}
}
} }
},
"fileMeta": {
"7a992350f3eb6": ["H1", 230, 40, 3],
"8c58a65414c23": ["H0", 1058, 176, 2],
"88d59a277361b": ["H2", 584, 92, 1],
"db7e733775d4d": ["H1", 35, 6, 1],
"fb609cd8319dc": ["H2", 419, 67, 1],
"88243afbe5ed8": ["H3", 2758, 404, 4],
"f96ec11c6a3da": ["H3", 4043, 600, 6],
"846352075de7d": ["H2", 631, 109, 3],
"441420a886d82": ["H2", 477, 70, 1],
"eb103bc70c90c": ["H3", 3006, 439, 4],
"f8c0562e50f1b": ["H3", 3839, 563, 6],
"47666c91c7ccf": ["H3", 3644, 543, 5],
"4c4f28287af27": ["H1", 1864, 284, 3],
"2426c6f0ca922": ["H1", 1369, 195, 2],
"04468803b92e1": ["H1", 1770, 259, 3]
}
} }
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-03 21:38:23"> <novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-05-21 17:49:23">
<project> <project>
<name>Test Custom</name> <name>Test Custom</name>
<title>Test Novel</title> <title>Test Novel</title>
@@ -29,110 +29,106 @@
<section></section> <section></section>
</titleFormat> </titleFormat>
<status> <status>
<entry red="100" green="100" blue="100">New</entry> <entry key="s000008" count="15" red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Note</entry> <entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry red="200" green="150" blue="0">Draft</entry> <entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry red="50" green="200" blue="0">Finished</entry> <entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <importance>
<entry red="100" green="100" blue="100">New</entry> <entry key="i00000c" count="7" red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Minor</entry> <entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry red="200" green="150" blue="0">Major</entry> <entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry red="50" green="200" blue="0">Main</entry> <entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="23"> <content count="22">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL"> <item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Novel</name> <name status="s000008" import="i00000c">Novel</name>
</item> </item>
<item handle="44cb730c42048" parent="None" order="0" type="ROOT" class="PLOT"> <item handle="0000000000011" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Title Page</name>
</item>
<item handle="0000000000012" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Chapter 1</name>
</item>
<item handle="0000000000013" parent="0000000000012" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 1.1</name>
</item>
<item handle="0000000000014" parent="0000000000012" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 1.2</name>
</item>
<item handle="0000000000015" parent="0000000000012" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 1.3</name>
</item>
<item handle="0000000000016" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Chapter 2</name>
</item>
<item handle="0000000000017" parent="0000000000016" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 2.1</name>
</item>
<item handle="0000000000018" parent="0000000000016" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 2.2</name>
</item>
<item handle="0000000000019" parent="0000000000016" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 2.3</name>
</item>
<item handle="000000000001a" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Chapter 3</name>
</item>
<item handle="000000000001b" parent="000000000001a" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 3.1</name>
</item>
<item handle="000000000001c" parent="000000000001a" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 3.2</name>
</item>
<item handle="000000000001d" parent="000000000001a" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 3.3</name>
</item>
<item handle="000000000001e" parent="None" root="000000000001e" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Plot</name> <name status="s000008" import="i00000c">Plot</name>
</item> </item>
<item handle="71ee45a3c0db9" parent="None" order="0" type="ROOT" class="CHARACTER"> <item handle="000000000001f" parent="000000000001e" root="000000000001e" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Main Plot</name>
</item>
<item handle="0000000000020" parent="None" root="0000000000020" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Characters</name> <name status="s000008" import="i00000c">Characters</name>
</item> </item>
<item handle="811786ad1ae74" parent="None" order="0" type="ROOT" class="WORLD"> <item handle="0000000000021" parent="0000000000020" root="0000000000020" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Protagonist</name>
</item>
<item handle="0000000000022" parent="None" root="0000000000022" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Locations</name> <name status="s000008" import="i00000c">Locations</name>
</item> </item>
<item handle="25fc0e7096fc6" parent="None" order="0" type="ROOT" class="TIMELINE"> <item handle="0000000000023" parent="0000000000022" root="0000000000022" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Main Location</name>
</item>
<item handle="0000000000024" parent="None" root="0000000000024" order="0" type="ROOT" class="ARCHIVE">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Timeline</name> <name status="s000008" import="i00000c">Archive</name>
</item> </item>
<item handle="31489056e0916" parent="None" order="0" type="ROOT" class="OBJECT"> <item handle="0000000000025" parent="None" root="0000000000025" order="0" type="ROOT" class="TRASH">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Objects</name> <name status="s000008" import="i00000c">Trash</name>
</item>
<item handle="98010bd9270f9" parent="None" order="0" type="ROOT" class="ENTITY">
<meta expanded="False"/>
<name status="New" import="None">Entities</name>
</item>
<item handle="0e17daca5f3e1" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Title Page</name>
</item>
<item handle="1a6562590ef19" parent="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="New" import="None">Chapter 1</name>
</item>
<item handle="031b4af5197ec" parent="1a6562590ef19" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Chapter 1</name>
</item>
<item handle="41cfc0d1f2d12" parent="1a6562590ef19" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Scene 1.1</name>
</item>
<item handle="2858dcd1057d3" parent="1a6562590ef19" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Scene 1.2</name>
</item>
<item handle="2fca346db6561" parent="1a6562590ef19" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Scene 1.3</name>
</item>
<item handle="02d20bbd7e394" parent="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="New" import="None">Chapter 2</name>
</item>
<item handle="7688b6ef52555" parent="02d20bbd7e394" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Chapter 2</name>
</item>
<item handle="c837649cce43f" parent="02d20bbd7e394" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Scene 2.1</name>
</item>
<item handle="6208ef0f7750c" parent="02d20bbd7e394" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Scene 2.2</name>
</item>
<item handle="3e1e967e9b793" parent="02d20bbd7e394" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Scene 2.3</name>
</item>
<item handle="39fa9ec190eee" parent="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/>
<name status="New" import="None">Chapter 3</name>
</item>
<item handle="d029fa3a95e17" parent="39fa9ec190eee" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Chapter 3</name>
</item>
<item handle="81b8a03f97e87" parent="39fa9ec190eee" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Scene 3.1</name>
</item>
<item handle="da4ea2a5506f2" parent="39fa9ec190eee" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Scene 3.2</name>
</item>
<item handle="a68b412c42825" parent="39fa9ec190eee" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Scene 3.3</name>
</item> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-03 21:38:23"> <novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-05-21 17:48:40">
<project> <project>
<name>Test Custom</name> <name>Test Custom</name>
<title>Test Novel</title> <title>Test Novel</title>
@@ -29,74 +29,82 @@
<section></section> <section></section>
</titleFormat> </titleFormat>
<status> <status>
<entry red="100" green="100" blue="100">New</entry> <entry key="s000008" count="9" red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Note</entry> <entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry red="200" green="150" blue="0">Draft</entry> <entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry red="50" green="200" blue="0">Finished</entry> <entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <importance>
<entry red="100" green="100" blue="100">New</entry> <entry key="i00000c" count="7" red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Minor</entry> <entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry red="200" green="150" blue="0">Major</entry> <entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry red="50" green="200" blue="0">Main</entry> <entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="14"> <content count="16">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL"> <item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Novel</name> <name status="s000008" import="i00000c">Novel</name>
</item> </item>
<item handle="44cb730c42048" parent="None" order="0" type="ROOT" class="PLOT"> <item handle="0000000000011" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Title Page</name>
</item>
<item handle="0000000000012" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 1</name>
</item>
<item handle="0000000000013" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 2</name>
</item>
<item handle="0000000000014" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 3</name>
</item>
<item handle="0000000000015" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 4</name>
</item>
<item handle="0000000000016" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 5</name>
</item>
<item handle="0000000000017" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Scene 6</name>
</item>
<item handle="0000000000018" parent="None" root="0000000000018" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Plot</name> <name status="s000008" import="i00000c">Plot</name>
</item> </item>
<item handle="71ee45a3c0db9" parent="None" order="0" type="ROOT" class="CHARACTER"> <item handle="0000000000019" parent="0000000000018" root="0000000000018" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Main Plot</name>
</item>
<item handle="000000000001a" parent="None" root="000000000001a" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Characters</name> <name status="s000008" import="i00000c">Characters</name>
</item> </item>
<item handle="811786ad1ae74" parent="None" order="0" type="ROOT" class="WORLD"> <item handle="000000000001b" parent="000000000001a" root="000000000001a" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Protagonist</name>
</item>
<item handle="000000000001c" parent="None" root="000000000001c" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Locations</name> <name status="s000008" import="i00000c">Locations</name>
</item> </item>
<item handle="25fc0e7096fc6" parent="None" order="0" type="ROOT" class="TIMELINE"> <item handle="000000000001d" parent="000000000001c" root="000000000001c" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Main Location</name>
</item>
<item handle="000000000001e" parent="None" root="000000000001e" order="0" type="ROOT" class="ARCHIVE">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Timeline</name> <name status="s000008" import="i00000c">Archive</name>
</item> </item>
<item handle="31489056e0916" parent="None" order="0" type="ROOT" class="OBJECT"> <item handle="000000000001f" parent="None" root="000000000001f" order="0" type="ROOT" class="TRASH">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Objects</name> <name status="s000008" import="i00000c">Trash</name>
</item>
<item handle="98010bd9270f9" parent="None" order="0" type="ROOT" class="ENTITY">
<meta expanded="False"/>
<name status="New" import="None">Entities</name>
</item>
<item handle="0e17daca5f3e1" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Title Page</name>
</item>
<item handle="1a6562590ef19" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Scene 1</name>
</item>
<item handle="031b4af5197ec" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Scene 2</name>
</item>
<item handle="41cfc0d1f2d12" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Scene 3</name>
</item>
<item handle="2858dcd1057d3" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Scene 4</name>
</item>
<item handle="2fca346db6561" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Scene 5</name>
</item>
<item handle="02d20bbd7e394" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Scene 6</name>
</item> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>
@@ -1,8 +1,9 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-03 21:38:23"> <novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-05-21 17:08:21">
<project> <project>
<name>New Project</name> <name>New Project</name>
<title></title> <title>New Novel</title>
<author>Jane Doe</author>
<saveCount>2</saveCount> <saveCount>2</saveCount>
<autoCount>1</autoCount> <autoCount>1</autoCount>
<editTime>0</editTime> <editTime>0</editTime>
@@ -27,58 +28,58 @@
<section></section> <section></section>
</titleFormat> </titleFormat>
<status> <status>
<entry red="100" green="100" blue="100">New</entry> <entry key="s000008" count="5" red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Note</entry> <entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry red="200" green="150" blue="0">Draft</entry> <entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry red="50" green="200" blue="0">Finished</entry> <entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <importance>
<entry red="100" green="100" blue="100">New</entry> <entry key="i00000c" count="3" red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Minor</entry> <entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry red="200" green="150" blue="0">Major</entry> <entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry red="50" green="200" blue="0">Main</entry> <entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="10"> <content count="10">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL"> <item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">Novel</name> <name status="s000008" import="i00000c">Novel</name>
</item> </item>
<item handle="44cb730c42048" parent="None" order="0" type="ROOT" class="PLOT"> <item handle="0000000000011" parent="None" root="0000000000011" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">Plot</name> <name status="s000008" import="i00000c">Plot</name>
</item> </item>
<item handle="71ee45a3c0db9" parent="None" order="0" type="ROOT" class="CHARACTER"> <item handle="0000000000012" parent="None" root="0000000000012" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">Characters</name> <name status="s000008" import="i00000c">Characters</name>
</item> </item>
<item handle="811786ad1ae74" parent="None" order="0" type="ROOT" class="WORLD"> <item handle="0000000000013" parent="None" root="0000000000013" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">World</name> <name status="s000008" import="i00000c">World</name>
</item> </item>
<item handle="25fc0e7096fc6" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="0000000000014" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/> <meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="New" exported="True">Title Page</name> <name status="s000008" import="i00000c" exported="True">Title Page</name>
</item> </item>
<item handle="31489056e0916" parent="73475cb40a568" order="0" type="FOLDER" class="NOVEL"> <item handle="0000000000015" parent="0000000000010" root="0000000000010" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">New Chapter</name> <name status="s000008" import="i00000c">New Chapter</name>
</item> </item>
<item handle="98010bd9270f9" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="0000000000016" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/> <meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="New" exported="True">New Chapter</name> <name status="s000008" import="i00000c" exported="True">New Chapter</name>
</item> </item>
<item handle="0e17daca5f3e1" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="0000000000017" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/> <meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="New" exported="True">New Scene</name> <name status="s000008" import="i00000c" exported="True">New Scene</name>
</item> </item>
<item handle="1a6562590ef19" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="0000000000028" parent="31489056e0916" root="None" order="0" type="FILE" class="NO_CLASS" layout="NO_LAYOUT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/> <meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Hello</name> <name status="None" import="None" exported="True">Hello</name>
</item> </item>
<item handle="031b4af5197ec" parent="71ee45a3c0db9" order="0" type="FILE" class="CHARACTER" layout="NOTE"> <item handle="0000000000029" parent="71ee45a3c0db9" root="None" order="0" type="FILE" class="NO_CLASS" layout="NO_LAYOUT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/> <meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Jane</name> <name status="None" import="None" exported="True">Jane</name>
</item> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>
@@ -1,9 +1,9 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-03 21:54:40"> <novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-05-21 17:50:26">
<project> <project>
<name>New Project</name> <name>New Project</name>
<title></title> <title></title>
<saveCount>1</saveCount> <saveCount>2</saveCount>
<autoCount>1</autoCount> <autoCount>1</autoCount>
<editTime>0</editTime> <editTime>0</editTime>
</project> </project>
@@ -27,50 +27,50 @@
<section></section> <section></section>
</titleFormat> </titleFormat>
<status> <status>
<entry red="100" green="100" blue="100">New</entry> <entry key="s000008" count="5" red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Note</entry> <entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry red="200" green="150" blue="0">Draft</entry> <entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry red="50" green="200" blue="0">Finished</entry> <entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <importance>
<entry red="100" green="100" blue="100">New</entry> <entry key="i00000c" count="3" red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Minor</entry> <entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry red="200" green="150" blue="0">Major</entry> <entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry red="50" green="200" blue="0">Main</entry> <entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="8"> <content count="8">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL"> <item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">Novel</name> <name status="s000008" import="i00000c">Novel</name>
</item> </item>
<item handle="44cb730c42048" parent="None" order="0" type="ROOT" class="PLOT"> <item handle="0000000000011" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Title Page</name>
</item>
<item handle="0000000000012" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Chapter</name>
</item>
<item handle="0000000000013" parent="0000000000012" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Scene</name>
</item>
<item handle="0000000000014" parent="None" root="0000000000014" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">Plot</name> <name status="s000008" import="i00000c">Plot</name>
</item> </item>
<item handle="71ee45a3c0db9" parent="None" order="0" type="ROOT" class="CHARACTER"> <item handle="0000000000015" parent="None" root="0000000000015" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">Characters</name> <name status="s000008" import="i00000c">Characters</name>
</item> </item>
<item handle="811786ad1ae74" parent="None" order="0" type="ROOT" class="WORLD"> <item handle="0000000000016" parent="None" root="0000000000016" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">World</name> <name status="s000008" import="i00000c">Locations</name>
</item> </item>
<item handle="25fc0e7096fc6" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="0000000000017" parent="None" root="0000000000017" order="0" type="ROOT" class="ARCHIVE">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="New" exported="True">Title Page</name>
</item>
<item handle="31489056e0916" parent="73475cb40a568" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">New Chapter</name> <name status="s000008" import="i00000c">Archive</name>
</item>
<item handle="98010bd9270f9" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="New" exported="True">New Chapter</name>
</item>
<item handle="0e17daca5f3e1" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="New" exported="True">New Scene</name>
</item> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>
@@ -1,8 +1,9 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-03 21:38:23"> <novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-05-21 22:07:28">
<project> <project>
<name>New Project</name> <name>New Project</name>
<title></title> <title>New Novel</title>
<author>Jane Doe</author>
<saveCount>2</saveCount> <saveCount>2</saveCount>
<autoCount>1</autoCount> <autoCount>1</autoCount>
<editTime>0</editTime> <editTime>0</editTime>
@@ -27,66 +28,82 @@
<section></section> <section></section>
</titleFormat> </titleFormat>
<status> <status>
<entry red="100" green="100" blue="100">New</entry> <entry key="s000008" count="6" red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Note</entry> <entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry red="200" green="150" blue="0">Draft</entry> <entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry red="50" green="200" blue="0">Finished</entry> <entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <importance>
<entry red="100" green="100" blue="100">New</entry> <entry key="i00000c" count="10" red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Minor</entry> <entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry red="200" green="150" blue="0">Major</entry> <entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry red="50" green="200" blue="0">Main</entry> <entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </settings>
<content count="12"> <content count="16">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL"> <item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">Novel</name> <name status="s000008" import="i00000c">Novel</name>
</item> </item>
<item handle="44cb730c42048" parent="None" order="0" type="ROOT" class="PLOT"> <item handle="0000000000011" parent="None" root="0000000000011" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">Plot</name> <name status="s000008" import="i00000c">Plot</name>
</item> </item>
<item handle="71ee45a3c0db9" parent="None" order="0" type="ROOT" class="CHARACTER"> <item handle="0000000000012" parent="None" root="0000000000012" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">Characters</name> <name status="s000008" import="i00000c">Characters</name>
</item> </item>
<item handle="811786ad1ae74" parent="None" order="0" type="ROOT" class="WORLD"> <item handle="0000000000013" parent="None" root="0000000000013" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">World</name> <name status="s000008" import="i00000c">World</name>
</item> </item>
<item handle="25fc0e7096fc6" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="0000000000014" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/> <meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="New" exported="True">Title Page</name> <name status="s000008" import="i00000c" exported="True">Title Page</name>
</item> </item>
<item handle="31489056e0916" parent="73475cb40a568" order="0" type="FOLDER" class="NOVEL"> <item handle="0000000000015" parent="0000000000010" root="0000000000010" order="0" type="FOLDER" class="NOVEL">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="New">New Chapter</name> <name status="s000008" import="i00000c">New Chapter</name>
</item> </item>
<item handle="98010bd9270f9" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="0000000000016" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/> <meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="New" exported="True">New Chapter</name> <name status="s000008" import="i00000c" exported="True">New Chapter</name>
</item> </item>
<item handle="0e17daca5f3e1" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT"> <item handle="0000000000017" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/> <meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="New" exported="True">New Scene</name> <name status="s000008" import="i00000c" exported="True">New Scene</name>
</item> </item>
<item handle="1a6562590ef19" parent="None" order="0" type="ROOT" class="TIMELINE"> <item handle="0000000000028" parent="None" root="0000000000028" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Timeline</name> <name status="s000008" import="i00000c">Novel</name>
</item> </item>
<item handle="031b4af5197ec" parent="None" order="0" type="ROOT" class="OBJECT"> <item handle="0000000000029" parent="None" root="0000000000029" order="0" type="ROOT" class="PLOT">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Object</name> <name status="s000008" import="i00000c">Plot</name>
</item> </item>
<item handle="41cfc0d1f2d12" parent="None" order="0" type="ROOT" class="CUSTOM"> <item handle="000000000002a" parent="None" root="000000000002a" order="0" type="ROOT" class="CHARACTER">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Custom1</name> <name status="s000008" import="i00000c">Characters</name>
</item> </item>
<item handle="2858dcd1057d3" parent="None" order="0" type="ROOT" class="CUSTOM"> <item handle="000000000002b" parent="None" root="000000000002b" order="0" type="ROOT" class="WORLD">
<meta expanded="False"/> <meta expanded="False"/>
<name status="New" import="None">Custom2</name> <name status="s000008" import="i00000c">Locations</name>
</item>
<item handle="000000000002c" parent="None" root="000000000002c" order="0" type="ROOT" class="TIMELINE">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Timeline</name>
</item>
<item handle="000000000002d" parent="None" root="000000000002d" order="0" type="ROOT" class="OBJECT">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Objects</name>
</item>
<item handle="000000000002e" parent="None" root="000000000002e" order="0" type="ROOT" class="CUSTOM">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Custom</name>
</item>
<item handle="000000000002f" parent="None" root="000000000002f" order="0" type="ROOT" class="CUSTOM">
<meta expanded="False"/>
<name status="s000008" import="i00000c">Custom</name>
</item> </item>
</content> </content>
</novelWriterXML> </novelWriterXML>
@@ -1,5 +1,5 @@
%%~name: New Scene %%~name: New Scene
%%~path: 31489056e0916/0e17daca5f3e1 %%~path: 000000000000d/000000000000f
%%~kind: NOVEL/DOCUMENT %%~kind: NOVEL/DOCUMENT
# Novel # Novel
@@ -1,5 +1,5 @@
%%~name: New File %%~name: New Note
%%~path: 71ee45a3c0db9/1a6562590ef19 %%~path: 000000000000a/0000000000020
%%~kind: CHARACTER/NOTE %%~kind: CHARACTER/NOTE
# Jane Doe # Jane Doe
@@ -1,5 +1,5 @@
%%~name: New File %%~name: New Note
%%~path: 44cb730c42048/031b4af5197ec %%~path: 0000000000009/0000000000021
%%~kind: PLOT/NOTE %%~kind: PLOT/NOTE
# Main Plot # Main Plot
@@ -1,5 +1,5 @@
%%~name: New File %%~name: New Note
%%~path: 811786ad1ae74/41cfc0d1f2d12 %%~path: 000000000000b/0000000000022
%%~kind: WORLD/NOTE %%~kind: WORLD/NOTE
# Main Location # Main Location

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