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
## 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]
### Release Notes
+6 -4
View File
@@ -11,13 +11,15 @@
## Translations
The default language is English (UK) with English (US) as an option.
* Dutch: Martijn van der Kleijn (@mvdkleijn)
* French: Jan Lüdke (@jyhelle)
* German: Myian (@heymyian)
* Latin American Spanish: Tommy Marplatt (@tmarplatt)
* Norwegian: Veronica Berglyd Olsen (@vkbo)
* Portuguese: Bruno Meneguello (@bkmeneguello)
* Simplified Chinese: Qianzhi Long (@longqzh)
* Latin American Spanish: Tommy Marplatt (@tmarplatt)
* Dutch: Martijn van der Kleijn (@mvdkleijn)
* German: Myian (@heymyian)
## 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:
* 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)
* Owl syntax themes by Sarah Drasner (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
[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
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
* Documentation: https://novelwriter.readthedocs.io
* Internationalisation: https://crowdin.com/project/novelwriter
* Source Code: https://github.com/vkbo/novelWriter
* Source Releases: https://github.com/vkbo/novelWriter/releases
* Issue Tracker: https://github.com/vkbo/novelWriter/issues
* Feature Discussions: https://github.com/vkbo/novelWriter/discussions
* PyPi Project: https://pypi.org/project/novelWriter
* Social Media: https://fosstodon.org/@novelwriter
.. toctree::
: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:`Z`", "Undo latest changes."
":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:`Down`", "Move item one step down in the project tree."
":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:`F8`", "Activate :guilabel:`Focus Mode`, hiding the project tree and document viewer."
":kbd:`F9`", "Re-build the project index."
":kbd:`F10`", "Re-build the project outline."
":kbd:`F11`", "Activate full screen mode."
":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."
+17
View File
@@ -2,6 +2,7 @@
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).
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
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.
# 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
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"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__version__ = "1.7-alpha0"
__hexversion__ = "0x010700a0"
__date__ = "2022-02-20"
__version__ = "1.7-beta1"
__hexversion__ = "0x010700b1"
__date__ = "2022-05-17"
__status__ = "Stable"
__domain__ = "novelwriter.io"
__url__ = "https://novelwriter.io"
+100 -100
View File
@@ -2,104 +2,104 @@
"Synopsis": "Synopsis",
"Comment": "Opmerking",
"Notes": "Notities",
"0": "Nul",
"1": "Één",
"2": "Twee",
"3": "Drie",
"4": "Vier",
"5": "Vijf",
"6": "Zes",
"7": "Zeven",
"8": "Acht",
"9": "Negen",
"10": "Tien",
"11": "Elf",
"12": "Twaalf",
"13": "Dertien",
"14": "Veertien",
"15": "Vijftien",
"16": "Zestien",
"17": "Zeventien",
"18": "Achttien",
"19": "Negentien",
"20": "Twintig",
"21": "Eenentwintig",
"22": "Tweeentwintig",
"23": "Drieentwintig",
"24": "Vierentwintig",
"25": "Vijfentwintig",
"26": "Zesentwintig",
"27": "Zevenentwintig",
"28": "Achtentwintig",
"29": "Negenentwintig",
"30": "Dertig",
"31": "Eenendertig",
"32": "Tweeendertig",
"33": "Drieendertig",
"34": "Vierendertig",
"35": "Vijfendertig",
"36": "Zesendertig",
"37": "Zevenendertig",
"38": "Achtendertig",
"39": "Negenendertig",
"40": "Veertig",
"41": "Eenenveertig",
"42": "Tweeënveertig",
"43": "Drieenveertig",
"44": "Vierenveertig",
"45": "Vijfenveertig",
"46": "Zesenveertig",
"47": "Zevenenveertig",
"48": "Achtenveertig",
"49": "Negenenveertig",
"50": "Vijftig",
"51": "Eenenvijftig",
"52": "Tweeenvijftig",
"53": "Drieenvijftig",
"54": "Vierenvijftig",
"55": "Vijfenvijftig",
"56": "Zesenvijftig",
"57": "Zevenenvijftig",
"58": "Achtenvijftig",
"59": "Negenenvijftig",
"60": "Zestig",
"61": "Eenenzestig",
"62": "Tweeenzestig",
"63": "Drieenzestig",
"64": "Vierenzestig",
"65": "Vijfenzestig",
"66": "Zesenzestig",
"67": "Zevenenzestig",
"68": "Achtenzestig",
"69": "Negenenzestig",
"70": "Zeventig",
"71": "Eenenzeventig",
"72": "Tweeenzeventig",
"73": "Drieenzeventig",
"74": "Vierenzeventig",
"75": "Vijfenzeventig",
"76": "Zesenzeventig",
"77": "Zevenenzeventig",
"78": "Achtenzeventig",
"79": "Negenenzeventig",
"80": "Tachtig",
"81": "Eenentachtig",
"82": "Tweeentachtig",
"83": "Drieentachtig",
"84": "Vierentachtig",
"85": "Vijfentachtig",
"86": "Zesentachtig",
"87": "Zevenentachtig",
"88": "Achtentachtig",
"89": "Negenentachtig",
"90": "Negentig",
"91": "Eenennegentig",
"92": "Tweeennegentig",
"93": "Drieennegentig",
"94": "Vierennegentig",
"95": "Vijfennegentig",
"96": "Zesennegentig",
"97": "Zevenennegentig",
"98": "Achtennegentig",
"99": "Negenennegentig"
"0": "nul",
"1": "één",
"2": "twee",
"3": "drie",
"4": "vier",
"5": "vijf",
"6": "zes",
"7": "zeven",
"8": "acht",
"9": "negen",
"10": "tien",
"11": "elf",
"12": "twaalf",
"13": "dertien",
"14": "veertien",
"15": "vijftien",
"16": "zestien",
"17": "zeventien",
"18": "achttien",
"19": "negentien",
"20": "twintig",
"21": "eenentwintig",
"22": "tweeëntwintig",
"23": "drieëntwintig",
"24": "vierentwintig",
"25": "vijfentwintig",
"26": "zesentwintig",
"27": "zevenentwintig",
"28": "achtentwintig",
"29": "negenentwintig",
"30": "dertig",
"31": "eenendertig",
"32": "tweeëndertig",
"33": "drieëndertig",
"34": "vierendertig",
"35": "vijfendertig",
"36": "zesendertig",
"37": "zevenendertig",
"38": "achtendertig",
"39": "negenendertig",
"40": "veertig",
"41": "eenenveertig",
"42": "tweeënveertig",
"43": "drieënveertig",
"44": "vierenveertig",
"45": "vijfenveertig",
"46": "zesenveertig",
"47": "zevenenveertig",
"48": "achtenveertig",
"49": "negenenveertig",
"50": "vijftig",
"51": "eenenvijftig",
"52": "tweeënvijftig",
"53": "drieënvijftig",
"54": "vierenvijftig",
"55": "vijfenvijftig",
"56": "zesenvijftig",
"57": "zevenenvijftig",
"58": "achtenvijftig",
"59": "negenenvijftig",
"60": "zestig",
"61": "eenenzestig",
"62": "tweeënzestig",
"63": "drieënzestig",
"64": "vierenzestig",
"65": "vijfenzestig",
"66": "zesenzestig",
"67": "zevenenzestig",
"68": "achtenzestig",
"69": "negenenzestig",
"70": "zeventig",
"71": "eenenzeventig",
"72": "tweeënzeventig",
"73": "drieënzeventig",
"74": "vierenzeventig",
"75": "vijfenzeventig",
"76": "zesenzeventig",
"77": "zevenenzeventig",
"78": "achtenzeventig",
"79": "negenenzeventig",
"80": "tachtig",
"81": "eenentachtig",
"82": "tweeëntachtig",
"83": "drieëntachtig",
"84": "vierentachtig",
"85": "vijfentachtig",
"86": "zesentachtig",
"87": "zevenentachtig",
"88": "achtentachtig",
"89": "negenentachtig",
"90": "negentig",
"91": "eenennegentig",
"92": "tweeënnegentig",
"93": "drieënnegentig",
"94": "vierennegentig",
"95": "vijfennegentig",
"96": "zesennegentig",
"97": "zevenennegentig",
"98": "achtennegentig",
"99": "negenennegentig"
}
@@ -42,16 +42,20 @@ doc_h2 = mixed_heading2.svg
doc_h3 = mixed_heading3.svg
doc_h4 = mixed_heading4.svg
done = typ_input-checked.svg
down = typ_chevron-down.svg
edit = typ_pencil.svg
forward = typ_chevron-right.svg
hash = typ_hash.svg
maximise = typ_arrow-maximise.svg
menu = typ_th-menu.svg
minimise = typ_arrow-minimise.svg
proj_chapter = mixed_document-chapter.svg
proj_details = typ_th-list-grey.svg
proj_document = typ_document-text.svg
proj_folder = typ_folder.svg
proj_note = mixed_document-note.svg
proj_scene = mixed_document-scene.svg
proj_stats = typ_chart-bar-grey.svg
proj_title = mixed_document-title.svg
reference = typ_at.svg
refresh = typ_refresh.svg
@@ -74,3 +78,8 @@ status_stats = typ_chart-bar-grey.svg
status_time = typ_stopwatch-grey.svg
sticky-off = typ_pin-outline.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_h4 = mixed_heading4.svg
done = typ_input-checked.svg
down = typ_chevron-down.svg
edit = typ_pencil.svg
forward = typ_chevron-right.svg
hash = typ_hash.svg
maximise = typ_arrow-maximise.svg
menu = typ_th-menu.svg
minimise = typ_arrow-minimise.svg
proj_chapter = mixed_document-chapter.svg
proj_details = typ_th-list-grey.svg
proj_document = typ_document-text.svg
proj_folder = typ_folder.svg
proj_note = mixed_document-note.svg
proj_scene = mixed_document-scene.svg
proj_stats = typ_chart-bar-grey.svg
proj_title = mixed_document-title.svg
reference = typ_at.svg
refresh = typ_refresh.svg
@@ -74,3 +78,8 @@ status_stats = typ_chart-bar-grey.svg
status_time = typ_stopwatch-grey.svg
sticky-off = typ_pin-outline.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>
<body>
<h2>Release Notes for 1.6</h2>
<p><i>Released on 20 February 2022</i></p>
<h2>Release Notes for 1.7 Beta 1</h2>
<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
improvements and tweaks based on user requests. There are also a number of changes under the hood
to improve the structure and performance of novelWriter.</p>
<p><u>Some key improvements to the user interface are:</u></p>
<p>&#10003; The max text width setting in Preferences now also applies to the document viewer, and
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>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.</p>
<p>Please check the changelog for an overview of changes. The full release notes will be added to
the final release.</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>
</html>
+13
View File
@@ -184,6 +184,12 @@ def checkIntRange(value, first, last, 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):
"""Check that an int is an element of a tuple. If it isn't, return
the default value.
@@ -245,6 +251,13 @@ def formatTime(tS):
# 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):
"""Split a version string on the form aa.bb.cc into major, minor
and patch, and computes an integer value aabbcc.
+4 -4
View File
@@ -978,12 +978,12 @@ class Config:
"""
try:
import enchant # noqa: F401
self.hasEnchant = True
logger.debug("Checking package 'pyenchant': OK")
except Exception:
except ImportError:
self.hasEnchant = False
logger.debug("Checking package 'pyenchant': Missing")
else:
self.hasEnchant = True
logger.debug("Checking package 'pyenchant': OK")
return
# 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 novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwOutline
from novelwriter.enum import nwItemClass, nwItemLayout, nwOutline
def trConst(tString):
@@ -34,7 +34,7 @@ def trConst(tString):
return QCoreApplication.translate("Constant", tString)
class nwConst():
class nwConst:
# Date and Time Formats
FMT_TSTAMP = "%Y-%m-%d %H:%M:%S" # Default format
@@ -42,32 +42,13 @@ class nwConst():
FMT_DSTAMP = "%Y-%m-%d" # Date only format
# Various Hard Limits
MAX_DEPTH = 30 # Maximum folder depth of a project
MAX_DOCSIZE = 5000000 # Maxium size of a single document
MAX_BUILDSIZE = 10000000 # Maxium size of a project build
# END Class nwConst
class nwLists():
"""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():
class nwRegEx:
FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
@@ -76,7 +57,7 @@ class nwRegEx():
# END Class nwRegEx
class nwFiles():
class nwFiles:
PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt"
@@ -126,7 +107,7 @@ class nwKeyWords:
# END Class nwKeyWords
class nwLabels():
class nwLabels:
CLASS_NAME = {
nwItemClass.NO_CLASS: QT_TRANSLATE_NOOP("Constant", "None"),
@@ -204,7 +185,7 @@ class nwLabels():
# END Class nwLabels
class nwQuotes():
class nwQuotes:
"""Allowed quotation marks.
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.index import NWIndex, countWords
from novelwriter.core.index import countWords
from novelwriter.core.project import NWProject
from novelwriter.core.spellcheck import NWSpellEnchant
from novelwriter.core.tohtml import ToHtml
@@ -30,7 +30,6 @@ from novelwriter.core.tomd import ToMarkdown
__all__ = [
"countWords",
"NWDoc",
"NWIndex",
"NWProject",
"NWSpellEnchant",
"ToHtml",
+1 -1
View File
@@ -52,7 +52,7 @@ class NWDoc():
self._docHandle = theHandle
if self._docHandle is not None:
self._theItem = self.theProject.projTree[theHandle]
self._theItem = self.theProject.tree[theHandle]
return
+755 -364
View File
File diff suppressed because it is too large Load Diff
+141 -66
View File
@@ -29,9 +29,9 @@ from lxml import etree
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
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__)
@@ -45,6 +45,7 @@ class NWItem():
self._name = ""
self._handle = None
self._parent = None
self._root = None
self._order = 0
self._type = nwItemType.NO_TYPE
self._class = nwItemClass.NO_CLASS
@@ -85,6 +86,10 @@ class NWItem():
def itemParent(self):
return self._parent
@property
def itemRoot(self):
return self._root
@property
def itemOrder(self):
return self._order
@@ -147,6 +152,7 @@ class NWItem():
itemAttrib = {}
itemAttrib["handle"] = str(self._handle)
itemAttrib["parent"] = str(self._parent)
itemAttrib["root"] = str(self._root)
itemAttrib["order"] = str(self._order)
itemAttrib["type"] = str(self._type.name)
itemAttrib["class"] = str(self._class.name)
@@ -154,19 +160,18 @@ class NWItem():
itemAttrib["layout"] = str(self._layout.name)
metaAttrib = {}
metaAttrib["expanded"] = str(self._expanded)
if self._type == nwItemType.FILE:
metaAttrib["charCount"] = str(self._charCount)
metaAttrib["wordCount"] = str(self._wordCount)
metaAttrib["paraCount"] = str(self._paraCount)
metaAttrib["cursorPos"] = str(self._cursorPos)
else:
metaAttrib["expanded"] = str(self._expanded)
nameAttrib = {}
nameAttrib["status"] = str(self._status)
nameAttrib["import"] = str(self._import)
if self._type == nwItemType.FILE:
nameAttrib["exported"] = str(self._exported)
nameAttrib["exported"] = str(self._exported)
xPack = etree.SubElement(xParent, "item", attrib=itemAttrib)
self._subPack(xPack, "meta", attrib=metaAttrib)
@@ -188,6 +193,7 @@ class NWItem():
return False
self.setParent(xItem.attrib.get("parent", None))
self.setRoot(xItem.attrib.get("root", None))
self.setOrder(xItem.attrib.get("order", 0))
self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE))
self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS))
@@ -233,6 +239,17 @@ class NWItem():
# version of novelWriter that doesn't know the 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
@staticmethod
@@ -249,7 +266,7 @@ class NWItem():
return
##
# Methods
# Lookup Methods
##
def describeMe(self, hLevel=None):
@@ -275,168 +292,226 @@ class NWItem():
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):
"""Return the relevant importance or status label and icon for
the current item based on its class.
"""
if self._class in nwLists.CLS_NOVEL:
stName = self.theProject.statusItems.checkEntry(self._status)
stIcon = self.theProject.statusItems.getIcon(stName)
if self.isNovelLike():
stName = self.theProject.statusItems.name(self._status)
stIcon = self.theProject.statusItems.icon(self._status)
else:
stName = self.theProject.importItems.checkEntry(self._import)
stIcon = self.theProject.importItems.getIcon(stName)
stName = self.theProject.importItems.name(self._import)
stIcon = self.theProject.importItems.icon(self._import)
return stName, stIcon
def setImportStatus(self, theLabel):
##
# Special Setters
##
def setImportStatus(self, value):
"""Update the importance or status value based on class. This is
a wrapper setter for setStatus and setImport.
"""
if self._class in nwLists.CLS_NOVEL:
self.setStatus(theLabel)
if self.isNovelLike():
self.setStatus(value)
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
##
# Set Item Values
##
def setName(self, theName):
def setName(self, name):
"""Set the item name.
"""
if isinstance(theName, str):
self._name = theName.strip()
if isinstance(name, str):
self._name = simplified(name)
else:
self._name = ""
return
def setHandle(self, theHandle):
def setHandle(self, handle):
"""Set the item handle, and ensure it is valid.
"""
if isHandle(theHandle):
self._handle = theHandle
if isHandle(handle):
self._handle = handle
else:
self._handle = None
return
def setParent(self, theParent):
def setParent(self, handle):
"""Set the parent handle, and ensure it is valid.
"""
if theParent is None:
if handle is None:
self._parent = None
elif isHandle(theParent):
self._parent = theParent
elif isHandle(handle):
self._parent = handle
else:
self._parent = None
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
is purely a meta value, and not actually used by novelWriter at
the moment.
"""
self._order = checkInt(theOrder, 0)
self._order = checkInt(order, 0)
return
def setType(self, theType):
def setType(self, value):
"""Set the item type from either a proper nwItemType, or set it
from a string representing an nwItemType.
"""
if isinstance(theType, nwItemType):
self._type = theType
elif isItemType(theType):
self._type = nwItemType[theType]
if isinstance(value, nwItemType):
self._type = value
elif isItemType(value):
self._type = nwItemType[value]
elif value == "TRASH":
self._type = nwItemType.ROOT
else:
logger.error("Unrecognised item type '%s'", theType)
logger.error("Unrecognised item type '%s'", value)
self._type = nwItemType.NO_TYPE
return
def setClass(self, theClass):
def setClass(self, value):
"""Set the item class from either a proper nwItemClass, or set
it from a string representing an nwItemClass.
"""
if isinstance(theClass, nwItemClass):
self._class = theClass
elif isItemClass(theClass):
self._class = nwItemClass[theClass]
if isinstance(value, nwItemClass):
self._class = value
elif isItemClass(value):
self._class = nwItemClass[value]
else:
logger.error("Unrecognised item class '%s'", theClass)
logger.error("Unrecognised item class '%s'", value)
self._class = nwItemClass.NO_CLASS
return
def setLayout(self, theLayout):
def setLayout(self, value):
"""Set the item layout from either a proper nwItemLayout, or set
it from a string representing an nwItemLayout.
"""
if isinstance(theLayout, nwItemLayout):
self._layout = theLayout
elif isItemLayout(theLayout):
self._layout = nwItemLayout[theLayout]
elif theLayout in nwLists.DEP_LAYOUT:
if isinstance(value, nwItemLayout):
self._layout = value
elif isItemLayout(value):
self._layout = nwItemLayout[value]
elif value in ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"):
self._layout = nwItemLayout.DOCUMENT
else:
logger.error("Unrecognised item layout '%s'", theLayout)
logger.error("Unrecognised item layout '%s'", value)
self._layout = nwItemLayout.NO_LAYOUT
return
def setStatus(self, theStatus):
def setStatus(self, value):
"""Set the item status by looking it up in the valid status
items of the current project.
"""
self._status = self.theProject.statusItems.checkEntry(theStatus)
self._status = self.theProject.statusItems.check(value)
return
def setImport(self, theImport):
def setImport(self, value):
"""Set the item importance by looking it up in the valid import
items of the current project.
"""
self._import = self.theProject.importItems.checkEntry(theImport)
self._import = self.theProject.importItems.check(value)
return
def setExpanded(self, expState):
def setExpanded(self, state):
"""Set the expanded status of an item in the project tree.
"""
if isinstance(expState, str):
self._expanded = (expState == str(True))
if isinstance(state, str):
self._expanded = (state == str(True))
else:
self._expanded = (expState is True)
self._expanded = (state is True)
return
def setExported(self, expState):
def setExported(self, state):
"""Set the export flag.
"""
if isinstance(expState, str):
self._exported = (expState == str(True))
if isinstance(state, str):
self._exported = (state == str(True))
else:
self._exported = (expState is True)
self._exported = (state is True)
return
##
# Set Document Meta Data
##
def setCharCount(self, theCount):
def setCharCount(self, count):
"""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
def setWordCount(self, theCount):
def setWordCount(self, count):
"""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
def setParaCount(self, theCount):
def setParaCount(self, count):
"""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
def setCursorPos(self, thePosition):
def setCursorPos(self, position):
"""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
def saveInitialCount(self):
+192 -154
View File
@@ -37,6 +37,7 @@ from PyQt5.QtCore import QCoreApplication
from novelwriter.core.tree import NWTree
from novelwriter.core.item import NWItem
from novelwriter.core.index import NWIndex
from novelwriter.core.status import NWStatus
from novelwriter.core.options import OptionState
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.common import (
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__)
class NWProject():
FILE_VERSION = "1.4"
FILE_VERSION = "1.4" # The current project file format version
def __init__(self, theParent):
@@ -62,9 +63,10 @@ class NWProject():
self.mainConf = novelwriter.CONFIG
# Core Elements
self.optState = OptionState(self) # Project-specific GUI options
self.projTree = NWTree(self) # The project tree
self.langData = {} # Localisation data
self._optState = OptionState(self) # Project-specific GUI options
self._projTree = NWTree(self) # The project tree
self._projIndex = NWIndex(self) # The projecty index
self._langData = {} # Localisation data
# Project Status
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
# Project Meta
self.projName = "" # Project name (working title)
self.projName = "" # Project name
self.bookTitle = "" # The final title; should only be used for exports
self.bookAuthors = [] # A list of book authors
@@ -116,63 +118,70 @@ class NWProject():
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
##
def newRoot(self, rootName, rootClass):
"""Add a new root item. These items are unique, except for item class
CUSTOM, and always have parent handle set to None.
def newRoot(self, itemClass, label=None):
"""Add a new root item. If label is None, use the class label.
"""
if not self.projTree.checkRootUnique(rootClass):
self.theParent.makeAlert(self.tr("Duplicate root item detected."), nwAlert.ERROR)
return None
if label is None:
label = trConst(nwLabels.CLASS_NAME[itemClass])
newItem = NWItem(self)
newItem.setName(rootName)
newItem.setName(label)
newItem.setType(nwItemType.ROOT)
newItem.setClass(rootClass)
newItem.setStatus(0)
self.projTree.append(None, None, newItem)
newItem.setClass(itemClass)
self._projTree.append(None, None, newItem)
self._projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def newFolder(self, folderName, folderClass, pHandle):
"""Add a new folder with a given name and class and parent item.
def newFolder(self, label, pHandle):
"""Add a new folder with a given label and parent item.
"""
newItem = NWItem(self)
newItem.setName(folderName)
newItem.setName(label)
newItem.setType(nwItemType.FOLDER)
newItem.setClass(folderClass)
newItem.setStatus(0)
self.projTree.append(None, pHandle, newItem)
self._projTree.append(None, pHandle, newItem)
self._projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def newFile(self, fileName, fileClass, pHandle):
"""Add a new file with a given name and class, and set a layout
based on the class. DOCUMENT for NOVEL, otherwise NOTE.
def newFile(self, label, pHandle):
"""Add a new file with a given label and parent item.
"""
newItem = NWItem(self)
newItem.setName(fileName)
newItem.setName(label)
newItem.setType(nwItemType.FILE)
if fileClass == nwItemClass.NOVEL:
newItem.setLayout(nwItemLayout.DOCUMENT)
else:
newItem.setLayout(nwItemLayout.NOTE)
newItem.setClass(fileClass)
newItem.setStatus(0)
self.projTree.append(None, pHandle, newItem)
self._projTree.append(None, pHandle, newItem)
self._projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def trashFolder(self):
"""Add the special trash root folder to the project.
"""
trashHandle = self.projTree.trashRoot()
trashHandle = self._projTree.trashRoot()
if trashHandle is None:
newItem = NWItem(self)
newItem.setName(trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH]))
newItem.setType(nwItemType.TRASH)
newItem.setType(nwItemType.ROOT)
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 trashHandle
@@ -193,7 +202,7 @@ class NWProject():
self.autoCount = 0
# Project Tree
self.projTree.clear()
self._projTree.clear()
# Project Settings
self.projPath = None
@@ -218,16 +227,16 @@ class NWProject():
}
self.spellCheck = False
self.autoOutline = True
self.statusItems = NWStatus()
self.statusItems.addEntry(self.tr("New"), (100, 100, 100))
self.statusItems.addEntry(self.tr("Note"), (200, 50, 0))
self.statusItems.addEntry(self.tr("Draft"), (200, 150, 0))
self.statusItems.addEntry(self.tr("Finished"), (50, 200, 0))
self.importItems = NWStatus()
self.importItems.addEntry(self.tr("New"), (100, 100, 100))
self.importItems.addEntry(self.tr("Minor"), (200, 50, 0))
self.importItems.addEntry(self.tr("Major"), (200, 150, 0))
self.importItems.addEntry(self.tr("Main"), (50, 200, 0))
self.statusItems = NWStatus(NWStatus.STATUS)
self.statusItems.write(None, self.tr("New"), (100, 100, 100))
self.statusItems.write(None, self.tr("Note"), (200, 50, 0))
self.statusItems.write(None, self.tr("Draft"), (200, 150, 0))
self.statusItems.write(None, self.tr("Finished"), (50, 200, 0))
self.importItems = NWStatus(NWStatus.IMPORT)
self.importItems.write(None, self.tr("New"), (100, 100, 100))
self.importItems.write(None, self.tr("Minor"), (200, 50, 0))
self.importItems.write(None, self.tr("Major"), (200, 150, 0))
self.importItems.write(None, self.tr("Main"), (50, 200, 0))
self.lastEdited = None
self.lastViewed = None
self.lastWCount = 0
@@ -267,6 +276,7 @@ class NWProject():
logger.error("No project path set for the new project")
return False
self.clearProject()
if not self.setProjectPath(projPath, newProject=True):
return False
@@ -274,86 +284,88 @@ class NWProject():
self.setBookTitle(projTitle)
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)
if self.bookAuthors:
titlePage = "%s>> %s %s <<\n" % (titlePage, self.tr("By"), self.getAuthors())
aDoc = NWDoc(self, hTitlePage)
aDoc.writeDocument(titlePage)
if popMinimal:
# 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 = NWDoc(self, xHandle[7])
# 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 = 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"))
self.newRoot(nwItemClass.PLOT)
self.newRoot(nwItemClass.CHARACTER)
self.newRoot(nwItemClass.WORLD)
self.newRoot(nwItemClass.ARCHIVE)
elif popCustom:
# Create a project structure based on selected root folders
# and a number of chapters and scenes selected in the
# wizard's custom page.
# Create 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
numChapters = projData.get("numChapters", 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
if numChapters > 0:
for ch in range(numChapters):
chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}")
pHandle = nHandle
if chFolders:
pHandle = self.newFolder(chTitle, nwItemClass.NOVEL, nHandle)
cHandle = self.newFile(chTitle, nwItemClass.NOVEL, pHandle)
self.projTree.setFileItemLayout(cHandle, nwItemLayout.DOCUMENT)
cHandle = self.newFile(chTitle, hNovelRoot)
aDoc = NWDoc(self, cHandle)
aDoc.writeDocument("## %s\n\n" % chTitle)
aDoc.writeDocument(f"## {chTitle}\n\n% Synopsis: {chSynop}\n\n")
# Create chapter scenes
if numScenes > 0:
for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
sHandle = self.newFile(scTitle, nwItemClass.NOVEL, pHandle)
sHandle = self.newFile(scTitle, cHandle)
aDoc = NWDoc(self, sHandle)
aDoc.writeDocument("### %s\n\n" % scTitle)
aDoc.writeDocument(f"### {scTitle}\n\n% Synopsis: {scSynop}\n\n")
# Create scenes (no chapters)
elif numScenes > 0:
for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
sHandle = self.newFile(scTitle, nwItemClass.NOVEL, nHandle)
sHandle = self.newFile(scTitle, hNovelRoot)
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
if popCustom or popMinimal:
@@ -480,8 +492,9 @@ class NWProject():
# documents and one for project notes. Introduced in
# version 1.5.
# 1.4 : Introduces a more compact format for storing items. All
# settings aside from name are now attributes. Introduced
# in version 1.7.
# settings aside from name are now attributes. This format
# 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"):
self.theParent.makeAlert(self.tr(
@@ -533,14 +546,16 @@ class NWProject():
if xItem.text is None:
continue
if xItem.tag == "name":
logger.verbose("Working Title: '%s'", xItem.text)
self.projName = xItem.text
self.projName = checkString(simplified(xItem.text), "")
logger.verbose("Working Title: '%s'", self.projName)
elif xItem.tag == "title":
logger.verbose("Title is '%s'", xItem.text)
self.bookTitle = xItem.text
self.bookTitle = checkString(simplified(xItem.text), "")
logger.verbose("Title is '%s'", self.bookTitle)
elif xItem.tag == "author":
logger.verbose("Author: '%s'", xItem.text)
self.bookAuthors.append(xItem.text)
author = checkString(simplified(xItem.text), "")
if author:
self.bookAuthors.append(author)
logger.verbose("Author: '%s'", author)
elif xItem.tag == "saveCount":
self.saveCount = checkInt(xItem.text, 0)
elif xItem.tag == "autoCount":
@@ -591,9 +606,9 @@ class NWProject():
elif xChild.tag == "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
if legacyList:
@@ -610,7 +625,13 @@ class NWProject():
self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time())
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._loadProjectLocalisation()
@@ -621,6 +642,7 @@ class NWProject():
self._writeLockFile()
self.setProjectChanged(False)
self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName))
return True
@@ -687,6 +709,8 @@ class NWProject():
if len(aKey) > 0:
self._packProjectValue(xTitleFmt, aKey, aValue)
# Save Status/Importance
self.countStatus()
xStatus = etree.SubElement(xSettings, "status")
self.statusItems.packXML(xStatus)
xStatus = etree.SubElement(xSettings, "importance")
@@ -694,7 +718,7 @@ class NWProject():
# Save Tree Content
logger.debug("Writing project content")
self.projTree.packXML(nwXML)
self._projTree.packXML(nwXML)
# Write the xml tree to file
tempFile = os.path.join(self.projPath, self.projFile+"~")
@@ -727,7 +751,7 @@ class NWProject():
return False
# Save project GUI options
self.optState.saveSettings()
self._optState.saveSettings()
# Update recent projects
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.
"""
logger.info("Closing project: %s", self.projPath)
self.optState.saveSettings()
self.projTree.writeToCFile()
self._optState.saveSettings()
self._projTree.writeToCFile()
self._appendSessionStats(idleTime)
self._clearLockFile()
self.clearProject()
@@ -950,17 +974,17 @@ class NWProject():
return True
def setProjectName(self, projName):
"""Set the project name (working title), This is the the title
used for backup files etc.
"""Set the project name, This is the the name used for backup
files etc.
"""
self.projName = projName.strip()
self.projName = simplified(projName)
self.setProjectChanged(True)
return True
def setBookTitle(self, bookTitle):
"""Set the book title, that is, the title to include in exports.
"""
self.bookTitle = bookTitle.strip()
self.bookTitle = simplified(bookTitle)
self.setProjectChanged(True)
return True
@@ -972,7 +996,7 @@ class NWProject():
self.bookAuthors = []
for bookAuthor in bookAuthors.splitlines():
bookAuthor = bookAuthor.strip()
bookAuthor = simplified(bookAuthor)
if bookAuthor == "":
continue
self.bookAuthors.append(bookAuthor)
@@ -1018,7 +1042,8 @@ class NWProject():
if self.projSpell != theLang:
self.projSpell = theLang
self.setProjectChanged(True)
return True
return True
return False
def setProjectLang(self, theLang):
"""Set the project-specific language.
@@ -1043,9 +1068,9 @@ class NWProject():
items in the GUI project tree. The user can rearrange the order
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")
self.projTree.setOrder(newOrder)
self._projTree.setOrder(newOrder)
self.setProjectChanged(True)
return True
@@ -1065,34 +1090,22 @@ class NWProject():
self.setProjectChanged(True)
return True
def setStatusColours(self, newCols):
"""Update the list of novel file status flags. Also iterate
through the project and replace keys that have been renamed.
def setStatusColours(self, newCols, delCols):
"""Update the list of novel file status flags.
"""
replaceMap = self.statusItems.setNewEntries(newCols)
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
return self._setStatusImport(newCols, delCols, self.statusItems)
def setImportColours(self, newCols):
"""Update the list of note file importance flags. Also iterate
through the project and replace keys that have been renamed.
def setImportColours(self, newCols, delCols):
"""Update the list of note file importance flags.
"""
replaceMap = self.importItems.setNewEntries(newCols)
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
return self._setStatusImport(newCols, delCols, self.importItems)
def setAutoReplace(self, autoReplace):
"""Update the auto-replace dictionary.
"""
self.autoReplace = autoReplace
self.autoReplace = {}
for key, entry in autoReplace.items():
self.autoReplace[key] = simplified(entry)
self.setProjectChanged(True)
return True
@@ -1101,7 +1114,9 @@ class NWProject():
"""
for valKey, valEntry in titleFormat.items():
if valKey in self.titleFormat:
self.titleFormat[valKey] = checkString(valEntry, self.titleFormat[valKey])
self.titleFormat[valKey] = checkString(
simplified(valEntry), self.titleFormat[valKey]
)
return True
def setProjectChanged(self, bValue):
@@ -1149,16 +1164,16 @@ class NWProject():
capable of handling it.
"""
sentItems = []
iterItems = self.projTree.handles()
iterItems = self._projTree.handles()
n = 0
nMax = min(len(iterItems), 10000)
while n < nMax:
tHandle = iterItems[n]
tItem = self.projTree[tHandle]
tItem = self._projTree[tHandle]
n += 1
if tItem is None:
# Technically a bug since treeOrder is built from the
# same data as projTree
# same data as _projTree
continue
elif tItem.itemParent is None:
# Item is a root, or already been identified as an
@@ -1189,7 +1204,7 @@ class NWProject():
def updateWordCounts(self):
"""Update the total word count values.
"""
wcNovel, wcNotes = self.projTree.sumWords()
wcNovel, wcNotes = self._projTree.sumWords()
wcTotal = wcNovel + wcNotes
if wcTotal != self.currWCount:
self.currNovelWC = wcNovel
@@ -1205,11 +1220,11 @@ class NWProject():
"""
self.statusItems.resetCounts()
self.importItems.resetCounts()
for nwItem in self.projTree:
if nwItem.itemClass in nwLists.CLS_NOVEL:
self.statusItems.countEntry(nwItem.itemStatus)
for nwItem in self._projTree:
if nwItem.isNovelLike():
self.statusItems.increment(nwItem.itemStatus)
else:
self.importItems.countEntry(nwItem.itemImport)
self.importItems.increment(nwItem.itemImport)
return
def localLookup(self, theWord):
@@ -1217,17 +1232,39 @@ class NWProject():
return it. The variable is cast to a string before lookup. If
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
##
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):
"""Load the language data for the current project language.
"""
if self.projLang is None:
self.langData = {}
self._langData = {}
return False
langFile = os.path.join(self.mainConf.nwLangPath, "project_%s.json" % self.projLang)
@@ -1236,7 +1273,7 @@ class NWProject():
try:
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))
except Exception:
@@ -1371,7 +1408,7 @@ class NWProject():
logger.warning("Skipping file: %s", fileItem)
continue
if fHandle in self.projTree:
if fHandle in self._projTree:
self.projFiles.append(fHandle)
logger.debug("Checking file %s, handle '%s': OK", fileItem, fHandle)
else:
@@ -1418,10 +1455,10 @@ class NWProject():
if oLayout is None:
oLayout = nwItemLayout.NOTE
if oParent is None or oParent not in self.projTree:
oParent = self.projTree.findRoot(oClass)
if oParent is None or oParent not in self._projTree:
oParent = self._projTree.findRoot(oClass)
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 oParent is None:
@@ -1433,7 +1470,8 @@ class NWProject():
orphItem.setType(nwItemType.FILE)
orphItem.setClass(oClass)
orphItem.setLayout(oLayout)
self.projTree.append(oHandle, oParent, orphItem)
self._projTree.append(oHandle, oParent, orphItem)
self._projTree.updateItemData(orphItem.itemHandle)
if noWhere:
self.theParent.makeAlert(self.tr(
+198 -109
View File
@@ -4,7 +4,8 @@ novelWriter Project Item Status Class
Data class for the status/importance settings of a project item
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
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/>.
"""
import random
import logging
import novelwriter
@@ -30,134 +32,208 @@ from lxml import etree
from PyQt5.QtGui import QIcon, QPixmap, QColor
from novelwriter.common import checkInt
from novelwriter.common import checkInt, minmax, simplified
logger = logging.getLogger(__name__)
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)
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
def addEntry(self, theLabel, theColours):
"""Add a status entry to the status object, but ensure it isn't
a duplicate.
def write(self, key, name, cols, count=None):
"""Add or update a status entry. If the key is invalid, a new
key is generated.
"""
theLabel = theLabel.strip()
if self._getIndex(theLabel) is None:
theIcon = QPixmap(self._iconSize, self._iconSize)
theIcon.fill(QColor(*theColours))
self._theIcons.append(QIcon(theIcon))
self._theLabels.append(theLabel)
self._theColours.append(theColours)
self._theCounts.append(0)
self._theMap[theLabel] = self._theLength
self._theLength += 1
if not self._isKey(key):
key = self._newKey()
if not isinstance(cols, tuple):
cols = (100, 100, 100)
if len(cols) != 3:
cols = (100, 100, 100)
pixmap = QPixmap(self._iconSize, self._iconSize)
pixmap.fill(QColor(*cols))
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
def checkEntry(self, theStatus):
"""Check if a status value is valid, and returns the safe
reference to be used internally.
def check(self, value):
"""Check the key against the stored status names.
"""
if isinstance(theStatus, str):
if self._getIndex(theStatus) is not None:
return theStatus.strip()
return self._theLabels[0]
if self._isKey(value) and value in self._store:
return value
elif value in self._reverse:
return self._reverse[value]
elif self._default is not None:
return self._default
else:
return ""
def getIcon(self, theLabel):
"""Return the icon for the given status item.
def name(self, key):
"""Return the name associated with a given key.
"""
theIndex = self._getIndex(theLabel)
if theIndex is not None:
return self._theIcons[theIndex]
return QIcon()
if key in self._store:
return self._store[key]["name"]
elif self._default is not None:
return self._store[self._default]["name"]
else:
return ""
def setNewEntries(self, newList):
"""Update the list of entries after they have been modified by
the GUI tool.
def cols(self, key):
"""Return the colours associated with a given key.
"""
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:
self._theLabels = []
self._theColours = []
self._theCounts = []
self._theIcons = []
self._theMap = {}
self._theLength = 0
self._theIndex = 0
def count(self, key):
"""Return the count associated with a given key.
"""
if key in self._store:
return self._store[key]["count"]
elif self._default is not None:
return self._store[self._default]["count"]
else:
return 0
for nName, nR, nG, nB, oName in newList:
self.addEntry(nName, (nR, nG, nB))
if nName != oName and oName is not None:
replaceMap[oName] = nName
def icon(self, key):
"""Return the icon associated with a given key.
"""
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):
"""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
def countEntry(self, theLabel):
"""Increment the counter for a given label. This should be used
together with resetCounts in a loop over project items.
def increment(self, key):
"""Increment the counter for a given entry.
"""
theIndex = self._getIndex(theLabel)
if theIndex is not None:
self._theCounts[theIndex] += 1
if key in self._store:
self._store[key]["count"] += 1
return
def packXML(self, xParent):
"""Pack the status entries into an XML object for saving to the
main project file.
"""
for n in range(self._theLength):
for key, data in self._store.items():
xSub = etree.SubElement(xParent, "entry", attrib={
"red": str(self._theColours[n][0]),
"green": str(self._theColours[n][1]),
"blue": str(self._theColours[n][2]),
"key": key,
"count": str(data["count"]),
"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
def unpackXML(self, xParent):
"""Unpack an XML tree and set the class values.
"""
theLabels = []
theColours = []
self._store = {}
self._reverse = {}
self._default = None
for xChild in xParent:
theLabels.append(xChild.text)
cR = checkInt(xChild.attrib.get("red", 0), 0, False)
cG = checkInt(xChild.attrib.get("green", 0), 0, False)
cB = checkInt(xChild.attrib.get("blue", 0), 0, False)
theColours.append((cR, cG, cB))
if len(theLabels) > 0:
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])
key = xChild.attrib.get("key", None)
name = xChild.text.strip()
count = max(checkInt(xChild.attrib.get("count", 0), 0), 0)
red = minmax(checkInt(xChild.attrib.get("red", 100), 100), 0, 255)
green = minmax(checkInt(xChild.attrib.get("green", 100), 100), 0, 255)
blue = minmax(checkInt(xChild.attrib.get("blue", 100), 100), 0, 255)
self.write(key, name, (red, green, blue), count)
return True
@@ -165,39 +241,52 @@ class NWStatus():
# Internal Functions
##
def _getIndex(self, theLabel):
"""Look up a status entry in the object lists, and return it if
it exists.
def _newKey(self):
"""Generate a new key for a status flag. This method is
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:
return None
return self._theMap.get(theLabel.strip(), None)
key = f"{self._prefix}{random.getrandbits(24):06x}"
if key in self._store:
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
##
def __getitem__(self, n):
"""Return an entry by its index.
"""
if n >= 0 and n < self._theLength:
return self._theLabels[n], self._theColours[n], self._theCounts[n], self._theIcons[n]
return None, None, None, QIcon()
def __len__(self):
return len(self._store)
def __getitem__(self, key):
return self._store[key]
def __iter__(self):
"""Initialise the iterator.
"""
self._theIndex = 0
return self
return iter(self._store)
def __next__(self):
"""Return the next entry for the iterator.
"""
if self._theIndex < self._theLength:
theLabel, theColour, theCount, theIcon = self.__getitem__(self._theIndex)
self._theIndex += 1
return theLabel, theColour, theCount, theIcon
else:
raise StopIteration
def keys(self):
return self._store.keys()
def items(self):
return self._store.items()
def values(self):
return self._store.values()
# END Class NWStatus
+1 -1
View File
@@ -451,7 +451,7 @@ class ToHtml(Tokenizer):
def _formatKeywords(self, tText):
"""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:
return ""
+3 -3
View File
@@ -275,7 +275,7 @@ class Tokenizer(ABC):
def addRootHeading(self, theHandle):
"""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
if self._isFirst:
@@ -284,7 +284,7 @@ class Tokenizer(ABC):
else:
textAlign = self.A_PBB | self.A_CENTRE
theItem = self.theProject.projTree[theHandle]
theItem = self.theProject.tree[theHandle]
locNotes = self._localLookup("Notes")
theTitle = f"{locNotes}: {theItem.itemName}"
self._theTokens = []
@@ -301,7 +301,7 @@ class Tokenizer(ABC):
not set, load it from the file.
"""
self._theHandle = theHandle
self._theItem = self.theProject.projTree[theHandle]
self._theItem = self.theProject.tree[theHandle]
if self._theItem is None:
return False
+1 -1
View File
@@ -193,7 +193,7 @@ class ToMarkdown(Tokenizer):
def _formatKeywords(self, tText, tStyle):
"""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:
return ""
+1 -1
View File
@@ -550,7 +550,7 @@ class ToOdt(Tokenizer):
def _formatKeywords(self, tText):
"""Apply formatting to keywords.
"""
isValid, theBits, _ = self.theParent.theIndex.scanThis("@"+tText)
isValid, theBits, _ = self.theProject.index.scanThis("@"+tText)
if not isValid or not theBits:
return ""
+109 -137
View File
@@ -24,16 +24,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import random
import logging
from time import time
from lxml import etree
from hashlib import sha256
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.error import logException
from novelwriter.common import checkHandle
from novelwriter.constants import nwConst, nwFiles
from novelwriter.constants import nwFiles
from novelwriter.core.item import NWItem
logger = logging.getLogger(__name__)
@@ -41,21 +40,20 @@ logger = logging.getLogger(__name__)
class NWTree():
MAX_DEPTH = 1000 # Cap of tree traversing for loops
def __init__(self, theProject):
self.theProject = theProject
self._projTree = {} # Holds all the items of the project
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._archRoot = None # The handle of the archive root folder
self._theIndex = 0 # The current iterator index
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
##
@@ -67,7 +65,7 @@ class NWTree():
"""
self._projTree = {}
self._treeOrder = []
self._treeRoots = []
self._treeRoots = {}
self._trashRoot = None
self._archRoot = None
self._theIndex = 0
@@ -98,18 +96,17 @@ class NWTree():
if nwItem.itemType == nwItemType.ROOT:
logger.verbose("Item '%s' is a root item", str(tHandle))
self._treeRoots.append(tHandle)
self._treeRoots[tHandle] = nwItem
if nwItem.itemClass == nwItemClass.ARCHIVE:
logger.verbose("Item '%s' is the archive folder", str(tHandle))
self._archRoot = tHandle
if nwItem.itemType == nwItemType.TRASH:
if self._trashRoot is None:
logger.verbose("Item '%s' is the trash folder", str(tHandle))
self._trashRoot = tHandle
else:
logger.error("Only one trash folder allowed")
return False
elif nwItem.itemClass == nwItemClass.TRASH:
if self._trashRoot is None:
logger.verbose("Item '%s' is the trash folder", str(tHandle))
self._trashRoot = tHandle
else:
logger.error("Only one trash folder allowed")
return False
self._projTree[tHandle] = nwItem
self._treeOrder.append(tHandle)
@@ -207,9 +204,30 @@ class NWTree():
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):
"""Return true of item exists and is of the specified item type.
"""
@@ -218,71 +236,6 @@ class NWTree():
return False
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):
"""Iterate upwards in the tree until we find the item with
parent None, the root item, and return the list of handles.
@@ -293,7 +246,7 @@ class NWTree():
tItem = self.__getitem__(tHandle)
if tItem is not None:
tTree.append(tHandle)
for _ in range(nwConst.MAX_DEPTH + 1):
for _ in range(self.MAX_DEPTH):
if tItem.itemParent is None:
return tTree
else:
@@ -303,8 +256,72 @@ class NWTree():
return tTree
else:
tTree.append(tHandle)
else:
raise RecursionError("Critical internal error")
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
##
@@ -334,14 +351,6 @@ class NWTree():
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):
"""Set the nwItemLayout for a specific file.
"""
@@ -358,30 +367,6 @@ class NWTree():
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
##
@@ -420,7 +405,7 @@ class NWTree():
return
if tHandle in self._treeRoots:
self._treeRoots.remove(tHandle)
del self._treeRoots[tHandle]
if tHandle == self._trashRoot:
self._trashRoot = None
if tHandle == self._archRoot:
@@ -468,29 +453,16 @@ class NWTree():
self.theProject.setProjectChanged(True)
return
def _makeHandle(self, addSeed=""):
def _makeHandle(self):
"""Generate a unique item handle. In the event that the key
already exists, salt the seed and generate a new handle.
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.
already exists, generate a new one.
"""
if self._handleSeed is None:
newSeed = "%s_%d_%s" % (str(time()), self._handleCount, addSeed)
self._handleCount += 1
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.verbose("Generating new handle")
handle = f"{random.getrandbits(52):013x}"
if handle in self._projTree:
logger.warning("Duplicate handle encountered! Retrying ...")
itemHandle = self._makeHandle(addSeed+"!")
handle = self._makeHandle()
return itemHandle
return handle
# END Class NWTree
+3 -3
View File
@@ -181,14 +181,14 @@ class GuiAbout(QDialog):
aboutMsg += "<h4>{0}</h4><p>{1}</p>".format(
self.tr("Translations"),
self._wrapTable([
("Deutsch", "Myian"),
("English", "Veronica Berglyd Olsen"),
("Español Latinoamericano", "Tommy Marplatt"),
("Français", "Jan Lüdke (jyhelle)"),
("Nederlands", "Martijn van der Kleijn"),
("Norsk Bokmål", "Veronica Berglyd Olsen"),
("Português", "Bruno Meneguello"),
("简体中文", "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)
return False
srcItem = self.theProject.projTree[self.sourceItem]
srcItem = self.theProject.tree[self.sourceItem]
if srcItem is None:
self.theParent.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
return False
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent)
newItem = self.theProject.projTree[nHandle]
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent)
newItem = self.theProject.tree[nHandle]
newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport)
@@ -170,7 +170,7 @@ class GuiDocMerge(QDialog):
if tHandle is None:
return False
nwItem = self.theProject.projTree[tHandle]
nwItem = self.theProject.tree[tHandle]
if nwItem is None:
return False
@@ -182,7 +182,7 @@ class GuiDocMerge(QDialog):
for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle):
newItem = QListWidgetItem()
nwItem = self.theProject.projTree[sHandle]
nwItem = self.theProject.tree[sHandle]
if nwItem.itemType is not nwItemType.FILE:
continue
newItem.setText(nwItem.itemName)
+9 -27
View File
@@ -33,8 +33,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter.core import NWDoc
from novelwriter.enum import nwAlert, nwItemType, nwItemClass, nwItemLayout
from novelwriter.constants import nwConst
from novelwriter.enum import nwAlert, nwItemType
from novelwriter.gui.custom import QHelpLabel
logger = logging.getLogger(__name__)
@@ -51,7 +50,6 @@ class GuiDocSplit(QDialog):
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.sourceItem = None
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 4 (Section)"), 4)
spIndex = self.splitLevel.findData(
self.optState.getInt("GuiDocSplit", "spLevel", 3)
self.theProject.options.getInt("GuiDocSplit", "spLevel", 3)
)
if spIndex != -1:
self.splitLevel.setCurrentIndex(spIndex)
@@ -122,7 +120,7 @@ class GuiDocSplit(QDialog):
), nwAlert.ERROR)
return False
srcItem = self.theProject.projTree[self.sourceItem]
srcItem = self.theProject.tree[self.sourceItem]
if srcItem is None:
self.theParent.makeAlert(self.tr(
"Could not parse source document."
@@ -160,16 +158,6 @@ class GuiDocSplit(QDialog):
), nwAlert.ERROR)
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(
self.tr("Split Document"),
"{0}<br><br>{1}".format(
@@ -186,22 +174,16 @@ class GuiDocSplit(QDialog):
return False
# Create the folder
fHandle = self.theProject.newFolder(
srcItem.itemName, srcItem.itemClass, srcItem.itemParent
)
fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent)
self.theParent.treeView.revealNewTreeItem(fHandle)
logger.verbose("Creating folder '%s'", fHandle)
# Loop through, and create the files
for wTitle, iStart, iEnd in finalOrder:
isNovel = srcItem.itemClass == nwItemClass.NOVEL
itemLayout = nwItemLayout.DOCUMENT if isNovel else nwItemLayout.NOTE
wTitle = wTitle.lstrip("#").strip()
nHandle = self.theProject.newFile(wTitle, srcItem.itemClass, fHandle)
newItem = self.theProject.projTree[nHandle]
newItem.setLayout(itemLayout)
nHandle = self.theProject.newFile(wTitle, fHandle)
newItem = self.theProject.tree[nHandle]
newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport)
logger.verbose(
@@ -228,7 +210,7 @@ class GuiDocSplit(QDialog):
def _doClose(self):
"""Close the dialog window without doing anything.
"""
self.optState.saveSettings()
self.theProject.options.saveSettings()
self.close()
return
@@ -249,7 +231,7 @@ class GuiDocSplit(QDialog):
if self.sourceItem is None:
return False
nwItem = self.theProject.projTree[self.sourceItem]
nwItem = self.theProject.tree[self.sourceItem]
if nwItem is None:
return False
@@ -266,7 +248,7 @@ class GuiDocSplit(QDialog):
return False
spLevel = self.splitLevel.currentData()
self.optState.setValue("GuiDocSplit", "spLevel", spLevel)
self.theProject.options.setValue("GuiDocSplit", "spLevel", spLevel)
logger.debug(
"Scanning document '%s' for headings level <= %d",
self.sourceItem, spLevel
+21 -17
View File
@@ -33,7 +33,7 @@ from PyQt5.QtWidgets import (
)
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
logger = logging.getLogger(__name__)
@@ -55,7 +55,7 @@ class GuiItemEditor(QDialog):
# Build GUI
##
self.theItem = self.theProject.projTree[tHandle]
self.theItem = self.theProject.tree[tHandle]
if self.theItem is None:
self.close()
return
@@ -74,19 +74,28 @@ class GuiItemEditor(QDialog):
# Item Status
self.editStatus = QComboBox()
self.editStatus.setMinimumWidth(mVd)
if self.theItem.itemClass in nwLists.CLS_NOVEL:
for sLabel, _, _, sIcon in self.theProject.statusItems:
self.editStatus.addItem(sIcon, sLabel, sLabel)
if self.theItem.isNovelLike():
for key, entry in self.theProject.statusItems.items():
self.editStatus.addItem(entry["icon"], entry["name"], key)
index = self.editStatus.findData(self.theItem.itemStatus)
if index != -1:
self.editStatus.setCurrentIndex(index)
else:
for sLabel, _, _, sIcon in self.theProject.importItems:
self.editStatus.addItem(sIcon, sLabel, sLabel)
for key, entry in self.theProject.importItems.items():
self.editStatus.addItem(entry["icon"], entry["name"], key)
index = self.editStatus.findData(self.theItem.itemImport)
if index != -1:
self.editStatus.setCurrentIndex(index)
# Item Layout
self.editLayout = QComboBox()
self.editLayout.setMinimumWidth(mVd)
validLayouts = []
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.NOTE)
else:
@@ -97,6 +106,10 @@ class GuiItemEditor(QDialog):
if itemLayout in validLayouts:
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
self.textExport = QLabel(self.tr("Include when building project"))
self.editExport = QSwitch()
@@ -116,15 +129,6 @@ class GuiItemEditor(QDialog):
self.editName.setText(self.theItem.itemName)
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
##
+19 -23
View File
@@ -203,6 +203,22 @@ class GuiPreferencesGeneral(QWidget):
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
self.guiFont = QLineEdit()
self.guiFont.setReadOnly(True)
@@ -275,6 +291,7 @@ class GuiPreferencesGeneral(QWidget):
guiLang = self.guiLang.currentData()
guiTheme = self.guiTheme.currentData()
guiIcons = self.guiIcons.currentData()
guiSyntax = self.guiSyntax.currentData()
guiFont = self.guiFont.text()
guiFontSize = self.guiFontSize.value()
emphLabels = self.emphLabels.isChecked()
@@ -294,6 +311,7 @@ class GuiPreferencesGeneral(QWidget):
self.mainConf.guiLang = guiLang
self.mainConf.guiTheme = guiTheme
self.mainConf.guiIcons = guiIcons
self.mainConf.guiSyntax = guiSyntax
self.mainConf.guiFont = guiFont
self.mainConf.guiFontSize = guiFontSize
self.mainConf.emphLabels = emphLabels
@@ -834,25 +852,6 @@ class GuiPreferencesSyntax(QWidget):
self.mainForm.setHelpTextStyle(self.theTheme.helpText)
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
# =================
self.mainForm.addGroupLabel(self.tr("Quotes & Dialogue"))
@@ -902,7 +901,7 @@ class GuiPreferencesSyntax(QWidget):
self.showMultiSpaces = QSwitch()
self.showMultiSpaces.setChecked(self.mainConf.showMultiSpaces)
self.mainForm.addRow(
self.tr("Highlight multiple spaces"),
self.tr("Highlight multiple or trailing spaces"),
self.showMultiSpaces,
self.tr("Applies to the document editor only.")
)
@@ -912,9 +911,6 @@ class GuiPreferencesSyntax(QWidget):
def saveValues(self):
"""Save the values set for this tab.
"""
# Highlighting Theme
self.mainConf.guiSyntax = self.guiSyntax.currentData()
# Quotes & Dialogue
self.mainConf.highlightQuotes = self.highlightQuotes.isChecked()
self.mainConf.allowOpenSQuote = self.allowOpenSQuote.isChecked()
+27 -27
View File
@@ -52,18 +52,18 @@ class GuiProjectDetails(PagedDialog):
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.setWindowTitle(self.tr("Project Details"))
wW = self.mainConf.pxInt(600)
wH = self.mainConf.pxInt(400)
pOptions = self.theProject.options
self.setMinimumWidth(wW)
self.setMinimumHeight(wH)
self.resize(
self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "winWidth", wW)),
self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "winHeight", wH))
self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "winWidth", wW)),
self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "winHeight", wH))
)
self.tabMain = GuiProjectDetailsMain(self.theParent, self.theProject)
@@ -120,16 +120,17 @@ class GuiProjectDetails(PagedDialog):
countFrom = self.tabContents.poValue.value()
clearDouble = self.tabContents.dblValue.isChecked()
self.optState.setValue("GuiProjectDetails", "winWidth", winWidth)
self.optState.setValue("GuiProjectDetails", "winHeight", winHeight)
self.optState.setValue("GuiProjectDetails", "widthCol0", widthCol0)
self.optState.setValue("GuiProjectDetails", "widthCol1", widthCol1)
self.optState.setValue("GuiProjectDetails", "widthCol2", widthCol2)
self.optState.setValue("GuiProjectDetails", "widthCol3", widthCol3)
self.optState.setValue("GuiProjectDetails", "widthCol4", widthCol4)
self.optState.setValue("GuiProjectDetails", "wordsPerPage", wordsPerPage)
self.optState.setValue("GuiProjectDetails", "countFrom", countFrom)
self.optState.setValue("GuiProjectDetails", "clearDouble", clearDouble)
pOptions = self.theProject.options
pOptions.setValue("GuiProjectDetails", "winWidth", winWidth)
pOptions.setValue("GuiProjectDetails", "winHeight", winHeight)
pOptions.setValue("GuiProjectDetails", "widthCol0", widthCol0)
pOptions.setValue("GuiProjectDetails", "widthCol1", widthCol1)
pOptions.setValue("GuiProjectDetails", "widthCol2", widthCol2)
pOptions.setValue("GuiProjectDetails", "widthCol3", widthCol3)
pOptions.setValue("GuiProjectDetails", "widthCol4", widthCol4)
pOptions.setValue("GuiProjectDetails", "wordsPerPage", wordsPerPage)
pOptions.setValue("GuiProjectDetails", "countFrom", countFrom)
pOptions.setValue("GuiProjectDetails", "clearDouble", clearDouble)
return
@@ -145,7 +146,6 @@ class GuiProjectDetailsMain(QWidget):
self.theParent = theParent
self.theProject = theProject
self.theTheme = theParent.theTheme
self.theIndex = theParent.theIndex
fPx = self.theTheme.fontPixelSize
fPt = self.theTheme.fontPointSize
@@ -245,8 +245,9 @@ class GuiProjectDetailsMain(QWidget):
def updateValues(self):
"""Set all the values.
"""
hCounts = self.theIndex.getNovelTitleCounts()
nwCount = self.theIndex.getNovelWordCount()
pIndex = self.theProject.index
hCounts = pIndex.getNovelTitleCounts()
nwCount = pIndex.getNovelWordCount()
edTime = self.theProject.getCurrentEditTime()
self.wordCountVal.setText(f"{nwCount:n}")
@@ -277,8 +278,6 @@ class GuiProjectDetailsContents(QWidget):
self.theParent = theParent
self.theProject = theProject
self.theTheme = theParent.theTheme
self.theIndex = theParent.theIndex
self.optState = theProject.optState
# Internal
self._theToC = []
@@ -286,6 +285,7 @@ class GuiProjectDetailsContents(QWidget):
iPx = self.theTheme.baseIconSize
hPx = self.mainConf.pxInt(12)
vPx = self.mainConf.pxInt(4)
pOptions = self.theProject.options
# Contents Tree
# =============
@@ -314,11 +314,11 @@ class GuiProjectDetailsContents(QWidget):
treeHeader.setStretchLastSection(True)
treeHeader.setMinimumSectionSize(hPx)
wCol0 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol0", 200))
wCol1 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol1", 60))
wCol2 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol2", 60))
wCol3 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol3", 60))
wCol4 = self.mainConf.pxInt(self.optState.getInt("GuiProjectDetails", "widthCol4", 90))
wCol0 = self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol0", 200))
wCol1 = self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol1", 60))
wCol2 = self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol2", 60))
wCol3 = self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol3", 60))
wCol4 = self.mainConf.pxInt(pOptions.getInt("GuiProjectDetails", "widthCol4", 90))
self.tocTree.setColumnWidth(0, wCol0)
self.tocTree.setColumnWidth(1, wCol1)
@@ -330,9 +330,9 @@ class GuiProjectDetailsContents(QWidget):
# Options
# =======
wordsPerPage = self.optState.getInt("GuiProjectDetails", "wordsPerPage", 350)
countFrom = self.optState.getInt("GuiProjectDetails", "countFrom", 1)
clearDouble = self.optState.getInt("GuiProjectDetails", "clearDouble", True)
wordsPerPage = pOptions.getInt("GuiProjectDetails", "wordsPerPage", 350)
countFrom = pOptions.getInt("GuiProjectDetails", "countFrom", 1)
clearDouble = pOptions.getInt("GuiProjectDetails", "clearDouble", True)
wordsHelp = (
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.
"""
self._theToC = []
self._theToC = self.theIndex.getTableOfContents(2)
self._theToC = self.theProject.index.getTableOfContents(2)
self._theToC.append(("", 0, self.tr("END"), 0))
return
+113 -78
View File
@@ -35,6 +35,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter.enum import nwAlert
from novelwriter.common import simplified
from novelwriter.gui.custom import QSwitch, PagedDialog, QConfigLayout
logger = logging.getLogger(__name__)
@@ -51,19 +52,19 @@ class GuiProjectSettings(PagedDialog):
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.theProject.countStatus()
self.setWindowTitle(self.tr("Project Settings"))
wW = self.mainConf.pxInt(570)
wH = self.mainConf.pxInt(375)
pOptions = self.theProject.options
self.setMinimumWidth(wW)
self.setMinimumHeight(wH)
self.resize(
self.mainConf.pxInt(self.optState.getInt("GuiProjectSettings", "winWidth", wW)),
self.mainConf.pxInt(self.optState.getInt("GuiProjectSettings", "winHeight", wH))
self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winWidth", wW)),
self.mainConf.pxInt(pOptions.getInt("GuiProjectSettings", "winHeight", wH))
)
self.tabMain = GuiProjectEditMain(self.theParent, self.theProject)
@@ -81,6 +82,9 @@ class GuiProjectSettings(PagedDialog):
self.buttonBox.rejected.connect(self._doClose)
self.addControls(self.buttonBox)
# Flags
self.spellChanged = False
logger.debug("GuiProjectSettings initialisation complete")
return
@@ -103,16 +107,18 @@ class GuiProjectSettings(PagedDialog):
self.theProject.setProjectName(projName)
self.theProject.setBookTitle(bookTitle)
self.theProject.setBookAuthors(bookAuthors)
self.theProject.setSpellLang(spellLang)
self.theProject.setProjBackup(doBackup)
# Remember this as updating spell dictionary can be expensive
self.spellChanged = self.theProject.setSpellLang(spellLang)
if self.tabStatus.colChanged:
statusCol = self.tabStatus.getNewList()
self.theProject.setStatusColours(statusCol)
newList, delList = self.tabStatus.getNewList()
self.theProject.setStatusColours(newList, delList)
if self.tabImport.colChanged:
importCol = self.tabImport.getNewList()
self.theProject.setImportColours(importCol)
newList, delList = self.tabImport.getNewList()
self.theProject.setImportColours(newList, delList)
if self.tabStatus.colChanged or self.tabImport.colChanged:
self.theParent.rebuildTrees()
@@ -146,11 +152,12 @@ class GuiProjectSettings(PagedDialog):
statusColW = self.mainConf.rpxInt(self.tabStatus.listBox.columnWidth(0))
importColW = self.mainConf.rpxInt(self.tabImport.listBox.columnWidth(0))
self.optState.setValue("GuiProjectSettings", "winWidth", winWidth)
self.optState.setValue("GuiProjectSettings", "winHeight", winHeight)
self.optState.setValue("GuiProjectSettings", "replaceColW", replaceColW)
self.optState.setValue("GuiProjectSettings", "statusColW", statusColW)
self.optState.setValue("GuiProjectSettings", "importColW", importColW)
pOptions = self.theProject.options
pOptions.setValue("GuiProjectSettings", "winWidth", winWidth)
pOptions.setValue("GuiProjectSettings", "winHeight", winHeight)
pOptions.setValue("GuiProjectSettings", "replaceColW", replaceColW)
pOptions.setValue("GuiProjectSettings", "statusColW", statusColW)
pOptions.setValue("GuiProjectSettings", "importColW", importColW)
return
@@ -181,7 +188,7 @@ class GuiProjectEditMain(QWidget):
self.editName.setMaximumWidth(xW)
self.editName.setText(self.theProject.projName)
self.mainForm.addRow(
self.tr("Working title"),
self.tr("Project name"),
self.editName,
self.tr("Should be set only once.")
)
@@ -245,13 +252,16 @@ class GuiProjectEditStatus(QWidget):
COL_LABEL = 0
COL_USAGE = 1
KEY_ROLE = Qt.UserRole
COL_ROLE = Qt.UserRole + 1
NUM_ROLE = Qt.UserRole + 2
def __init__(self, theParent, theProject, isStatus):
QWidget.__init__(self, theParent)
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theProject
self.optState = theProject.optState
self.theTheme = theParent.theTheme
if isStatus:
@@ -264,13 +274,12 @@ class GuiProjectEditStatus(QWidget):
colSetting = "importColW"
wCol0 = self.mainConf.pxInt(
self.optState.getInt("GuiProjectSettings", colSetting, 130)
self.theProject.options.getInt("GuiProjectSettings", colSetting, 130)
)
self.colData = []
self.colCounts = []
self.colDeleted = []
self.colChanged = False
self.selColour = None
self.selColour = QColor(100, 100, 100)
self.iPx = self.theTheme.baseIconSize
@@ -285,8 +294,8 @@ class GuiProjectEditStatus(QWidget):
self.listBox.setColumnWidth(self.COL_LABEL, wCol0)
self.listBox.setIndentation(0)
for iName, iCol, nUse, _ in self.theStatus:
self._addItem(iName, iCol, iName, nUse)
for key, entry in self.theStatus.items():
self._addItem(key, entry["name"], entry["cols"], entry["count"])
# List Controls
# =============
@@ -297,6 +306,12 @@ class GuiProjectEditStatus(QWidget):
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
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
# =========
@@ -306,7 +321,7 @@ class GuiProjectEditStatus(QWidget):
self.editName.setPlaceholderText(self.tr("Select item to edit"))
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.setIconSize(self.colPixmap.rect().size())
self.colButton.clicked.connect(self._selectColour)
@@ -320,6 +335,8 @@ class GuiProjectEditStatus(QWidget):
self.listControls = QVBoxLayout()
self.listControls.addWidget(self.addButton)
self.listControls.addWidget(self.delButton)
self.listControls.addWidget(self.upButton)
self.listControls.addWidget(self.dnButton)
self.listControls.addStretch(1)
self.editBox = QHBoxLayout()
@@ -349,12 +366,15 @@ class GuiProjectEditStatus(QWidget):
if self.colChanged:
newList = []
for n in range(self.listBox.topLevelItemCount()):
nItem = self.listBox.topLevelItem(n)
nIdx = nItem.data(self.COL_LABEL, Qt.UserRole)
newList.append(self.colData[nIdx])
return newList
item = self.listBox.topLevelItem(n)
newList.append({
"key": item.data(self.COL_LABEL, self.KEY_ROLE),
"name": item.text(self.COL_LABEL),
"cols": item.data(self.COL_LABEL, self.COL_ROLE),
})
return newList, self.colDeleted
return None
return [], []
##
# User Actions
@@ -369,16 +389,16 @@ class GuiProjectEditStatus(QWidget):
)
if newCol.isValid():
self.selColour = newCol
colPixmap = QPixmap(self.iPx, self.iPx)
colPixmap.fill(newCol)
self.colButton.setIcon(QIcon(colPixmap))
self.colButton.setIconSize(colPixmap.rect().size())
pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(newCol)
self.colButton.setIcon(QIcon(pixmap))
self.colButton.setIconSize(pixmap.rect().size())
return
def _newItem(self):
"""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_USAGE, QBrush(QColor(0, 255, 0, 70)))
self.colChanged = True
@@ -390,14 +410,14 @@ class GuiProjectEditStatus(QWidget):
selItem = self._getSelectedItem()
if selItem is not None:
iRow = self.listBox.indexOfTopLevelItem(selItem)
selIdx = selItem.data(self.COL_LABEL, Qt.UserRole)
if self.colCounts[selIdx] == 0:
self.listBox.takeTopLevelItem(iRow)
self.colChanged = True
else:
if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0:
self.theParent.makeAlert(self.tr(
"Cannot delete a status item that is in use."
), nwAlert.ERROR)
else:
self.listBox.takeTopLevelItem(iRow)
self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE))
self.colChanged = True
return
def _saveItem(self):
@@ -405,53 +425,75 @@ class GuiProjectEditStatus(QWidget):
"""
selItem = self._getSelectedItem()
if selItem is not None:
selIdx = selItem.data(self.COL_LABEL, Qt.UserRole)
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.setText(self.COL_LABEL, simplified(self.editName.text()))
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.colChanged = True
return
def _addItem(self, iName, iCol, oName, nUse):
def _addItem(self, key, name, cols, count):
"""Add a status item to the list.
"""
newIcon = QPixmap(self.iPx, self.iPx)
newIcon.fill(QColor(*iCol))
newItem = QTreeWidgetItem()
newItem.setText(self.COL_LABEL, iName)
newItem.setText(self.COL_USAGE, self._usageString(nUse))
newItem.setIcon(self.COL_LABEL, QIcon(newIcon))
newItem.setData(self.COL_LABEL, Qt.UserRole, len(self.colData))
self.listBox.addTopLevelItem(newItem)
self.colData.append((iName, iCol[0], iCol[1], iCol[2], oName))
self.colCounts.append(nUse)
return newItem
pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(QColor(*cols))
item = QTreeWidgetItem()
item.setText(self.COL_LABEL, name)
item.setIcon(self.COL_LABEL, QIcon(pixmap))
item.setData(self.COL_LABEL, self.KEY_ROLE, key)
item.setData(self.COL_LABEL, self.COL_ROLE, cols)
item.setData(self.COL_LABEL, self.NUM_ROLE, count)
item.setText(self.COL_USAGE, self._usageString(count))
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):
"""Extract the info of a selected item and populate the settings
boxes and button.
"""
selItem = self._getSelectedItem()
if selItem is not None:
selIdx = selItem.data(self.COL_LABEL, Qt.UserRole)
selVal = self.colData[selIdx]
self.selColour = QColor(selVal[1], selVal[2], selVal[3])
newIcon = QPixmap(self.iPx, self.iPx)
newIcon.fill(self.selColour)
self.editName.setText(selVal[0])
self.colButton.setIcon(QIcon(newIcon))
self.editName.setEnabled(True)
self.editName.selectAll()
self.editName.setFocus()
if selItem is None:
return
cols = selItem.data(self.COL_LABEL, self.COL_ROLE)
name = selItem.text(self.COL_LABEL)
pixmap = QPixmap(self.iPx, self.iPx)
pixmap.fill(QColor(*cols))
self.selColour = QColor(*cols)
self.editName.setText(name)
self.colButton.setIcon(QIcon(pixmap))
self.editName.setEnabled(True)
self.editName.selectAll()
self.editName.setFocus()
return
@@ -467,12 +509,6 @@ class GuiProjectEditStatus(QWidget):
return selItem[0]
return None
def _rowsMoved(self):
"""A row has been moved, so set the changed flag.
"""
self.colChanged = True
return
def _usageString(self, nUse):
"""Generate usage string.
"""
@@ -498,11 +534,10 @@ class GuiProjectEditReplace(QWidget):
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theProject
self.optState = theProject.optState
self.arChanged = False
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")
+6 -5
View File
@@ -52,19 +52,19 @@ class GuiWordList(QDialog):
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.setWindowTitle(self.tr("Project Word List"))
mS = self.mainConf.pxInt(250)
wW = self.mainConf.pxInt(320)
wH = self.mainConf.pxInt(340)
pOptions = self.theProject.options
self.setMinimumWidth(mS)
self.setMinimumHeight(mS)
self.resize(
self.mainConf.pxInt(self.optState.getInt("GuiWordList", "winWidth", wW)),
self.mainConf.pxInt(self.optState.getInt("GuiWordList", "winHeight", wH))
self.mainConf.pxInt(pOptions.getInt("GuiWordList", "winWidth", wW)),
self.mainConf.pxInt(pOptions.getInt("GuiWordList", "winHeight", wH))
)
# Main Widgets
@@ -207,8 +207,9 @@ class GuiWordList(QDialog):
winWidth = self.mainConf.rpxInt(self.width())
winHeight = self.mainConf.rpxInt(self.height())
self.optState.setValue("GuiWordList", "winWidth", winWidth)
self.optState.setValue("GuiWordList", "winHeight", winHeight)
pOptions = self.theProject.options
pOptions.setValue("GuiWordList", "winWidth", winWidth)
pOptions.setValue("GuiWordList", "winHeight", winHeight)
return
+18 -1
View File
@@ -32,7 +32,6 @@ class nwItemType(Enum):
ROOT = 1
FOLDER = 2
FILE = 3
TRASH = 4
# END Enum nwItemType
@@ -63,6 +62,14 @@ class nwItemLayout(Enum):
# END Enum nwItemLayout
class nwDocMode(Enum):
VIEW = 0
EDIT = 1
# END Enum nwDocMode
class nwDocAction(Enum):
NO_ACTION = 0
@@ -131,6 +138,16 @@ class nwState(Enum):
# END Enum nwState
class nwView(Enum):
EDITOR = 0
PROJECT = 1
NOVEL = 2
OUTLINE = 3
# END Enum nwView
class nwWidget(Enum):
TREE = 1
+2
View File
@@ -87,6 +87,8 @@ class NWErrorMessage(QDialog):
self.mainBox.addWidget(self.btnBox, 2, 0, 1, 2)
self.mainBox.setSpacing(16)
# Pick a random window title from a set of error messages by
# Hex, the computer, from Discworld
self.setWindowTitle([
"+++ Out of Cheese 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.noveltree import GuiNovelTree
from novelwriter.gui.outline import GuiOutline
from novelwriter.gui.outlinedetails import GuiOutlineDetails
from novelwriter.gui.projtree import GuiProjectTree
from novelwriter.gui.statusbar import GuiMainStatus
from novelwriter.gui.theme import GuiTheme
from novelwriter.gui.viewsbar import GuiViewsBar
__all__ = [
"GuiDocEditor",
@@ -39,7 +39,7 @@ __all__ = [
"GuiMainStatus",
"GuiNovelTree",
"GuiOutline",
"GuiOutlineDetails",
"GuiProjectTree",
"GuiTheme",
"GuiViewsBar",
]
+5 -5
View File
@@ -409,10 +409,10 @@ class PagedDialog(QDialog):
return
def addTab(self, tabWidget, tabLabel):
def addTab(self, widget, label):
"""Forwards the adding of tabs to the QTabWidget.
"""
self._tabBox.addTab(tabWidget, tabLabel)
self._tabBox.addTab(widget, label)
return
def addControls(self, buttonBar):
@@ -431,15 +431,15 @@ class VerticalTabBar(QTabBar):
self._mW = novelwriter.CONFIG.pxInt(150)
return
def tabSizeHint(self, theIndex):
def tabSizeHint(self, index):
"""Returns a transposed size hint for the rotated bar.
"""
tSize = QTabBar.tabSizeHint(self, theIndex)
tSize = QTabBar.tabSizeHint(self, index)
tSize.transpose()
tSize.setWidth(min(tSize.width(), self._mW))
return tSize
def paintEvent(self, theEvent):
def paintEvent(self, event):
"""Custom implementation of the label painter that rotates the
label 90 degrees.
"""
+32 -26
View File
@@ -33,6 +33,7 @@ import bisect
import logging
import novelwriter
from enum import Enum
from time import time
from PyQt5.QtCore import (
@@ -50,7 +51,7 @@ from PyQt5.QtWidgets import (
)
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.constants import nwConst, nwKeyWords, nwUnicode
from novelwriter.gui.dochighlight import GuiDocHighlighter
@@ -69,6 +70,7 @@ class GuiDocEditor(QTextEdit):
spellDictionaryChanged = pyqtSignal(str, str)
docEditedStatusChanged = pyqtSignal(bool)
docCountsChanged = pyqtSignal(str, int, int, int)
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, theParent):
QTextEdit.__init__(self, theParent)
@@ -79,7 +81,6 @@ class GuiDocEditor(QTextEdit):
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theIndex = theParent.theIndex
self.theProject = theParent.theProject
self._nwDocument = None
@@ -131,8 +132,9 @@ class GuiDocEditor(QTextEdit):
# Editor Settings
self.setMinimumWidth(self.mainConf.pxInt(300))
self.setAutoFillBackground(True)
self.setAcceptRichText(False)
self.setAutoFillBackground(True)
self.setFrameStyle(QFrame.NoFrame)
# Custom Shortcuts
QShortcut(
@@ -400,7 +402,7 @@ class GuiDocEditor(QTextEdit):
self.document().rootFrame().setFrameFormat(docFrame)
self.docFooter.updateLineCount()
self._docHeaders = self.theIndex.getHandleHeaders(self._docHandle)
self._docHeaders = self.theProject.index.getHandleHeaders(self._docHandle)
qApp.processEvents()
self.document().clearUndoRedoStacks()
@@ -505,9 +507,9 @@ class GuiDocEditor(QTextEdit):
self.setDocumentChanged(False)
oldHeader = self.theIndex.getHandleHeaderLevel(tHandle)
self.theIndex.scanText(tHandle, docText)
newHeader = self.theIndex.getHandleHeaderLevel(tHandle)
oldHeader = self.theProject.index.getHandleHeaderLevel(tHandle)
self.theProject.index.scanText(tHandle, docText)
newHeader = self.theProject.index.getHandleHeaderLevel(tHandle)
if self._updateHeaders(checkLevel=True):
self.theParent.requestNovelTreeRefresh()
@@ -567,16 +569,6 @@ class GuiDocEditor(QTextEdit):
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
##
@@ -1066,7 +1058,22 @@ class GuiDocEditor(QTextEdit):
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)
@@ -1894,7 +1901,7 @@ class GuiDocEditor(QTextEdit):
if loadTag:
logger.verbose("Attempting to follow tag '%s'", theWord)
self.theParent.docViewer.loadFromTag(theWord)
self.loadDocumentTagRequest.emit(theWord, nwDocMode.VIEW)
else:
logger.verbose("Potential tag '%s'", theWord)
@@ -2002,7 +2009,7 @@ class GuiDocEditor(QTextEdit):
if self._docHandle is None:
return False
newHeaders = self.theIndex.getHandleHeaders(self._docHandle)
newHeaders = self.theProject.index.getHandleHeaders(self._docHandle)
if checkPos:
newPos = [x[0] for x in newHeaders]
oldPos = [x[0] for x in self._docHeaders]
@@ -2701,15 +2708,15 @@ class GuiDocEditHeader(QWidget):
if self.mainConf.showFullPath:
tTitle = []
tTree = self.theProject.projTree.getItemPath(tHandle)
tTree = self.theProject.tree.getItemPath(tHandle)
for aHandle in reversed(tTree):
nwItem = self.theProject.projTree[aHandle]
nwItem = self.theProject.tree[aHandle]
if nwItem is not None:
tTitle.append(nwItem.itemName)
sSep = " %s " % nwUnicode.U_RSAQUO
self.theTitle.setText(sSep.join(tTitle))
else:
nwItem = self.theProject.projTree[tHandle]
nwItem = self.theProject.tree[tHandle]
if nwItem is None:
return False
self.theTitle.setText(nwItem.itemName)
@@ -2795,7 +2802,6 @@ class GuiDocEditFooter(QWidget):
self.theParent = docEditor.theParent
self.theProject = docEditor.theProject
self.theTheme = docEditor.theTheme
self.optState = docEditor.theProject.optState
self._theItem = None
self._docHandle = None
@@ -2918,7 +2924,7 @@ class GuiDocEditFooter(QWidget):
logger.verbose("No handle set, so clearing the editor footer")
self._theItem = None
else:
self._theItem = self.theProject.projTree[self._docHandle]
self._theItem = self.theProject.tree[self._docHandle]
self.setHasSelection(False)
self.updateInfo()
@@ -2942,7 +2948,7 @@ class GuiDocEditFooter(QWidget):
else:
theStatus, theIcon = self._theItem.getImportStatus()
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)}"
self.statusIcon.setPixmap(sIcon)
+5 -4
View File
@@ -55,7 +55,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.spEnchant = spEnchant
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theIndex = theParent.theIndex
self.theProject = theParent.theProject
self.theHandle = None
self.spellCheck = False
self.spellRx = None
@@ -287,9 +287,10 @@ class GuiDocHighlighter(QSyntaxHighlighter):
if theText.startswith("@"): # Keywords and commands
self.setCurrentBlockState(self.BLOCK_META)
tItem = self.theParent.theProject.projTree[self.theHandle]
isValid, theBits, thePos = self.theIndex.scanThis(theText)
isGood = self.theIndex.checkThese(theBits, tItem)
pIndex = self.theProject.index
tItem = self.theParent.theProject.tree[self.theHandle]
isValid, theBits, thePos = pIndex.scanThis(theText)
isGood = pIndex.checkThese(theBits, tItem)
if isValid:
for n, theBit in enumerate(theBits):
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 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 (
QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor
)
from PyQt5.QtWidgets import (
qApp, QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton,
QAction, QMenu
QAction, QMenu, QFrame
)
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.constants import nwUnicode
@@ -49,6 +51,8 @@ logger = logging.getLogger(__name__)
class GuiDocViewer(QTextBrowser):
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, theParent):
QTextBrowser.__init__(self, theParent)
@@ -68,6 +72,7 @@ class GuiDocViewer(QTextBrowser):
self.setAutoFillBackground(True)
self.setOpenExternalLinks(False)
self.setFocusPolicy(Qt.StrongFocus)
self.setFrameStyle(QFrame.NoFrame)
# Document Header and Footer
self.docHeader = GuiDocViewHeader(self)
@@ -159,7 +164,7 @@ class GuiDocViewer(QTextBrowser):
def loadText(self, tHandle, updateHistory=True):
"""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")
return False
@@ -238,30 +243,6 @@ class GuiDocViewer(QTextBrowser):
self.updateDocMargins()
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):
"""Wrapper function for various document actions on the current
document.
@@ -341,15 +322,6 @@ class GuiDocViewer(QTextBrowser):
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
##
@@ -408,19 +380,33 @@ class GuiDocViewer(QTextBrowser):
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")
def _linkClicked(self, theURL):
"""Slot for a link in the document being clicked.
"""Process a clicked link internally in the document.
"""
theLink = theURL.url()
logger.verbose("Clicked link: '%s'", theLink)
if len(theLink) > 0:
theBits = theLink.split("=")
if len(theBits) == 2:
self.loadFromTag(theBits[1])
self.loadDocumentTagRequest.emit(theBits[1], nwDocMode.VIEW)
return
@pyqtSlot("QPoint")
@@ -862,15 +848,15 @@ class GuiDocViewHeader(QWidget):
if self.mainConf.showFullPath:
tTitle = []
tTree = self.theProject.projTree.getItemPath(tHandle)
tTree = self.theProject.tree.getItemPath(tHandle)
for aHandle in reversed(tTree):
nwItem = self.theProject.projTree[aHandle]
nwItem = self.theProject.tree[aHandle]
if nwItem is not None:
tTitle.append(nwItem.itemName)
sSep = " %s " % nwUnicode.U_RSAQUO
self.theTitle.setText(sSep.join(tTitle))
else:
nwItem = self.theProject.projTree[tHandle]
nwItem = self.theProject.tree[tHandle]
if nwItem is None:
return False
self.theTitle.setText(nwItem.itemName)
@@ -1185,6 +1171,7 @@ class GuiDocViewDetails(QScrollArea):
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setWidgetResizable(True)
self.setMinimumHeight(self.mainConf.pxInt(50))
self.setFrameStyle(QFrame.NoFrame)
logger.debug("GuiDocViewDetails initialisation complete")
@@ -1197,10 +1184,10 @@ class GuiDocViewDetails(QScrollArea):
if self.theParent.docViewer.stickyRef:
return
theRefs = self.theParent.theIndex.getBackReferenceList(tHandle)
theRefs = self.theProject.index.getBackReferenceList(tHandle)
theList = []
for tHandle in theRefs:
tItem = self.theProject.projTree[tHandle]
tItem = self.theProject.tree[tHandle]
if tItem is not None:
theList.append("<a href='%s#%s' %s>%s</a>" % (
tHandle, theRefs[tHandle], self.linkStyle, tItem.itemName
+13 -6
View File
@@ -115,6 +115,7 @@ class GuiItemDetails(QWidget):
self.usageData = QLabel("")
self.usageData.setFont(fntValue)
self.usageData.setAlignment(Qt.AlignLeft)
self.usageData.setWordWrap(True)
# Character Count
self.cCountName = QLabel(" "+self.tr("Characters"))
@@ -214,6 +215,16 @@ class GuiItemDetails(QWidget):
return
def refreshDetails(self):
"""Reload the content of the details panel.
"""
self.updateViewBox(self._itemHandle)
##
# Public Slots
##
@pyqtSlot(str)
def updateViewBox(self, tHandle):
"""Populate the details box from a given handle.
"""
@@ -221,7 +232,7 @@ class GuiItemDetails(QWidget):
self.clearDetails()
return
nwItem = self.theProject.projTree[tHandle]
nwItem = self.theProject.tree[tHandle]
if nwItem is None:
self.clearDetails()
return
@@ -263,7 +274,7 @@ class GuiItemDetails(QWidget):
# Layout
# ======
hLevel = self.theParent.theIndex.getHandleHeaderLevel(tHandle)
hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
usageIcon = self.theTheme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
)
@@ -284,10 +295,6 @@ class GuiItemDetails(QWidget):
return
##
# Slots
##
@pyqtSlot(str, int, int, int)
def doUpdateCounts(self, tHandle, cC, wC, pC):
"""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
##
# 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
##
@@ -99,12 +81,6 @@ class GuiMainMenu(QMenuBar):
self.aSpellCheck.setChecked(theMode)
return
def setAutoOutline(self, theMode):
"""Forward auto outline check state to its action.
"""
self.aAutoOutline.setChecked(theMode)
return
def setFocusMode(self, theMode):
"""Forward focus mode check state to its action.
"""
@@ -123,12 +99,6 @@ class GuiMainMenu(QMenuBar):
self.theParent.docEditor.toggleSpellCheck(None)
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):
"""Open a URL in the system's default browser.
"""
@@ -215,7 +185,7 @@ class GuiMainMenu(QMenuBar):
# Project > New Folder
self.aCreateFolder = QAction(self.tr("Create Folder"), self)
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)
# Project > Separator
@@ -277,7 +247,7 @@ class GuiMainMenu(QMenuBar):
# Document > New
self.aNewDoc = QAction(self.tr("New Document"), self)
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)
# Document > Open
@@ -907,19 +877,6 @@ class GuiMainMenu(QMenuBar):
self.aRebuildIndex.triggered.connect(lambda: self.theParent.rebuildIndex())
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
self.toolsMenu.addSeparator()
+12 -11
View File
@@ -29,7 +29,9 @@ import novelwriter
from time import time
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.constants import nwKeyWords
@@ -52,7 +54,6 @@ class GuiNovelTree(QTreeWidget):
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theParent.theProject
self.theIndex = theParent.theIndex
# Internal Variables
self._treeMap = {}
@@ -60,6 +61,7 @@ class GuiNovelTree(QTreeWidget):
# Build GUI
iPx = self.theTheme.baseIconSize
self.setFrameStyle(QFrame.NoFrame)
self.setIconSize(QSize(iPx, iPx))
self.setIndentation(iPx)
self.setColumnCount(3)
@@ -134,7 +136,7 @@ class GuiNovelTree(QTreeWidget):
"""
logger.verbose("Requesting refresh of the novel tree")
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):
logger.verbose("No changes have been made to the novel index")
return
@@ -144,7 +146,6 @@ class GuiNovelTree(QTreeWidget):
if selItem:
titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2]
self.theParent.treeView.flushTreeOrder()
self._populateTree()
if titleKey is not None and titleKey in self._treeMap:
@@ -155,7 +156,7 @@ class GuiNovelTree(QTreeWidget):
def updateWordCounts(self, tHandle):
"""Update the word count for a given handle.
"""
tHeaders = self.theIndex.getHandleWordCounts(tHandle)
tHeaders = self.theProject.index.getHandleWordCounts(tHandle)
for titleKey, wCount in tHeaders:
if titleKey in self._treeMap:
self._treeMap[titleKey].setText(self.C_WORDS, f"{wCount:n}")
@@ -249,12 +250,12 @@ class GuiNovelTree(QTreeWidget):
currChapter = 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)
self._treeMap[tKey] = tItem
tLevel = novIdx["level"]
tLevel = novIdx.level
if tLevel == "H1":
self.addTopLevelItem(tItem)
currTitle = tItem
@@ -301,18 +302,18 @@ class GuiNovelTree(QTreeWidget):
"""Populate a tree item with all the column values.
"""
newItem = QTreeWidgetItem()
hIcon = "doc_%s" % novIdx["level"].lower()
hIcon = "doc_%s" % novIdx.level.lower()
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.setIcon(self.C_TITLE, self.theTheme.getIcon(hIcon))
newItem.setText(self.C_WORDS, f"{wC:n}")
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]))
return newItem
+626 -66
View File
@@ -4,7 +4,11 @@ novelWriter GUI Project Outline
GUI class for the project outline view
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
Copyright 20182022, Veronica Berglyd Olsen
@@ -27,20 +31,244 @@ import logging
import novelwriter
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 (
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.constants import trConst, nwKeyWords, nwLabels
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 = {
nwOutline.TITLE: 200,
@@ -82,19 +310,20 @@ class GuiOutline(QTreeWidget):
nwOutline.SYNOP: False,
}
def __init__(self, theParent):
QTreeWidget.__init__(self, theParent)
hiddenStateChanged = pyqtSignal()
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.theParent = theParent
self.theProject = theParent.theProject
self.theTheme = theParent.theTheme
self.theIndex = theParent.theIndex
self.optState = theParent.theProject.optState
self.headerMenu = GuiOutlineHeaderMenu(self)
self.theParent = theOutline.theParent
self.theProject = theOutline.theParent.theProject
self.theTheme = theOutline.theParent.theTheme
self.setFrameStyle(QFrame.NoFrame)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setExpandsOnDoubleClick(False)
@@ -107,8 +336,6 @@ class GuiOutline(QTreeWidget):
self.setIndentation(iPx)
self.treeHead = self.header()
self.treeHead.setContextMenuPolicy(Qt.CustomContextMenu)
self.treeHead.customContextMenuRequested.connect(self._headerRightClick)
self.treeHead.sectionMoved.connect(self._columnMoved)
# Internals
@@ -122,12 +349,25 @@ class GuiOutline(QTreeWidget):
self.initOutline()
self.clearOutline()
self.headerMenu.setHiddenState(self._colHidden)
logger.debug("GuiOutline initialisation complete")
self.hiddenStateChanged.emit()
logger.debug("GuiOutlineView initialisation complete")
return
##
# Properties
##
@property
def hiddenColumns(self):
return self._colHidden
##
# Methods
##
def initOutline(self):
"""Set or update outline settings.
"""
@@ -167,7 +407,7 @@ class GuiOutline(QTreeWidget):
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
what data to load, and if necessary, force a rebuild of the
tree.
@@ -175,17 +415,17 @@ class GuiOutline(QTreeWidget):
# If it's the first time, we always build
if self._firstView or self._firstView and overRide:
self._loadHeaderState()
self._populateTree()
self._populateTree(rootHandle)
self._firstView = False
return
# If the novel index or novel tree has changed since the tree
# was last built, we rebuild the tree from the updated index.
indexChanged = self.theIndex.novelChangedSince(self._lastBuild)
indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline
if doBuild or overRide:
logger.debug("Rebuilding Project Outline")
self._populateTree()
self._populateTree(rootHandle)
return
@@ -221,7 +461,7 @@ class GuiOutline(QTreeWidget):
document editor.
"""
tHandle, tLine = self.getSelectedHandle()
self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True)
self.theParent.openDocument(tHandle, tLine=tLine - 1, doScroll=True)
return
@pyqtSlot()
@@ -233,18 +473,10 @@ class GuiOutline(QTreeWidget):
if selItems:
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole)
self.theParent.projMeta.showItem(tHandle, sTitle)
self.theParent.treeView.setSelectedHandle(tHandle)
self.activeItemChanged.emit(tHandle, sTitle)
return
@pyqtSlot("QPoint")
def _headerRightClick(self, clickPos):
"""Show the header column menu.
"""
self.headerMenu.exec_(self.mapToGlobal(clickPos))
return
@pyqtSlot(int, int, int)
def _columnMoved(self, logIdx, oldVisualIdx, newVisualIdx):
"""Make sure the order array is up to date with the actual order
@@ -254,9 +486,10 @@ class GuiOutline(QTreeWidget):
self._saveHeaderState()
return
def _menuColumnToggled(self, isChecked, theItem):
@pyqtSlot(bool, Enum)
def menuColumnToggled(self, isChecked, theItem):
"""Receive the changes to column visibility forwarded by the
header context menu.
column selection menu.
"""
logger.verbose("User toggled Outline column '%s'", theItem.name)
if theItem in self._colIdx:
@@ -273,10 +506,12 @@ class GuiOutline(QTreeWidget):
"""Load the state of the main tree header, that is, column order
and column width.
"""
pOptions = self.theProject.options
# Load whatever we saved last time, regardless of wether it
# contains the correct names or number of columns. The names
# must be valid though.
tempOrder = self.optState.getValue("GuiOutline", "headerOrder", [])
tempOrder = pOptions.getValue("GuiOutline", "headerOrder", [])
treeOrder = []
for hName in tempOrder:
try:
@@ -299,21 +534,21 @@ class GuiOutline(QTreeWidget):
# We load whatever column widths and hidden states we find in
# 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:
try:
self._colWidth[nwOutline[hName]] = self.mainConf.pxInt(tmpWidth[hName])
except Exception:
logger.warning("Ignored unknown outline column '%s'", str(hName))
tmpHidden = self.optState.getValue("GuiOutline", "columnHidden", {})
tmpHidden = pOptions.getValue("GuiOutline", "columnHidden", {})
for hName in tmpHidden:
try:
self._colHidden[nwOutline[hName]] = tmpHidden[hName]
except Exception:
logger.warning("Ignored unknown outline column '%s'", str(hName))
self.headerMenu.setHiddenState(self._colHidden)
self.hiddenStateChanged.emit()
return
@@ -347,14 +582,15 @@ class GuiOutline(QTreeWidget):
if not logHidden and logWidth > 0:
colWidth[hName] = logWidth
self.optState.setValue("GuiOutline", "headerOrder", treeOrder)
self.optState.setValue("GuiOutline", "columnWidth", colWidth)
self.optState.setValue("GuiOutline", "columnHidden", colHidden)
self.optState.saveSettings()
pOptions = self.theProject.options
pOptions.setValue("GuiOutline", "headerOrder", treeOrder)
pOptions.setValue("GuiOutline", "columnWidth", colWidth)
pOptions.setValue("GuiOutline", "columnHidden", colHidden)
pOptions.saveSettings()
return
def _populateTree(self):
def _populateTree(self, rootHandle):
"""Build the tree based on the project index, and the header
based on the defined constants, default values and user selected
width, order and hidden state. All columns are populated, even
@@ -387,11 +623,12 @@ class GuiOutline(QTreeWidget):
currChapter = 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)
tLevel = novIdx["level"]
tLevel = novIdx.level
if tLevel == "H1":
self.addTopLevelItem(tItem)
currTitle = tItem
@@ -437,26 +674,26 @@ class GuiOutline(QTreeWidget):
def _createTreeItem(self, tHandle, sTitle, novIdx):
"""Populate a tree item with all the column values.
"""
nwItem = self.theProject.projTree[tHandle]
nwItem = self.theProject.tree[tHandle]
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)
cC = int(novIdx["cCount"])
wC = int(novIdx["wCount"])
pC = int(novIdx["pCount"])
cC = int(novIdx.charCount)
wC = int(novIdx.wordCount)
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.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.setIcon(self._colIdx[nwOutline.LABEL], dIcon)
newItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0"))
newItem.setData(self._colIdx[nwOutline.LINE], Qt.UserRole, sTitle)
newItem.setText(self._colIdx[nwOutline.SYNOP], novIdx["synopsis"])
newItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis)
newItem.setText(self._colIdx[nwOutline.CCOUNT], f"{cC:n}")
newItem.setText(self._colIdx[nwOutline.WCOUNT], f"{wC:n}")
newItem.setText(self._colIdx[nwOutline.PCOUNT], f"{pC:n}")
@@ -464,7 +701,7 @@ class GuiOutline(QTreeWidget):
newItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], 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.FOCUS], ", ".join(theRefs[nwKeyWords.FOCUS_KEY]))
newItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY]))
@@ -477,15 +714,16 @@ class GuiOutline(QTreeWidget):
return newItem
# END Class GuiOutline
# END Class GuiOutlineView
class GuiOutlineHeaderMenu(QMenu):
def __init__(self, theParent):
QMenu.__init__(self, theParent)
columnToggled = pyqtSignal(bool, Enum)
def __init__(self, theOutline):
QMenu.__init__(self, theOutline)
self.theParent = theParent
self.acceptToggle = True
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].setCheckable(True)
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])
@@ -520,16 +758,338 @@ class GuiOutlineHeaderMenu(QMenu):
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
##
def _columnToggled(self, isChecked, theItem):
"""The user has toggled the visibility of a column. Forward the
event to the parent class only if we're accepting changes.
@pyqtSlot(str, str)
def showItem(self, tHandle, sTitle):
"""Update the content of the tree with the given handle and line
number pointing to a header.
"""
if self.acceptToggle:
self.theParent._menuColumnToggled(isChecked, theItem)
pIndex = self.theProject.index
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
# 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
+289 -354
View File
@@ -32,12 +32,14 @@ from time import time
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction, QFrame,
QDialog
)
from novelwriter.core import NWDoc
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__)
@@ -49,8 +51,9 @@ class GuiProjectTree(QTreeWidget):
C_EXPORT = 2
C_STATUS = 3
novelItemChanged = pyqtSignal()
noteItemChanged = pyqtSignal()
treeItemChanged = pyqtSignal(str)
novelItemChanged = pyqtSignal(str)
rootFolderChanged = pyqtSignal(str)
wordCountsChanged = pyqtSignal()
def __init__(self, theParent):
@@ -62,13 +65,11 @@ class GuiProjectTree(QTreeWidget):
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theParent.theProject
self.theIndex = theParent.theIndex
# Internal Variables
self._treeMap = {}
self._treeChanged = False
self._treeMap = {}
self._lastMove = {}
self._timeChanged = 0
self._lastMove = {}
##
# Build GUI
@@ -82,7 +83,8 @@ class GuiProjectTree(QTreeWidget):
# Tree Settings
iPx = self.theTheme.baseIconSize
self.setIconSize(QSize(iPx, iPx))
self.setExpandsOnDoubleClick(True)
self.setFrameStyle(QFrame.NoFrame)
self.setExpandsOnDoubleClick(False)
self.setIndentation(iPx)
self.setColumnCount(4)
self.setHeaderLabels([
@@ -157,147 +159,97 @@ class GuiProjectTree(QTreeWidget):
"""
self.clear()
self._treeMap = {}
self._treeChanged = False
self._lastMove = {}
self._timeChanged = 0
return
def newTreeItem(self, itemType, itemClass):
"""Add new item to the tree, with a given itemType and
itemClass, and attach it to the selected handle. Also make sure
the item is added in a place it can be added, and that other
meta data is set correctly to ensure a valid project tree.
def newTreeItem(self, itemType, itemClass=None):
"""Add new item to the tree, with a given itemType (and
itemClass if Root), and attach it to the selected handle. Also
make sure the item is added in a place it can be added, and that
other meta data is set correctly to ensure a valid project tree.
"""
pHandle = self.getSelectedHandle()
nHandle = None
if not self.theParent.hasProject:
logger.error("No project open")
return False
if not isinstance(itemType, nwItemType):
# This would indicate an internal bug
logger.error("No itemType provided")
return False
nHandle = None
tHandle = None
# The item needs to be assigned an item class, so one must be
# 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 itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass):
# If class is still not set, alert the user and exit
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
tHandle = self.theProject.newRoot(itemClass)
# Everything is fine, we have what we need, so we proceed
logger.verbose(
"Adding new item of type '%s' and class '%s' to handle '%s'",
itemType.name, itemClass.name, str(pHandle)
)
elif itemType in (nwItemType.FILE, nwItemType.FOLDER):
if itemType == nwItemType.ROOT:
tHandle = self.theProject.newRoot(
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:
sHandle = self.getSelectedHandle()
if sHandle is None or sHandle not in self.theProject.tree:
self.theParent.makeAlert(self.tr(
"Did not find anywhere to add the file or folder!"
), nwAlert.ERROR)
return False
# Now check if the selected item is a file, in which case
# the new file will be a sibling
pItem = self.theProject.projTree[pHandle]
if pItem.itemType == nwItemType.FILE:
nHandle = pHandle
pHandle = pItem.itemParent
# If the selected item is a file, the new item will be a
# sibling if the file has no children, otherwise a child
pItem = self.theProject.tree[sHandle]
qItem = self._getTreeItem(sHandle)
if pItem.itemType == nwItemType.FILE and qItem.childCount() == 0:
nHandle = sHandle
sHandle = pItem.itemParent
if sHandle is None:
logger.error("Internal error") # Bug
return False
# If we again have no home, give up
if pHandle is None:
self.theParent.makeAlert(self.tr(
"Did not find anywhere to add the file or folder!"
), nwAlert.ERROR)
return False
if self.theProject.projTree.isTrashRoot(pHandle):
if self.theProject.tree.isTrash(sHandle):
self.theParent.makeAlert(self.tr(
"Cannot add new files or folders to the Trash folder."
), nwAlert.ERROR)
return False
parTree = self.theProject.projTree.getItemPath(pHandle)
# If we're still here, add the file or folder
# Add the file or folder
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:
if len(parTree) >= nwConst.MAX_DEPTH - 1:
# 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)
tHandle = self.theProject.newFolder(self.tr("New Folder"), sHandle)
else:
logger.error("Failed to add new item")
return False
else:
logger.error("Failed to add new item")
return False
# If there is no handle set, return here
if tHandle is None:
# If there is no handle set, return here. This is a bug
if tHandle is None: # pragma: no cover
return True
# Add the new item to the tree
self.revealNewTreeItem(tHandle, nHandle)
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:
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)
curTxt = newDoc.readDocument()
if curTxt is None:
curTxt = ""
if curTxt == "":
if not newDoc.readDocument():
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:
newText = f"# {nwItem.itemName}\n\n"
pIndex = self.theProject.index
# Save the text and index it
newDoc.writeDocument(newText)
self.theIndex.scanText(tHandle, newText)
pIndex.scanText(tHandle, newText)
# Get Word Counts
cC, wC, pC = self.theIndex.getCounts(tHandle)
cC, wC, pC = pIndex.getCounts(tHandle)
nwItem.setCharCount(cC)
nwItem.setWordCount(wC)
nwItem.setParaCount(pC)
@@ -309,7 +261,10 @@ class GuiProjectTree(QTreeWidget):
def revealNewTreeItem(self, tHandle, nHandle=None):
"""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)
if trItem is None:
return False
@@ -318,7 +273,7 @@ class GuiProjectTree(QTreeWidget):
if pHandle is not None and pHandle in self._treeMap:
self._treeMap[pHandle].setExpanded(True)
self._emitItemChange(tHandle)
self._alertTreeChange(tHandle=tHandle, flush=True)
self.clearSelection()
trItem.setSelected(True)
@@ -360,10 +315,31 @@ class GuiProjectTree(QTreeWidget):
pItem.insertChild(nIndex, cItem)
self._recordLastMove(cItem, pItem, tIndex)
self._alertTreeChange(tHandle=tHandle, flush=True)
self.clearSelection()
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
@@ -380,16 +356,6 @@ class GuiProjectTree(QTreeWidget):
self.theProject.setTreeOrder(theList)
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):
"""Recursively return all the children items starting from a
given item handle.
@@ -400,6 +366,14 @@ class GuiProjectTree(QTreeWidget):
theList = self._scanChildren(theList, theItem, 0)
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):
"""Return the column widths for the tree columns.
"""
@@ -419,7 +393,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("No project open")
return False
trashHandle = self.theProject.projTree.trashRoot()
trashHandle = self.theProject.tree.trashRoot()
logger.debug("Emptying Trash folder")
if trashHandle is None:
@@ -447,13 +421,13 @@ class GuiProjectTree(QTreeWidget):
return False
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:
continue
self.deleteItem(tHandle, alreadyAsked=True, bulkAction=True)
if nTrash > 0:
self._setTreeChanged(True)
self._alertTreeChange(tHandle=trashHandle, flush=True)
return True
@@ -461,8 +435,8 @@ class GuiProjectTree(QTreeWidget):
"""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
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,
and the deletion is always permanent.
delete the files on disk. Root folders are deleted if they're empty
only, and the deletion is always permanent.
"""
if not self.theParent.hasProject:
logger.error("No project open")
@@ -480,109 +454,22 @@ class GuiProjectTree(QTreeWidget):
return False
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.projTree[tHandle]
nwItemS = self.theProject.tree[tHandle]
if trItemS is None or nwItemS is None:
logger.error("Could not find tree item for deletion")
return False
wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole))
if nwItemS.itemType == nwItemType.FILE:
logger.debug("User requested file '%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
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)
wCount = self._getItemWordCount(tHandle)
autoFlush = not bulkAction
if nwItemS.itemType == nwItemType.ROOT:
# Only an empty ROOT folder can be deleted
logger.debug("User requested a root folder '%s' deleted", tHandle)
tIndex = self.indexOfTopLevelItem(trItemS)
if trItemS.childCount() == 0:
self.takeTopLevelItem(tIndex)
self._deleteTreeItem(tHandle)
self.theParent.mainMenu.setAvailableRoot()
self._setTreeChanged(True)
self._alertTreeChange(tHandle=tHandle, flush=True)
else:
self.theParent.makeAlert(self.tr(
"Cannot delete root folder. It is not empty. "
@@ -591,6 +478,70 @@ class GuiProjectTree(QTreeWidget):
), nwAlert.ERROR)
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
def setTreeItemValues(self, tHandle):
@@ -599,7 +550,7 @@ class GuiProjectTree(QTreeWidget):
already coming from the project tree.
"""
trItem = self._getTreeItem(tHandle)
nwItem = self.theProject.projTree[tHandle]
nwItem = self.theProject.tree[tHandle]
if trItem is None or nwItem is None:
return
@@ -610,8 +561,8 @@ class GuiProjectTree(QTreeWidget):
else:
expIcon = self.theTheme.getIcon("cross")
itempStatus, statusIcon = nwItem.getImportStatus()
hLevel = self.theIndex.getHandleHeaderLevel(tHandle)
itemStatus, statusIcon = nwItem.getImportStatus()
hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
itemIcon = self.theTheme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
)
@@ -620,7 +571,7 @@ class GuiProjectTree(QTreeWidget):
trItem.setText(self.C_NAME, nwItem.itemName)
trItem.setIcon(self.C_EXPORT, expIcon)
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:
trFont = trItem.font(self.C_NAME)
@@ -634,7 +585,7 @@ class GuiProjectTree(QTreeWidget):
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,
and propagating that count upwards in the tree until reaching a
root item. This function is more efficient than recalculating
@@ -646,20 +597,30 @@ class GuiProjectTree(QTreeWidget):
if tItem is None:
return
tItem.setText(self.C_COUNT, f"{theCount:n}")
tItem.setData(self.C_COUNT, Qt.UserRole, int(theCount))
if countChildren:
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()
if pItem is None:
return
pCount = 0
pHandle = None
for i in range(pItem.childCount()):
pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
pHandle = pItem.data(self.C_NAME, Qt.UserRole)
if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "":
self.propagateCount(pHandle, pCount, nDepth+1)
if pHandle:
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
@@ -703,20 +664,19 @@ class GuiProjectTree(QTreeWidget):
return False
dstIndex = min(max(0, dstIndex), dstItem.childCount())
wCount = int(srcItem.data(self.C_COUNT, Qt.UserRole))
sHandle = srcItem.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)
wCount = self._getItemWordCount(sHandle)
self.propagateCount(sHandle, 0)
parItem = srcItem.parent()
srcIndex = parItem.indexOfChild(srcItem)
movItem = parItem.takeChild(srcIndex)
dstItem.insertChild(dstIndex, movItem)
snItem = self.theProject.projTree[sHandle]
dnItem = self.theProject.projTree[dHandle]
self._postItemMove(sHandle, snItem, dnItem, wCount)
self._postItemMove(sHandle, wCount)
self._alertTreeChange(tHandle=sHandle, flush=True)
self.clearSelection()
movItem.setSelected(True)
@@ -771,7 +731,7 @@ class GuiProjectTree(QTreeWidget):
if isinstance(selItem, QTreeWidgetItem):
tHandle = selItem.data(self.C_NAME, Qt.UserRole)
self.setSelectedHandle(tHandle) # Just to be safe
tItem = self.theProject.projTree[tHandle]
tItem = self.theProject.tree[tHandle]
if tItem is not None:
if self.ctxMenu.filterActions(tItem):
# Only open menu if any actions remain after filter
@@ -783,7 +743,7 @@ class GuiProjectTree(QTreeWidget):
def doUpdateCounts(self, tHandle, cCount, wCount, pCount):
"""Slot for updating the word count of a specific item.
"""
self.propagateCount(tHandle, wCount)
self.propagateCount(tHandle, wCount, countChildren=True)
self.wordCountsChanged.emit()
return
@@ -809,7 +769,7 @@ class GuiProjectTree(QTreeWidget):
return
tHandle = selItem.data(self.C_NAME, Qt.UserRole)
tItem = self.theProject.projTree[tHandle]
tItem = self.theProject.tree[tHandle]
if tItem is None:
return
@@ -819,61 +779,34 @@ class GuiProjectTree(QTreeWidget):
return
def dropEvent(self, theEvent):
"""Overload the drop of dragged item event to check whether the
drop is allowed or not. Disallowed drops are cancelled.
"""Overload the drop item event to ensure relevant data has been
updated.
"""
sHandle = self.getSelectedHandle()
if sHandle is None:
logger.error("No handle selected")
logger.error("Invalid drag and drop event")
return
dIndex = self.indexAt(theEvent.pos())
if not dIndex.isValid():
logger.error("Invalid drop index")
return
logger.debug("Drag'n'drop of item '%s' accepted", sHandle)
sItem = self._getTreeItem(sHandle)
dItem = self.itemFromIndex(dIndex)
dHandle = dItem.data(self.C_NAME, Qt.UserRole)
snItem = self.theProject.projTree[sHandle]
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
isExpanded = False
if sItem is not None:
isExpanded = sItem.isExpanded()
pItem = sItem.parent()
pIndex = 0
if pItem is not None:
pIndex = pItem.indexOfChild(sItem)
wCount = int(sItem.data(self.C_COUNT, Qt.UserRole))
isFile = snItem.itemType == nwItemType.FILE
isRoot = snItem.itemType == nwItemType.ROOT
onFile = dnItem.itemType == nwItemType.FILE
wCount = self._getItemWordCount(sHandle)
self.propagateCount(sHandle, 0)
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)
QTreeWidget.dropEvent(self, theEvent)
self._postItemMove(sHandle, snItem, dnItem, wCount)
self._recordLastMove(sItem, pItem, pIndex)
else:
theEvent.ignore()
logger.debug("Drag'n'drop of item '%s' not accepted", sHandle)
self.theParent.makeAlert(self.tr(
"The item cannot be moved to that location."
), nwAlert.ERROR)
QTreeWidget.dropEvent(self, theEvent)
self._postItemMove(sHandle, wCount)
self._recordLastMove(sItem, pItem, pIndex)
self._alertTreeChange(tHandle=sHandle, flush=True)
sItem.setExpanded(isExpanded)
return
@@ -881,64 +814,88 @@ class GuiProjectTree(QTreeWidget):
# Internal Functions
##
def _postItemMove(self, sHandle, snItem, dnItem, wCount):
def _postItemMove(self, tHandle, wCount):
"""Run various maintenance tasks for a moved item.
"""
isFile = snItem.itemType == nwItemType.FILE
isSame = snItem.itemClass == dnItem.itemClass
onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile
trItemS = self._getTreeItem(tHandle)
nwItemS = self.theProject.tree[tHandle]
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,
# and the target is not a free root folder, update its class
if not (isSame or onFree):
logger.debug(
"Item '%s' class has been changed from '%s' to '%s'",
sHandle, snItem.itemClass.name, dnItem.itemClass.name
)
snItem.setClass(dnItem.itemClass)
self.setTreeItemValues(sHandle)
mHandles = self.getTreeFromHandle(tHandle)
logger.debug("A total of %d item(s) were moved", len(mHandles))
for mHandle in mHandles:
logger.debug("Updating item '%s'", mHandle)
self.theProject.tree.updateItemData(mHandle)
self.propagateCount(sHandle, wCount)
# Update the index
if nwItemS.isInactive():
self.theProject.index.deleteHandle(mHandle)
else:
self.theProject.index.reIndexHandle(mHandle)
# The items dropped into archive or trash should be removed
# 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:
self.theIndex.reIndexHandle(sHandle)
self.setTreeItemValues(mHandle)
# Trigger dependent updates
self._setTreeChanged(True)
self._emitItemChange(sHandle)
self.propagateCount(tHandle, wCount)
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):
"""Returns the QTreeWidgetItem of a given item handle.
"""Return the QTreeWidgetItem of a given item handle.
"""
return self._treeMap.get(tHandle, None)
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)
return
return True
def _scanChildren(self, theList, tItem, tIndex):
"""This is a recursive function returning all items in a tree
starting at a given QTreeWidgetItem.
"""
tHandle = tItem.data(self.C_NAME, Qt.UserRole)
nwItem = self.theProject.projTree[tHandle]
nwItem.setExpanded(tItem.isExpanded())
cCount = tItem.childCount()
# Update tree-related meta data
nwItem = self.theProject.tree[tHandle]
nwItem.setExpanded(tItem.isExpanded() and cCount > 0)
nwItem.setOrder(tIndex)
theList.append(tHandle)
for i in range(tItem.childCount()):
for i in range(cCount):
self._scanChildren(theList, tItem.child(i), i)
return theList
def _addTreeItem(self, nwItem, nHandle=None):
@@ -965,9 +922,7 @@ class GuiProjectTree(QTreeWidget):
self._treeMap[tHandle] = newItem
if pHandle is None:
if nwItem.itemType == nwItemType.ROOT:
self.addTopLevelItem(newItem)
self.theParent.mainMenu.setAvailableRoot()
elif nwItem.itemType == nwItemType.TRASH:
newItem.setFlags(newItem.flags() ^ Qt.ItemIsDragEnabled)
self.addTopLevelItem(newItem)
else:
self.theParent.makeAlert(self.tr(
@@ -984,16 +939,14 @@ class GuiProjectTree(QTreeWidget):
except Exception:
logger.error("Failed to get index of item with handle '%s'", nHandle)
if byIndex >= 0:
self._treeMap[pHandle].insertChild(byIndex+1, newItem)
self._treeMap[pHandle].insertChild(byIndex + 1, newItem)
else:
self._treeMap[pHandle].addChild(newItem)
self.propagateCount(tHandle, nwItem.wordCount)
self.propagateCount(tHandle, nwItem.wordCount, countChildren=True)
self.setTreeItemValues(tHandle)
newItem.setExpanded(nwItem.isExpanded)
self._setTreeChanged(True)
return newItem
def _addTrashRoot(self):
@@ -1006,52 +959,34 @@ class GuiProjectTree(QTreeWidget):
trItem = self._getTreeItem(trashHandle)
if trItem is None:
trItem = self._addTreeItem(
self.theProject.projTree[trashHandle]
)
trItem = self._addTreeItem(self.theProject.tree[trashHandle])
if trItem is not None:
trItem.setExpanded(True)
self._setTreeChanged(True)
self._alertTreeChange(tHandle=trashHandle, flush=True)
return trItem
def _updateItemParent(self, tHandle):
"""Update the parent handle of an item so that the information
in the project is consistent with the treeView.
def _alertTreeChange(self, tHandle=None, flush=True):
"""Update information on tree change state, and emit necessary
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
self._timeChanged = time()
self.theProject.setProjectChanged(True)
if flush:
self.saveTreeOrder()
pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
nwItemS.setParent(pHandle)
self.setTreeItemValues(tHandle)
tItem = self.theProject.tree[tHandle]
if tItem is None:
return
logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle)
itemType = tItem.itemType
if itemType == nwItemType.ROOT:
self.rootFolderChanged.emit(tHandle)
elif itemType == nwItemType.FILE and tItem.isNovelLike():
self.novelItemChanged.emit(tHandle)
return True
self.treeItemChanged.emit(tHandle)
def _setTreeChanged(self, theState):
"""Set the tree change flag, and propagate to the project.
"""
self._treeChanged = theState
if theState:
self._timeChanged = time()
self.theProject.setProjectChanged(True)
return
def _emitItemChange(self, tHandle):
"""Emit an item change signal for a given handle.
"""
if self.theProject.projTree.checkType(tHandle, nwItemType.FILE):
nwItem = self.theProject.projTree[tHandle]
if nwItem.itemClass == nwItemClass.NOVEL:
self.novelItemChanged.emit()
else:
self.noteItemChanged.emit()
return
def _recordLastMove(self, srcItem, parItem, parIndex):
@@ -1130,9 +1065,9 @@ class GuiProjectTreeMenu(QMenu):
logger.error("Failed to extract information to build tree context menu")
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
isFile = theItem.itemType == nwItemType.FILE
@@ -1182,7 +1117,7 @@ class GuiProjectTreeMenu(QMenu):
"""Forward the new file call to the project tree.
"""
if self.theItem is not None:
self.theTree.newTreeItem(nwItemType.FILE, None)
self.theTree.newTreeItem(nwItemType.FILE)
return
@pyqtSlot()
@@ -1190,7 +1125,7 @@ class GuiProjectTreeMenu(QMenu):
"""Forward the new folder call to the project tree.
"""
if self.theItem is not None:
self.theTree.newTreeItem(nwItemType.FOLDER, None)
self.theTree.newTreeItem(nwItemType.FOLDER)
return
@pyqtSlot()
+10 -15
View File
@@ -456,20 +456,18 @@ class GuiIcons:
ICON_KEYS = {
# Project and GUI icons
"novelwriter", "proj_nwx",
"cls_none", "cls_novel", "cls_plot", "cls_character", "cls_world",
"cls_timeline", "cls_object", "cls_entity", "cls_custom", "cls_archive", "cls_trash",
"proj_document", "proj_title", "proj_chapter", "proj_scene", "proj_note", "proj_folder",
"status_lang", "status_time", "status_idle", "status_stats", "status_lines",
"doc_h0", "doc_h1", "doc_h2", "doc_h3", "doc_h4",
"search_case", "search_regex", "search_word", "search_loop", "search_project",
"search_cancel", "search_preserve",
"novelwriter", "cls_archive", "cls_character", "cls_custom", "cls_entity", "cls_none",
"cls_novel", "cls_object", "cls_plot", "cls_timeline", "cls_trash", "cls_world", "doc_h0",
"doc_h1", "doc_h2", "doc_h3", "doc_h4", "proj_chapter", "proj_details", "proj_document",
"proj_folder", "proj_note", "proj_nwx", "proj_scene", "proj_stats", "proj_title",
"search_cancel", "search_case", "search_loop", "search_preserve", "search_project",
"search_regex", "search_word", "status_idle", "status_lang", "status_lines",
"status_stats", "status_time", "view_build", "view_editor", "view_novel", "view_outline",
# General Button Icons
"delete", "close", "done", "clear", "save", "add", "remove",
"search", "search_replace", "edit", "check", "cross", "hash",
"maximise", "minimise", "refresh", "reference", "backward",
"forward", "settings",
"add", "backward", "check", "clear", "close", "cross", "delete", "done", "down", "edit",
"forward", "hash", "maximise", "menu", "minimise", "reference", "refresh", "remove",
"save", "search_replace", "search", "settings", "up",
# Switches
"sticky-on", "sticky-off",
@@ -639,9 +637,6 @@ class GuiIcons:
iconName = "proj_scene"
elif tLayout == nwItemLayout.NOTE:
iconName = "proj_note"
elif tType == nwItemType.TRASH:
iconName = nwLabels.CLASS_ICON[tClass]
if iconName is None:
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
+154 -172
View File
@@ -27,35 +27,34 @@ import os
import logging
import novelwriter
from enum import Enum
from time import time
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.QtWidgets import (
qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut,
QMessageBox, QDialog, QTabWidget, QToolBar, QAction
QMessageBox, QDialog, QStackedWidget
)
from novelwriter.gui import (
GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu,
GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails, GuiProjectTree,
GuiTheme
GuiMainStatus, GuiNovelTree, GuiOutline, GuiProjectTree, GuiTheme,
GuiViewsBar
)
from novelwriter.dialogs import (
GuiAbout, GuiDocMerge, GuiDocSplit, GuiItemEditor, GuiPreferences,
GuiProjectDetails, GuiProjectLoad, GuiProjectSettings, GuiUpdates,
GuiWordList
GuiAbout, GuiDocMerge, GuiDocSplit, GuiPreferences, GuiProjectDetails,
GuiProjectLoad, GuiProjectSettings, GuiUpdates, GuiWordList
)
from novelwriter.tools import (
GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats
)
from novelwriter.core import NWProject, NWIndex
from novelwriter.core import NWProject
from novelwriter.enum import (
nwItemType, nwItemClass, nwAlert, nwWidget, nwState
nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwState, nwView
)
from novelwriter.common import getGuiItem, hexToInt
from novelwriter.constants import nwLists
logger = logging.getLogger(__name__)
@@ -87,7 +86,6 @@ class GuiMain(QMainWindow):
# Core Classes and Settings
self.theTheme = GuiTheme()
self.theProject = NWProject(self)
self.theIndex = NWIndex(self.theProject)
self.hasProject = False
self.isFocusMode = False
self.idleRefTime = time()
@@ -103,8 +101,7 @@ class GuiMain(QMainWindow):
# Sizes
mPx = self.mainConf.pxInt(4)
fPx = self.theTheme.fontPixelSize
fPt = self.theTheme.fontPointSize
hWd = self.mainConf.pxInt(4)
# Main GUI Elements
self.statusBar = GuiMainStatus(self)
@@ -115,62 +112,43 @@ class GuiMain(QMainWindow):
self.docViewer = GuiDocViewer(self)
self.treeMeta = GuiItemDetails(self)
self.projView = GuiOutline(self)
self.projMeta = GuiOutlineDetails(self)
self.mainMenu = GuiMainMenu(self)
self.viewsBar = GuiViewsBar(self)
# Connect Signals Between Main Elements
self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage)
self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus)
self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts)
self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts)
self.viewsBar.viewChangeRequested.connect(self._changeView)
self.treeView.itemSelectionChanged.connect(self._treeSingleClick)
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.treeView.novelItemChanged.connect(self._treeNovelItemChanged)
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.projTabs = QTabWidget()
self.projTabs.setTabPosition(QTabWidget.South)
self.projTabs.setStyleSheet("QTabWidget::pane {border: 0;};")
self.projTabs.addTab(self.treeView, self.tr("Project"))
self.projTabs.addTab(self.novelView, self.tr("Novel"))
self.projTabs.currentChanged.connect(self._projTabsChanged)
self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage)
self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus)
self.docEditor.docCountsChanged.connect(self.treeMeta.doUpdateCounts)
self.docEditor.docCountsChanged.connect(self.treeView.doUpdateCounts)
self.docEditor.loadDocumentTagRequest.connect(self._followTag)
tabFont = self.projTabs.tabBar().font()
tabFont.setPointSizeF(0.9*fPt)
self.projTabs.tabBar().setFont(tabFont)
self.docViewer.loadDocumentTagRequest.connect(self._followTag)
# Project Tree Action Buttons
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.projView.loadDocumentTagRequest.connect(self._followTag)
self.projDetailsBtn = QAction(self.tr("Project Details"))
self.projDetailsBtn.setIcon(self.theTheme.getIcon("status_lines"))
self.projDetailsBtn.triggered.connect(lambda: self.showProjectDetailsDialog())
self.treeButtons.addAction(self.projDetailsBtn)
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 Stack
self.projStack = QStackedWidget()
self.projStack.addWidget(self.treeView)
self.projStack.addWidget(self.novelView)
self.projStack.currentChanged.connect(self._projStackChanged)
# Project Tree View
self.treePane = QWidget()
self.treeBox = QVBoxLayout()
self.treeBox.setContentsMargins(0, 0, 0, 0)
self.treeBox.setSpacing(mPx)
self.treeBox.addWidget(self.projTabs)
self.treeBox.addWidget(self.projStack)
self.treeBox.addWidget(self.treeMeta)
self.treePane.setLayout(self.treeBox)
@@ -178,47 +156,42 @@ class GuiMain(QMainWindow):
self.splitView = QSplitter(Qt.Vertical)
self.splitView.addWidget(self.docViewer)
self.splitView.addWidget(self.viewMeta)
self.splitView.setHandleWidth(hWd)
self.splitView.setSizes(self.mainConf.getViewPanePos())
# Splitter : Document Editor / Document Viewer
self.splitDocs = QSplitter(Qt.Horizontal)
self.splitDocs.addWidget(self.docEditor)
self.splitDocs.addWidget(self.splitView)
# 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)
self.splitDocs.setHandleWidth(hWd)
# Splitter : Project Tree / Main Tabs
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.mainTabs)
self.splitMain.addWidget(self.splitDocs)
self.splitMain.setHandleWidth(hWd)
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
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.idxViewer = self.splitDocs.indexOf(self.splitView)
self.idxViewDoc = self.splitView.indexOf(self.docViewer)
self.idxViewMeta = self.splitView.indexOf(self.viewMeta)
# Indices of Tab Widgets
self.idxTabEdit = self.mainTabs.indexOf(self.splitDocs)
self.idxTabProj = self.mainTabs.indexOf(self.splitOutline)
self.idxTreeView = self.projTabs.indexOf(self.treeView)
self.idxNovelView = self.projTabs.indexOf(self.novelView)
self.idxEditorView = self.mainStack.indexOf(self.splitMain)
self.idxOutlineView = self.mainStack.indexOf(self.projView)
self.idxTreeView = self.projStack.indexOf(self.treeView)
self.idxNovelView = self.projStack.indexOf(self.novelView)
# Splitter Behaviour
self.splitMain.setCollapsible(self.idxTree, False)
@@ -237,8 +210,9 @@ class GuiMain(QMainWindow):
# Set Main Window Elements
self.setMenuBar(self.mainMenu)
self.setCentralWidget(self.splitMain)
self.setCentralWidget(self.mainStack)
self.setStatusBar(self.statusBar)
self.addToolBar(Qt.LeftToolBarArea, self.viewsBar)
# Finalise Initialisation
# =======================
@@ -323,7 +297,7 @@ class GuiMain(QMainWindow):
self.docEditor.clearEditor()
self.docEditor.setDictionaries()
self.closeDocViewer()
self.projMeta.clearDetails()
self.projView.clearOutline()
# General
self.statusBar.clearStatus()
@@ -385,6 +359,7 @@ class GuiMain(QMainWindow):
self.rebuildTrees()
self.saveProject()
self.docEditor.setDictionaries()
self.projView.updateRootItem(None)
self.rebuildIndex(beQuiet=True)
self.statusBar.setRefTime(self.theProject.projOpened)
self.statusBar.setProjectStatus(nwState.GOOD)
@@ -446,10 +421,10 @@ class GuiMain(QMainWindow):
self.idleRefTime = time()
self.idleTime = 0.0
self.theIndex.clearIndex()
self.theProject.index.clearIndex()
self.clearGUI()
self.hasProject = False
self.mainTabs.setCurrentWidget(self.splitDocs)
self._changeView(nwView.PROJECT)
return saveOK
@@ -465,7 +440,7 @@ class GuiMain(QMainWindow):
return False
# Switch main tab to editor view
self.mainTabs.setCurrentWidget(self.splitDocs)
self._changeView(nwView.PROJECT)
# Try to open the project
if not self.theProject.openProject(projFile):
@@ -523,15 +498,15 @@ class GuiMain(QMainWindow):
self.idleTime = 0.0
# Load the tag index
self.theIndex.loadIndex()
self.theProject.index.loadIndex()
# Update GUI
self._updateWindowTitle(self.theProject.projName)
self.rebuildTrees()
self.docEditor.setDictionaries()
self.docEditor.toggleSpellCheck(self.theProject.spellCheck)
self.mainMenu.setAutoOutline(self.theProject.autoOutline)
self.statusBar.setRefTime(self.theProject.projOpened)
self.projView.updateRootItem(None)
self._updateStatusWordCount()
# Restore previously open documents, if any
@@ -542,10 +517,10 @@ class GuiMain(QMainWindow):
self.viewDocument(self.theProject.lastViewed)
# Check if we need to rebuild the index
if self.theIndex.indexBroken:
if self.theProject.index.indexBroken:
self.makeAlert(self.tr(
"The project index is outdated or broken. Rebuilding index."
), nwAlert.WARN)
), nwAlert.INFO)
self.rebuildIndex()
# Make sure the changed status is set to false on things opened
@@ -566,7 +541,7 @@ class GuiMain(QMainWindow):
self.treeView.saveTreeOrder()
if self.theProject.saveProject(autoSave=autoSave):
self.theIndex.saveIndex()
self.theProject.index.saveIndex()
return True
@@ -599,12 +574,12 @@ class GuiMain(QMainWindow):
logger.error("No project open")
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)
return False
self.closeDocument()
self.mainTabs.setCurrentWidget(self.splitDocs)
self._changeView(nwView.EDITOR)
if self.docEditor.loadText(tHandle, tLine):
if changeFocus:
self.docEditor.setFocus()
@@ -623,12 +598,11 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return False
self.treeView.flushTreeOrder()
nHandle = None # The next handle after tHandle
fHandle = None # The first file handle we encounter
foundIt = False # We've found tHandle, pick the next we see
for tItem in self.theProject.projTree:
if not self.theProject.projTree.checkType(tItem.itemHandle, nwItemType.FILE):
for tItem in self.theProject.tree:
if not self.theProject.tree.checkType(tItem.itemHandle, nwItemType.FILE):
continue
if fHandle is None:
fHandle = tItem.itemHandle
@@ -687,7 +661,7 @@ class GuiMain(QMainWindow):
return False
# 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)
if self.docViewer.loadText(tHandle):
@@ -817,7 +791,7 @@ class GuiMain(QMainWindow):
tHandle = self.treeView.getSelectedHandle()
elif self.novelView.hasFocus():
tHandle, tLine = self.novelView.getSelectedHandle()
elif self.projView.hasFocus():
elif self.projView.treeFocus():
tHandle, tLine = self.projView.getSelectedHandle()
else:
logger.warning("No item selected")
@@ -840,27 +814,10 @@ class GuiMain(QMainWindow):
tHandle = self.docEditor.docHandle()
else:
tHandle = self.treeView.getSelectedHandle()
if tHandle:
return self.treeView.editTreeItem(tHandle)
if tHandle is None:
logger.warning("No item selected")
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
return False
def rebuildTrees(self):
"""Rebuild the project tree.
@@ -872,7 +829,7 @@ class GuiMain(QMainWindow):
def requestNovelTreeRefresh(self):
"""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()
return True
return False
@@ -889,25 +846,16 @@ class GuiMain(QMainWindow):
tStart = time()
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:
self.setStatus(self.tr("Indexing: '{0}'").format(tItem.itemName))
else:
self.setStatus(self.tr("Indexing: '{0}'").format(self.tr("Unknown item")))
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)
logger.verbose("Indexing '%s'", tItem.itemName)
if self.theProject.index.reIndexHandle(tItem.itemHandle):
# Update Word Counts
self.treeView.propagateCount(tItem.itemHandle, tItem.wordCount, countChildren=True)
self.treeView.setTreeItemValues(tItem.itemHandle)
tEnd = time()
@@ -925,19 +873,6 @@ class GuiMain(QMainWindow):
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
##
@@ -985,7 +920,6 @@ class GuiMain(QMainWindow):
self.treeView.initTree()
self.novelView.initTree()
self.projView.initOutline()
self.projMeta.initDetails()
self._updateStatusWordCount()
return
@@ -1002,7 +936,9 @@ class GuiMain(QMainWindow):
if dlgProj.result() == QDialog.Accepted:
logger.debug("Applying new project settings")
self.docEditor.setDictionaries()
if dlgProj.spellChanged:
self.docEditor.setDictionaries()
self.treeMeta.refreshDetails()
self._updateWindowTitle(self.theProject.projName)
return True
@@ -1014,8 +950,6 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return False
self.treeView.flushTreeOrder()
dlgDetails = getGuiItem("GuiProjectDetails")
if dlgDetails is None:
dlgDetails = GuiProjectDetails(self)
@@ -1220,7 +1154,7 @@ class GuiMain(QMainWindow):
if not self.isFocusMode:
self.mainConf.setMainPanePos(self.splitMain.sizes())
self.mainConf.setDocPanePos(self.splitDocs.sizes())
self.mainConf.setOutlinePanePos(self.splitOutline.sizes())
self.mainConf.setOutlinePanePos(self.projView.splitSizes())
if self.viewMeta.isVisible():
self.mainConf.setViewPanePos(self.splitView.sizes())
@@ -1244,20 +1178,20 @@ class GuiMain(QMainWindow):
"""Switch focus between main GUI views.
"""
if paneNo == nwWidget.TREE:
tabIdx = self.projTabs.currentIndex()
tabIdx = self.projStack.currentIndex()
if tabIdx == self.idxTreeView:
self.treeView.setFocus()
elif tabIdx == self.idxNovelView:
self.novelView.setFocus()
elif paneNo == nwWidget.EDITOR:
self.mainTabs.setCurrentWidget(self.splitDocs)
self._changeView(nwView.EDITOR)
self.docEditor.setFocus()
elif paneNo == nwWidget.VIEWER:
self.mainTabs.setCurrentWidget(self.splitDocs)
self._changeView(nwView.EDITOR)
self.docViewer.setFocus()
elif paneNo == nwWidget.OUTLINE:
self.mainTabs.setCurrentWidget(self.splitOutline)
self.projView.setFocus()
self._changeView(nwView.OUTLINE)
self.projView.setTreeFocus()
return
def closeDocEditor(self):
@@ -1289,7 +1223,6 @@ class GuiMain(QMainWindow):
self.mainMenu.setFocusMode(self.isFocusMode)
if self.isFocusMode:
logger.debug("Activating Focus Mode")
self.mainTabs.setCurrentWidget(self.splitDocs)
self.switchFocus(nwWidget.EDITOR)
else:
logger.debug("Deactivating Focus Mode")
@@ -1298,7 +1231,7 @@ class GuiMain(QMainWindow):
self.treePane.setVisible(isVisible)
self.statusBar.setVisible(isVisible)
self.mainMenu.setVisible(isVisible)
self.mainTabs.tabBar().setVisible(isVisible)
self.viewsBar.setVisible(isVisible)
hideDocFooter = self.isFocusMode and self.mainConf.hideFocusFooter
self.docEditor.docFooter.setVisible(not hideDocFooter)
@@ -1426,7 +1359,7 @@ class GuiMain(QMainWindow):
return True
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
if projName is not None:
@@ -1469,9 +1402,9 @@ class GuiMain(QMainWindow):
"popMinimal": newProj.field("popMinimal"),
"popCustom": newProj.field("popCustom"),
"addRoots": [],
"addNotes": False,
"numChapters": 0,
"numScenes": 0,
"chFolders": False,
}
if newProj.field("popCustom"):
addRoots = []
@@ -1481,19 +1414,30 @@ class GuiMain(QMainWindow):
addRoots.append(nwItemClass.CHARACTER)
if newProj.field("addWorld"):
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["addNotes"] = newProj.field("addNotes")
projData["numChapters"] = newProj.field("numChapters")
projData["numScenes"] = newProj.field("numScenes")
projData["chFolders"] = newProj.field("chFolders")
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
##
@@ -1509,9 +1453,42 @@ class GuiMain(QMainWindow):
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()
def _timeTick(self):
"""Triggered on every tick of the main timer.
@@ -1567,11 +1544,17 @@ class GuiMain(QMainWindow):
@pyqtSlot("QTreeWidgetItem*", int)
def _treeDoubleClick(self, tItem, colNo):
"""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()
if tHandle is not None:
self.openDocument(tHandle, changeFocus=False, doScroll=False)
tItem = self.theProject.tree[tHandle]
if tItem is None:
return
if tItem.itemType == nwItemType.FILE:
self.openDocument(tHandle, changeFocus=False, doScroll=False)
else:
self.treeView.toggleExpanded(tHandle)
return
@pyqtSlot()
@@ -1579,11 +1562,10 @@ class GuiMain(QMainWindow):
"""Triggered when there is a change to a novel item in the
project tree.
"""
if self.mainTabs.currentIndex() == self.idxTabProj:
if self.mainStack.currentIndex() == self.idxOutlineView:
logger.verbose("Novel tree changed while Outline tab active")
if self.hasProject:
self.treeView.flushTreeOrder()
self.projView.refreshTree(novelChanged=True)
self.projView.refreshView(novelChanged=True)
return
@@ -1608,20 +1590,20 @@ class GuiMain(QMainWindow):
return
@pyqtSlot(int)
def _mainTabChanged(self, tabIndex):
def _mainStackChanged(self, tabIndex):
"""Activated when the main window tab is changed.
"""
if tabIndex == self.idxTabEdit:
if tabIndex == self.idxEditorView:
logger.verbose("Editor tab activated")
elif tabIndex == self.idxTabProj:
elif tabIndex == self.idxOutlineView:
logger.verbose("Project outline tab activated")
if self.hasProject:
self.projView.refreshTree()
self.projView.refreshView()
return
@pyqtSlot(int)
def _projTabsChanged(self, tabIndex):
def _projStackChanged(self, tabIndex):
"""Activated when the project view tab is changed.
"""
sHandle = None
+50 -55
View File
@@ -75,7 +75,6 @@ class GuiBuildNovel(QDialog):
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.htmlText = [] # List of html documents
self.htmlStyle = [] # List of html styles
@@ -86,9 +85,10 @@ class GuiBuildNovel(QDialog):
self.setMinimumWidth(self.mainConf.pxInt(700))
self.setMinimumHeight(self.mainConf.pxInt(600))
pOptions = self.theProject.options
self.resize(
self.mainConf.pxInt(self.optState.getInt("GuiBuildNovel", "winWidth", 900)),
self.mainConf.pxInt(self.optState.getInt("GuiBuildNovel", "winHeight", 800))
self.mainConf.pxInt(pOptions.getInt("GuiBuildNovel", "winWidth", 900)),
self.mainConf.pxInt(pOptions.getInt("GuiBuildNovel", "winHeight", 800))
)
self.docView = GuiBuildNovelDocView(self, self.theProject)
@@ -174,12 +174,12 @@ class GuiBuildNovel(QDialog):
self.hideScene = QSwitch(width=wS, height=hS)
self.hideScene.setChecked(
self.optState.getBool("GuiBuildNovel", "hideScene", False)
pOptions.getBool("GuiBuildNovel", "hideScene", False)
)
self.hideSection = QSwitch(width=wS, height=hS)
self.hideSection.setChecked(
self.optState.getBool("GuiBuildNovel", "hideSection", True)
pOptions.getBool("GuiBuildNovel", "hideSection", True)
)
# Wrapper boxes due to QGridView and QLineEdit expand bug
@@ -235,7 +235,7 @@ class GuiBuildNovel(QDialog):
self.textFont.setReadOnly(True)
self.textFont.setMinimumWidth(xFmt)
self.textFont.setText(
self.optState.getString("GuiBuildNovel", "textFont", self.mainConf.textFont)
pOptions.getString("GuiBuildNovel", "textFont", self.mainConf.textFont)
)
self.fontButton = QPushButton("...")
self.fontButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("...")))
@@ -247,7 +247,7 @@ class GuiBuildNovel(QDialog):
self.textSize.setMaximum(72)
self.textSize.setSingleStep(1)
self.textSize.setValue(
self.optState.getInt("GuiBuildNovel", "textSize", self.mainConf.textSize)
pOptions.getInt("GuiBuildNovel", "textSize", self.mainConf.textSize)
)
self.lineHeight = QDoubleSpinBox(self)
@@ -257,7 +257,7 @@ class GuiBuildNovel(QDialog):
self.lineHeight.setSingleStep(0.05)
self.lineHeight.setDecimals(2)
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
@@ -291,12 +291,12 @@ class GuiBuildNovel(QDialog):
self.justifyText = QSwitch(width=wS, height=hS)
self.justifyText.setChecked(
self.optState.getBool("GuiBuildNovel", "justifyText", False)
pOptions.getBool("GuiBuildNovel", "justifyText", False)
)
self.noStyling = QSwitch(width=wS, height=hS)
self.noStyling.setChecked(
self.optState.getBool("GuiBuildNovel", "noStyling", False)
pOptions.getBool("GuiBuildNovel", "noStyling", False)
)
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.setChecked(
self.optState.getBool("GuiBuildNovel", "incSynopsis", False)
pOptions.getBool("GuiBuildNovel", "incSynopsis", False)
)
self.includeComments = QSwitch(width=wS, height=hS)
self.includeComments.setChecked(
self.optState.getBool("GuiBuildNovel", "incComments", False)
pOptions.getBool("GuiBuildNovel", "incComments", False)
)
self.includeKeywords = QSwitch(width=wS, height=hS)
self.includeKeywords.setChecked(
self.optState.getBool("GuiBuildNovel", "incKeywords", False)
pOptions.getBool("GuiBuildNovel", "incKeywords", False)
)
self.includeBody = QSwitch(width=wS, height=hS)
self.includeBody.setChecked(
self.optState.getBool("GuiBuildNovel", "incBodyText", True)
pOptions.getBool("GuiBuildNovel", "incBodyText", True)
)
synopsisLabel = QLabel(self.tr("Include synopsis"))
@@ -360,17 +360,17 @@ class GuiBuildNovel(QDialog):
self.novelFiles = QSwitch(width=wS, height=hS)
self.novelFiles.setChecked(
self.optState.getBool("GuiBuildNovel", "addNovel", True)
pOptions.getBool("GuiBuildNovel", "addNovel", True)
)
self.noteFiles = QSwitch(width=wS, height=hS)
self.noteFiles.setChecked(
self.optState.getBool("GuiBuildNovel", "addNotes", False)
pOptions.getBool("GuiBuildNovel", "addNotes", False)
)
self.ignoreFlag = QSwitch(width=wS, height=hS)
self.ignoreFlag.setChecked(
self.optState.getBool("GuiBuildNovel", "ignoreFlag", False)
pOptions.getBool("GuiBuildNovel", "ignoreFlag", False)
)
novelLabel = QLabel(self.tr("Include novel files"))
@@ -396,12 +396,12 @@ class GuiBuildNovel(QDialog):
self.replaceTabs = QSwitch(width=wS, height=hS)
self.replaceTabs.setChecked(
self.optState.getBool("GuiBuildNovel", "replaceTabs", False)
pOptions.getBool("GuiBuildNovel", "replaceTabs", False)
)
self.replaceUCode = QSwitch(width=wS, height=hS)
self.replaceUCode.setChecked(
self.optState.getBool("GuiBuildNovel", "replaceUCode", False)
pOptions.getBool("GuiBuildNovel", "replaceUCode", False)
)
tabsLabel = QLabel(self.tr("Replace tabs with spaces"))
@@ -493,9 +493,9 @@ class GuiBuildNovel(QDialog):
# Splitter Position
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 = self.optState.getInt("GuiBuildNovel", "docWidth", docWidth)
docWidth = pOptions.getInt("GuiBuildNovel", "docWidth", docWidth)
# The Tool Box
self.toolsBox = QVBoxLayout()
@@ -659,6 +659,7 @@ class GuiBuildNovel(QDialog):
fmtUnnumbered = self.fmtUnnumbered.text()
fmtScene = self.fmtScene.text()
fmtSection = self.fmtSection.text()
buildLang = self.buildLang.currentData()
hideScene = self.hideScene.isChecked()
hideSection = self.hideSection.isChecked()
textFont = self.textFont.text()
@@ -676,7 +677,7 @@ class GuiBuildNovel(QDialog):
replaceUCode = self.replaceUCode.isChecked()
# The language lookup dict is reloaded if needed
self.theProject.setProjectLang(self.buildLang.currentData())
self.theProject.setProjectLang(buildLang)
# Get font information
fontInfo = QFontInfo(QFont(textFont, textSize))
@@ -706,16 +707,16 @@ class GuiBuildNovel(QDialog):
if isOdt:
bldObj.setColourHeaders(not noStyling)
bldObj.setLanguage(buildLang)
bldObj.initDocument()
# Make sure the project and document is up to date
self.theParent.treeView.flushTreeOrder()
self.theParent.saveDocument()
self.buildProgress.setMaximum(len(self.theProject.projTree))
self.buildProgress.setMaximum(len(self.theProject.tree))
self.buildProgress.setValue(0)
for nItt, tItem in enumerate(self.theProject.projTree):
for nItt, tItem in enumerate(self.theProject.tree):
noteRoot = noteFiles
noteRoot &= tItem.itemType == nwItemType.ROOT
@@ -780,14 +781,12 @@ class GuiBuildNovel(QDialog):
if theItem is None:
return False
if not theItem.isExported and not ignoreFlag:
if not (theItem.isExported or ignoreFlag):
return False
isNone = theItem.itemType != nwItemType.FILE
isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT
isNone |= theItem.itemClass == nwItemClass.NO_CLASS
isNone |= theItem.itemClass == nwItemClass.TRASH
isNone |= theItem.itemParent == self.theProject.projTree.trashRoot()
isNone |= theItem.isInactive()
isNone |= theItem.itemParent is None
isNote = theItem.itemLayout == nwItemLayout.NOTE
isNovel = not isNone and not isNote
@@ -799,10 +798,6 @@ class GuiBuildNovel(QDialog):
if isNovel and not novelFiles:
return False
rootItem = self.theProject.projTree.getRootItem(theItem.itemHandle)
if rootItem.itemClass == nwItemClass.ARCHIVE:
return False
return True
def _saveDocument(self, theFmt):
@@ -1159,28 +1154,28 @@ class GuiBuildNovel(QDialog):
self.theProject.setProjectLang(buildLang)
# GUI Settings
self.optState.setValue("GuiBuildNovel", "hideScene", hideScene)
self.optState.setValue("GuiBuildNovel", "hideSection", hideSection)
self.optState.setValue("GuiBuildNovel", "winWidth", winWidth)
self.optState.setValue("GuiBuildNovel", "winHeight", winHeight)
self.optState.setValue("GuiBuildNovel", "boxWidth", boxWidth)
self.optState.setValue("GuiBuildNovel", "docWidth", docWidth)
self.optState.setValue("GuiBuildNovel", "justifyText", justifyText)
self.optState.setValue("GuiBuildNovel", "noStyling", noStyling)
self.optState.setValue("GuiBuildNovel", "textFont", textFont)
self.optState.setValue("GuiBuildNovel", "textSize", textSize)
self.optState.setValue("GuiBuildNovel", "lineHeight", lineHeight)
self.optState.setValue("GuiBuildNovel", "addNovel", novelFiles)
self.optState.setValue("GuiBuildNovel", "addNotes", noteFiles)
self.optState.setValue("GuiBuildNovel", "ignoreFlag", ignoreFlag)
self.optState.setValue("GuiBuildNovel", "incSynopsis", incSynopsis)
self.optState.setValue("GuiBuildNovel", "incComments", incComments)
self.optState.setValue("GuiBuildNovel", "incKeywords", incKeywords)
self.optState.setValue("GuiBuildNovel", "incBodyText", incBodyText)
self.optState.setValue("GuiBuildNovel", "replaceTabs", replaceTabs)
self.optState.setValue("GuiBuildNovel", "replaceUCode", replaceUCode)
self.optState.saveSettings()
pOptions = self.theProject.options
pOptions.setValue("GuiBuildNovel", "hideScene", hideScene)
pOptions.setValue("GuiBuildNovel", "hideSection", hideSection)
pOptions.setValue("GuiBuildNovel", "winWidth", winWidth)
pOptions.setValue("GuiBuildNovel", "winHeight", winHeight)
pOptions.setValue("GuiBuildNovel", "boxWidth", boxWidth)
pOptions.setValue("GuiBuildNovel", "docWidth", docWidth)
pOptions.setValue("GuiBuildNovel", "justifyText", justifyText)
pOptions.setValue("GuiBuildNovel", "noStyling", noStyling)
pOptions.setValue("GuiBuildNovel", "textFont", textFont)
pOptions.setValue("GuiBuildNovel", "textSize", textSize)
pOptions.setValue("GuiBuildNovel", "lineHeight", lineHeight)
pOptions.setValue("GuiBuildNovel", "addNovel", novelFiles)
pOptions.setValue("GuiBuildNovel", "addNotes", noteFiles)
pOptions.setValue("GuiBuildNovel", "ignoreFlag", ignoreFlag)
pOptions.setValue("GuiBuildNovel", "incSynopsis", incSynopsis)
pOptions.setValue("GuiBuildNovel", "incComments", incComments)
pOptions.setValue("GuiBuildNovel", "incKeywords", incKeywords)
pOptions.setValue("GuiBuildNovel", "incBodyText", incBodyText)
pOptions.setValue("GuiBuildNovel", "replaceTabs", replaceTabs)
pOptions.setValue("GuiBuildNovel", "replaceUCode", replaceUCode)
pOptions.saveSettings()
return
+1 -1
View File
@@ -72,7 +72,7 @@ class GuiLipsum(QDialog):
# Form
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.setMinimum(1)
self.paraCount.setMaximum(100)
+119 -90
View File
@@ -31,12 +31,10 @@ from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QWizard, QWizardPage, QLabel, QVBoxLayout, QLineEdit, QPlainTextEdit,
QPushButton, QFileDialog, QHBoxLayout, QRadioButton, QFormLayout,
QGroupBox, QGridLayout, QSpinBox
QGridLayout, QSpinBox
)
from novelwriter.enum import nwItemClass
from novelwriter.common import makeFileNameSafe
from novelwriter.constants import trConst, nwLabels
from novelwriter.gui.custom import QSwitch
logger = logging.getLogger(__name__)
@@ -98,10 +96,10 @@ class ProjWizardIntroPage(QWizardPage):
self.setTitle(self.tr("Create New Project"))
self.theText = QLabel(self.tr(
"Provide at least a working title. The working title should not "
"be change beyond this point as it is used by the application for "
"generating file names for for instance backups. The other fields "
"are optional and can be changed at any time in Project Settings."
"Provide at least a project name. The project name should not "
"be changed beyond this point as it is used for generating file "
"names for for instance backups. The other fields are optional "
"and can be changed at any time in Project Settings."
))
self.theText.setWordWrap(True)
@@ -134,7 +132,7 @@ class ProjWizardIntroPage(QWizardPage):
self.projAuthors.setPlaceholderText(self.tr("Optional. One name per line."))
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("Author(s)"), self.projAuthors)
self.mainForm.setVerticalSpacing(fS)
@@ -185,6 +183,9 @@ class ProjWizardFolderPage(QWizardPage):
self.browseButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("...")))
self.browseButton.clicked.connect(self._doBrowse)
self.errLabel = QLabel("")
self.errLabel.setWordWrap(True)
self.mainForm = QHBoxLayout()
self.mainForm.addWidget(QLabel(self.tr("Project Path")), 0)
self.mainForm.addWidget(self.projPath, 1)
@@ -198,11 +199,36 @@ class ProjWizardFolderPage(QWizardPage):
self.outerBox.setSpacing(vS)
self.outerBox.addWidget(self.theText)
self.outerBox.addLayout(self.mainForm)
self.outerBox.addWidget(self.errLabel)
self.outerBox.addStretch(1)
self.setLayout(self.outerBox)
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
##
@@ -296,68 +322,28 @@ class ProjWizardCustomPage(QWizardPage):
self.setTitle(self.tr("Custom Project Options"))
self.theText = QLabel(self.tr(
"Select which additional root folders to make, and how to populate "
"the Novel folder. If you don't want to add chapters or scenes, set "
"the values to 0. You can add scenes without chapters."
"Select which additional elements to populate the project with. "
"You can skip making chapters and add only scenes by setting the "
"number of chapters to 0."
))
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
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.addChar = QSwitch()
self.addWorld = QSwitch()
self.addTime = QSwitch()
self.addObject = QSwitch()
self.addEntity = QSwitch()
self.addPlot = QSwitch()
self.addChar = QSwitch()
self.addWorld = QSwitch()
self.addNotes = QSwitch()
self.addPlot.setChecked(True)
self.addChar.setChecked(True)
self.addWorld.setChecked(True)
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)
self.addWorld.setChecked(False)
self.addNotes.setChecked(False)
# Generate Content
self.numChapters = QSpinBox()
self.numChapters.setRange(0, 100)
self.numChapters.setValue(5)
@@ -366,37 +352,40 @@ class ProjWizardCustomPage(QWizardPage):
self.numScenes.setRange(0, 200)
self.numScenes.setValue(5)
self.chFolders = QSwitch()
self.chFolders.setChecked(True)
self.novelForm.addWidget(QLabel(self.tr("Add chapters")), 0, 0)
self.novelForm.addWidget(QLabel(self.tr("Scenes (per chapter)")), 1, 0)
self.novelForm.addWidget(QLabel(self.tr("Add chapter folders")), 2, 0)
self.novelForm.addWidget(self.numChapters, 0, 1, 1, 1, Qt.AlignRight)
self.novelForm.addWidget(self.numScenes, 1, 1, 1, 1, Qt.AlignRight)
self.novelForm.addWidget(self.chFolders, 2, 1, 1, 1, Qt.AlignRight)
self.novelForm.setRowStretch(3, 1)
# Grid Form
self.addBox = QGridLayout()
self.addBox.addWidget(QLabel(self.tr("Add a folder for plot notes")), 0, 0)
self.addBox.addWidget(QLabel(self.tr("Add a folder for character notes")), 1, 0)
self.addBox.addWidget(QLabel(self.tr("Add a folder for location notes")), 2, 0)
self.addBox.addWidget(QLabel(self.tr("Add example notes to the above")), 3, 0)
self.addBox.addWidget(QLabel(self.tr("Add chapters to the novel folder")), 4, 0)
self.addBox.addWidget(QLabel(self.tr("Add scenes to each chapter")), 5, 0)
self.addBox.addWidget(self.addPlot, 0, 1, 1, 1, Qt.AlignRight)
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
self.registerField("addPlot", self.addPlot)
self.registerField("addChar", self.addChar)
self.registerField("addWorld", self.addWorld)
self.registerField("addTime", self.addTime)
self.registerField("addObject", self.addObject)
self.registerField("addEntity", self.addEntity)
self.registerField("addNotes", self.addNotes)
self.registerField("numChapters", self.numChapters)
self.registerField("numScenes", self.numScenes)
self.registerField("chFolders", self.chFolders)
# Assemble
self.innerBox = QHBoxLayout()
self.innerBox.addWidget(self.rootGroup)
self.innerBox.addWidget(self.novelGroup)
self.outerBox = QVBoxLayout()
self.outerBox.setSpacing(vS)
self.outerBox.setSpacing(cM)
self.outerBox.addWidget(self.theText)
self.outerBox.addLayout(self.innerBox)
self.outerBox.addLayout(self.addBox)
self.outerBox.addStretch(1)
self.setLayout(self.outerBox)
@@ -413,15 +402,8 @@ class ProjWizardFinalPage(QWizardPage):
self.mainConf = novelwriter.CONFIG
self.theWizard = theWizard
self.setTitle(self.tr("Finished"))
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.setTitle(self.tr("Summary"))
self.theText = QLabel("")
self.theText.setWordWrap(True)
# Assemble
@@ -433,4 +415,51 @@ class ProjWizardFinalPage(QWizardPage):
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
+34 -33
View File
@@ -67,33 +67,34 @@ class GuiWritingStats(QDialog):
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.logData = []
self.filterData = []
self.timeFilter = 0.0
self.wordOffset = 0
pOptions = self.theProject.options
self.setWindowTitle(self.tr("Writing Statistics"))
self.setMinimumWidth(self.mainConf.pxInt(420))
self.setMinimumHeight(self.mainConf.pxInt(400))
self.resize(
self.mainConf.pxInt(self.optState.getInt("GuiWritingStats", "winWidth", 550)),
self.mainConf.pxInt(self.optState.getInt("GuiWritingStats", "winHeight", 500))
self.mainConf.pxInt(pOptions.getInt("GuiWritingStats", "winWidth", 550)),
self.mainConf.pxInt(pOptions.getInt("GuiWritingStats", "winHeight", 500))
)
# List Box
wCol0 = self.mainConf.pxInt(
self.optState.getInt("GuiWritingStats", "widthCol0", 180)
pOptions.getInt("GuiWritingStats", "widthCol0", 180)
)
wCol1 = self.mainConf.pxInt(
self.optState.getInt("GuiWritingStats", "widthCol1", 80)
pOptions.getInt("GuiWritingStats", "widthCol1", 80)
)
wCol2 = self.mainConf.pxInt(
self.optState.getInt("GuiWritingStats", "widthCol2", 80)
pOptions.getInt("GuiWritingStats", "widthCol2", 80)
)
wCol3 = self.mainConf.pxInt(
self.optState.getInt("GuiWritingStats", "widthCol3", 80)
pOptions.getInt("GuiWritingStats", "widthCol3", 80)
)
self.listBox = QTreeWidget()
@@ -115,9 +116,9 @@ class GuiWritingStats(QDialog):
hHeader.setTextAlignment(self.C_IDLE, 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(
self.optState.getInt("GuiWritingStats", "sortOrder", Qt.DescendingOrder),
pOptions.getInt("GuiWritingStats", "sortOrder", Qt.DescendingOrder),
(Qt.AscendingOrder, Qt.DescendingOrder), Qt.DescendingOrder
)
self.listBox.sortByColumn(sortCol, sortOrder)
@@ -190,37 +191,37 @@ class GuiWritingStats(QDialog):
self.incNovel = QSwitch(width=2*sPx, height=sPx)
self.incNovel.setChecked(
self.optState.getBool("GuiWritingStats", "incNovel", True)
pOptions.getBool("GuiWritingStats", "incNovel", True)
)
self.incNovel.clicked.connect(self._updateListBox)
self.incNotes = QSwitch(width=2*sPx, height=sPx)
self.incNotes.setChecked(
self.optState.getBool("GuiWritingStats", "incNotes", True)
pOptions.getBool("GuiWritingStats", "incNotes", True)
)
self.incNotes.clicked.connect(self._updateListBox)
self.hideZeros = QSwitch(width=2*sPx, height=sPx)
self.hideZeros.setChecked(
self.optState.getBool("GuiWritingStats", "hideZeros", True)
pOptions.getBool("GuiWritingStats", "hideZeros", True)
)
self.hideZeros.clicked.connect(self._updateListBox)
self.hideNegative = QSwitch(width=2*sPx, height=sPx)
self.hideNegative.setChecked(
self.optState.getBool("GuiWritingStats", "hideNegative", False)
pOptions.getBool("GuiWritingStats", "hideNegative", False)
)
self.hideNegative.clicked.connect(self._updateListBox)
self.groupByDay = QSwitch(width=2*sPx, height=sPx)
self.groupByDay.setChecked(
self.optState.getBool("GuiWritingStats", "groupByDay", False)
pOptions.getBool("GuiWritingStats", "groupByDay", False)
)
self.groupByDay.clicked.connect(self._updateListBox)
self.showIdleTime = QSwitch(width=2*sPx, height=sPx)
self.showIdleTime.setChecked(
self.optState.getBool("GuiWritingStats", "showIdleTime", False)
pOptions.getBool("GuiWritingStats", "showIdleTime", False)
)
self.showIdleTime.clicked.connect(self._updateListBox)
@@ -244,7 +245,7 @@ class GuiWritingStats(QDialog):
self.histMax.setMaximum(100000)
self.histMax.setSingleStep(100)
self.histMax.setValue(
self.optState.getInt("GuiWritingStats", "histMax", 2000)
pOptions.getInt("GuiWritingStats", "histMax", 2000)
)
self.histMax.valueChanged.connect(self._updateListBox)
@@ -323,23 +324,23 @@ class GuiWritingStats(QDialog):
showIdleTime = self.showIdleTime.isChecked()
histMax = self.histMax.value()
self.optState.setValue("GuiWritingStats", "winWidth", winWidth)
self.optState.setValue("GuiWritingStats", "winHeight", winHeight)
self.optState.setValue("GuiWritingStats", "widthCol0", widthCol0)
self.optState.setValue("GuiWritingStats", "widthCol1", widthCol1)
self.optState.setValue("GuiWritingStats", "widthCol2", widthCol2)
self.optState.setValue("GuiWritingStats", "widthCol3", widthCol3)
self.optState.setValue("GuiWritingStats", "sortCol", sortCol)
self.optState.setValue("GuiWritingStats", "sortOrder", sortOrder)
self.optState.setValue("GuiWritingStats", "incNovel", incNovel)
self.optState.setValue("GuiWritingStats", "incNotes", incNotes)
self.optState.setValue("GuiWritingStats", "hideZeros", hideZeros)
self.optState.setValue("GuiWritingStats", "hideNegative", hideNegative)
self.optState.setValue("GuiWritingStats", "groupByDay", groupByDay)
self.optState.setValue("GuiWritingStats", "showIdleTime", showIdleTime)
self.optState.setValue("GuiWritingStats", "histMax", histMax)
self.optState.saveSettings()
pOptions = self.theProject.options
pOptions.setValue("GuiWritingStats", "winWidth", winWidth)
pOptions.setValue("GuiWritingStats", "winHeight", winHeight)
pOptions.setValue("GuiWritingStats", "widthCol0", widthCol0)
pOptions.setValue("GuiWritingStats", "widthCol1", widthCol1)
pOptions.setValue("GuiWritingStats", "widthCol2", widthCol2)
pOptions.setValue("GuiWritingStats", "widthCol3", widthCol3)
pOptions.setValue("GuiWritingStats", "sortCol", sortCol)
pOptions.setValue("GuiWritingStats", "sortOrder", sortOrder)
pOptions.setValue("GuiWritingStats", "incNovel", incNovel)
pOptions.setValue("GuiWritingStats", "incNotes", incNotes)
pOptions.setValue("GuiWritingStats", "hideZeros", hideZeros)
pOptions.setValue("GuiWritingStats", "hideNegative", hideNegative)
pOptions.setValue("GuiWritingStats", "groupByDay", groupByDay)
pOptions.setValue("GuiWritingStats", "showIdleTime", showIdleTime)
pOptions.setValue("GuiWritingStats", "histMax", histMax)
pOptions.saveSettings()
self.close()
return
+1
View File
@@ -4,5 +4,6 @@
# Mars
@tag: Mars
@location: Space
Its red. Dusty and red.
+6 -8
View File
@@ -1,19 +1,19 @@
%%~name: Making a Scene
%%~path: e7ded148d6e4a/636b6aa9b697b
%%~path: 6a2d6d5f4f401/636b6aa9b697b
%%~kind: NOVEL/DOCUMENT
### Making a Scene
@pov: Jane
@char: John
@char: John, Jane
@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.
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.
@@ -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 “* * *”.
#### 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:
@@ -35,8 +35,6 @@ This text is left-aligned. <<
>> This text is centred. <<
#### Text Indent
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. <
+3 -3
View File
@@ -1,11 +1,11 @@
%%~name: Chapter One
%%~path: e7ded148d6e4a/6a2d6d5f4f401
%%~path: 7031beac91f75/6a2d6d5f4f401
%%~kind: NOVEL/DOCUMENT
## So it Begins
@pov: Jane
@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
%%~path: e7ded148d6e4a/88706ddc78b1b
%%~path: 7031beac91f75/88706ddc78b1b
%%~kind: NOVEL/DOCUMENT
## Where has John Gone?
@@ -11,6 +11,7 @@
### Jane Cannot Find John
@pov: Jane
@focus: John
@location: 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
%%~path: ae9bf3c3ea159/8a5deb88c0e97
%%~kind: NOVEL/DOCUMENT
%%~kind: ARCHIVE/DOCUMENT
### 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
%%~path: e7ded148d6e4a/96b68994dfa3d
%%~path: 7031beac91f75/96b68994dfa3d
%%~kind: NOVEL/NOTE
# 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.
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
@@ -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.
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
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.
+1 -1
View File
@@ -6,4 +6,4 @@
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!
%%~path: e7ded148d6e4a/ae7339df26ded
%%~path: 88706ddc78b1b/ae7339df26ded
%%~kind: NOVEL/DOCUMENT
### We Found John!
@pov: John
@focus: John
@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.
+2 -1
View File
@@ -4,5 +4,6 @@
# 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!
%%~path: 98acd8c76c93a/b8136a5a774a0
%%~kind: NOVEL/DOCUMENT
%%~kind: TRASH/DOCUMENT
### Delete Me!
This scene is trash.
+2 -2
View File
@@ -1,9 +1,9 @@
%%~name: Interlude
%%~path: e7ded148d6e4a/ba8a28a246524
%%~path: 7031beac91f75/ba8a28a246524
%%~kind: NOVEL/DOCUMENT
##! 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'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
%%~path: e7ded148d6e4a/bc0cbd2a407f3
%%~path: 6a2d6d5f4f401/bc0cbd2a407f3
%%~kind: NOVEL/DOCUMENT
### Another Scene
@@ -7,9 +7,9 @@
@focus: Jane
@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
@@ -17,4 +17,4 @@ In fact, if you wish, you can add all the scenes in the chapter file too. All no
@focus: John
@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'?>
<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>
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
<author>Jay Doh</author>
<saveCount>1303</saveCount>
<autoCount>199</autoCount>
<editTime>65005</editTime>
<saveCount>1334</saveCount>
<autoCount>225</autoCount>
<editTime>67746</editTime>
</project>
<settings>
<doBackup>False</doBackup>
@@ -15,11 +15,11 @@
<spellCheck>True</spellCheck>
<spellLang>None</spellLang>
<autoOutline>True</autoOutline>
<lastEdited>636b6aa9b697b</lastEdited>
<lastEdited>a520879ca0b45</lastEdited>
<lastViewed>636b6aa9b697b</lastViewed>
<lastWordCount>1206</lastWordCount>
<novelWordCount>830</novelWordCount>
<notesWordCount>376</notesWordCount>
<lastWordCount>1363</lastWordCount>
<novelWordCount>954</novelWordCount>
<notesWordCount>409</notesWordCount>
<autoReplace>
<entry key="A">B</entry>
<entry key="B">E</entry>
@@ -33,121 +33,129 @@
<section></section>
</titleFormat>
<status>
<entry red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Notes</entry>
<entry red="182" green="60" blue="0">Started</entry>
<entry red="193" green="129" blue="0">1st Draft</entry>
<entry red="193" green="129" blue="0">2nd Draft</entry>
<entry red="193" green="129" blue="0">3rd Draft</entry>
<entry red="58" green="180" blue="58">Finished</entry>
<entry key="sf12341" count="7" red="100" green="100" blue="100">New</entry>
<entry key="sf24ce6" count="1" red="200" green="50" blue="0">Notes</entry>
<entry key="sc24b8f" count="2" red="182" green="60" blue="0">Started</entry>
<entry key="s90e6c9" count="6" red="193" green="129" blue="0">1st Draft</entry>
<entry key="sd51c5b" count="1" red="193" green="129" blue="0">2nd Draft</entry>
<entry key="s8ae72a" count="0" red="193" green="129" blue="0">3rd Draft</entry>
<entry key="s78ea90" count="0" red="58" green="180" blue="58">Finished</entry>
</status>
<importance>
<entry red="100" green="100" blue="100">None</entry>
<entry red="0" green="122" blue="188">Minor</entry>
<entry red="21" green="0" blue="180">Major</entry>
<entry red="117" green="0" blue="175">Main</entry>
<entry key="ia857f0" count="5" red="100" green="100" blue="100">None</entry>
<entry key="icfb3a5" count="2" red="0" green="122" blue="188">Minor</entry>
<entry key="i2d7a54" count="2" red="21" green="0" blue="180">Major</entry>
<entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry>
</importance>
</settings>
<content count="25">
<item handle="7031beac91f75" parent="None" order="0" type="ROOT" class="NOVEL">
<content count="27">
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="True"/>
<name status="Started" import="None">Novel</name>
<name status="sc24b8f" import="ia857f0">Novel</name>
</item>
<item handle="53b69b83cdafc" parent="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="93" wordCount="19" paraCount="2" cursorPos="2"/>
<name status="Started" import="None" exported="True">Title Page</name>
<item handle="53b69b83cdafc" parent="7031beac91f75" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="93" wordCount="19" paraCount="2" cursorPos="119"/>
<name status="sc24b8f" import="ia857f0" exported="True">Title Page</name>
</item>
<item handle="974e400180a99" parent="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="186" wordCount="39" paraCount="2" cursorPos="212"/>
<name status="New" import="None" exported="True">Page</name>
<item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="251" wordCount="50" paraCount="2" cursorPos="277"/>
<name status="sf12341" import="ia857f0" exported="True">Page</name>
</item>
<item handle="edca4be2fcaf8" parent="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="26" wordCount="6" paraCount="1" cursorPos="33"/>
<name status="New" import="None" exported="True">Part One</name>
<item handle="edca4be2fcaf8" parent="7031beac91f75" root="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="26" wordCount="6" paraCount="1" cursorPos="36"/>
<name status="sf12341" import="ia857f0" exported="True">Part One</name>
</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"/>
<name status="1st Draft" import="None">A Folder</name>
<name status="sf12341" import="ia857f0">Sequel</name>
</item>
<item handle="6a2d6d5f4f401" parent="e7ded148d6e4a" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="75" wordCount="14" paraCount="1" cursorPos="279"/>
<name status="Notes" import="None" exported="True">Chapter One</name>
<item handle="bacb7059e3083" parent="e5e47ebf63b1c" root="e5e47ebf63b1c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="27" wordCount="5" paraCount="1" cursorPos="100"/>
<name status="sf12341" import="ia857f0" exported="True">Title Page</name>
</item>
<item handle="636b6aa9b697b" parent="e7ded148d6e4a" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="2429" wordCount="432" paraCount="14" cursorPos="219"/>
<name status="1st Draft" import="None" exported="True">Making a Scene</name>
<item handle="a520879ca0b45" parent="e5e47ebf63b1c" root="e5e47ebf63b1c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="299" wordCount="55" paraCount="2" cursorPos="104"/>
<name status="s90e6c9" import="ia857f0" exported="True">Chapter One</name>
</item>
<item handle="bc0cbd2a407f3" parent="e7ded148d6e4a" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<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">
<item handle="f6622b4617424" parent="None" root="f6622b4617424" order="2" type="ROOT" class="CHARACTER">
<meta expanded="True"/>
<name status="New" import="None">Characters</name>
<name status="sf12341" import="ia857f0">Characters</name>
</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"/>
<name status="New" import="None">Main Characters</name>
<name status="sf12341" import="ia857f0">Main Characters</name>
</item>
<item handle="14298de4d9524" parent="f7e2d9f330615" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta charCount="49" wordCount="9" paraCount="1" cursorPos="24"/>
<name status="New" import="Minor" exported="True">John Smith</name>
<item handle="14298de4d9524" parent="f7e2d9f330615" root="f6622b4617424" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="49" wordCount="9" paraCount="1" cursorPos="24"/>
<name status="sf12341" import="icfb3a5" exported="True">John Smith</name>
</item>
<item handle="bb2c23b3c42cc" parent="f7e2d9f330615" order="1" type="FILE" class="CHARACTER" layout="NOTE">
<meta charCount="55" wordCount="9" paraCount="1" cursorPos="25"/>
<name status="New" import="Major" exported="True">Jane Smith</name>
<item handle="bb2c23b3c42cc" parent="f7e2d9f330615" root="f6622b4617424" order="1" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="55" wordCount="9" paraCount="1" cursorPos="25"/>
<name status="sf12341" import="i2d7a54" exported="True">Jane Smith</name>
</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"/>
<name status="New" import="None">Locations</name>
<name status="sf12341" import="ia857f0">Locations</name>
</item>
<item handle="b3e74dbc1f584" parent="15c4492bd5107" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta charCount="76" wordCount="15" paraCount="1" cursorPos="20"/>
<name status="New" import="Main" exported="True">Earth</name>
<item handle="b3e74dbc1f584" parent="15c4492bd5107" root="15c4492bd5107" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="76" wordCount="15" paraCount="1" cursorPos="20"/>
<name status="sf12341" import="i56be10" exported="True">Earth</name>
</item>
<item handle="f1471bef9f2ae" parent="15c4492bd5107" order="1" type="FILE" class="WORLD" layout="NOTE">
<meta charCount="115" wordCount="24" paraCount="1" cursorPos="133"/>
<name status="New" import="Minor" exported="True">Space</name>
<item handle="f1471bef9f2ae" parent="15c4492bd5107" root="15c4492bd5107" order="1" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="115" wordCount="24" paraCount="1" cursorPos="133"/>
<name status="sf12341" import="icfb3a5" exported="True">Space</name>
</item>
<item handle="5eaea4e8cdee8" parent="15c4492bd5107" order="2" type="FILE" class="WORLD" layout="NOTE">
<meta charCount="28" wordCount="6" paraCount="1" cursorPos="45"/>
<name status="New" import="Major" exported="True">Mars</name>
<item handle="5eaea4e8cdee8" parent="15c4492bd5107" root="15c4492bd5107" order="2" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="28" wordCount="6" paraCount="1" cursorPos="45"/>
<name status="sf12341" import="i2d7a54" exported="True">Mars</name>
</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"/>
<name status="New" import="None">Archive</name>
<name status="sf12341" import="ia857f0">Archive</name>
</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"/>
<name status="New" import="None">Scenes</name>
<name status="sf12341" import="ia857f0">Scenes</name>
</item>
<item handle="8a5deb88c0e97" parent="ae9bf3c3ea159" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="315" wordCount="55" paraCount="1" cursorPos="322"/>
<name status="1st Draft" import="None" exported="True">Old File</name>
<item handle="8a5deb88c0e97" parent="ae9bf3c3ea159" root="6827118336ac1" order="0" type="FILE" class="ARCHIVE" layout="DOCUMENT">
<meta expanded="False" charCount="232" wordCount="42" paraCount="1" cursorPos="239"/>
<name status="s90e6c9" import="ia857f0" exported="True">Old File</name>
</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"/>
<name status="New" import="None">Trash</name>
<name status="sf12341" import="ia857f0">Trash</name>
</item>
<item handle="b8136a5a774a0" parent="98acd8c76c93a" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="30" wordCount="6" paraCount="1" cursorPos="36"/>
<name status="New" import="None" exported="True">Delete Me!</name>
<item handle="b8136a5a774a0" parent="98acd8c76c93a" root="98acd8c76c93a" order="0" type="FILE" class="TRASH" layout="DOCUMENT">
<meta expanded="False" charCount="30" wordCount="6" paraCount="1" cursorPos="36"/>
<name status="sf12341" import="ia857f0" exported="True">Delete Me!</name>
</item>
</content>
</novelWriterXML>
+1
View File
@@ -4,5 +4,6 @@
<comment>novelWriter Project</comment>
<sub-class-of type="application/xml"/>
<glob pattern="*.nwx"/>
<icon name="application-x-novelwriter-project"/>
</mime-type>
</mime-info>
+20 -4
View File
@@ -180,6 +180,26 @@ def nwGUI(qtbot, monkeypatch, fncDir, fncConf):
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
##
@@ -248,10 +268,6 @@ def nwOldProj(tmpDir):
return
##
# Useful Fixtures
##
@pytest.fixture(scope="session")
def ipsumText():
"""Return five paragraphs of Lorem Ipsum text.
+68 -68
View File
@@ -1,12 +1,12 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-03 21:36:12">
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-23 15:34:13">
<project>
<name>Lorem Ipsum</name>
<title>Lorem Ipsum</title>
<author>lipsum.com</author>
<saveCount>23</saveCount>
<saveCount>26</saveCount>
<autoCount>24</autoCount>
<editTime>1854</editTime>
<editTime>1863</editTime>
</project>
<settings>
<doBackup>False</doBackup>
@@ -31,102 +31,102 @@
<section></section>
</titleFormat>
<status>
<entry red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Note</entry>
<entry red="200" green="150" blue="0">Draft</entry>
<entry red="50" green="200" blue="0">Finished</entry>
<entry key="sbaa94f" count="3" red="100" green="100" blue="100">New</entry>
<entry key="s27bf7c" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s92a87b" count="5" red="200" green="150" blue="0">Draft</entry>
<entry key="sedd043" count="7" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Minor</entry>
<entry red="200" green="150" blue="0">Major</entry>
<entry red="50" green="200" blue="0">Main</entry>
<entry key="i613591" count="6" red="100" green="100" blue="100">New</entry>
<entry key="i560cbf" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i37861c" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="id6b1d0" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<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"/>
<name status="New" import="New">Novel</name>
<name status="sbaa94f" import="i613591">Novel</name>
</item>
<item handle="7a992350f3eb6" parent="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="230" wordCount="40" paraCount="3" cursorPos="148"/>
<name status="Finished" import="New" exported="True">Lorem Ipsum</name>
<item handle="7a992350f3eb6" parent="b3643d0f92e32" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="230" wordCount="40" paraCount="3" cursorPos="148"/>
<name status="sedd043" import="i613591" exported="True">Lorem Ipsum</name>
</item>
<item handle="8c58a65414c23" parent="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="1058" wordCount="176" paraCount="2" cursorPos="43"/>
<name status="Finished" import="New" exported="True">Front Matter</name>
<item handle="8c58a65414c23" parent="b3643d0f92e32" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="1058" wordCount="176" paraCount="2" cursorPos="43"/>
<name status="sedd043" import="i613591" exported="True">Front Matter</name>
</item>
<item handle="88d59a277361b" parent="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="584" wordCount="92" paraCount="1" cursorPos="4"/>
<name status="Draft" import="New" exported="True">Prologue</name>
<item handle="88d59a277361b" parent="b3643d0f92e32" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="584" wordCount="92" paraCount="1" cursorPos="4"/>
<name status="s92a87b" import="i613591" exported="True">Prologue</name>
</item>
<item handle="db7e733775d4d" parent="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="35" wordCount="6" paraCount="1" cursorPos="42"/>
<name status="New" import="New" exported="True">Act One</name>
<item handle="db7e733775d4d" parent="b3643d0f92e32" root="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="35" wordCount="6" paraCount="1" cursorPos="42"/>
<name status="sbaa94f" import="i613591" exported="True">Act One</name>
</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"/>
<name status="Draft" import="New">Chapter One</name>
<name status="s92a87b" import="i613591">Chapter One</name>
</item>
<item handle="fb609cd8319dc" parent="45e6b01ca35c1" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="419" wordCount="67" paraCount="1" cursorPos="56"/>
<name status="Draft" import="New" exported="True">Chapter One</name>
<item handle="fb609cd8319dc" parent="45e6b01ca35c1" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="419" wordCount="67" paraCount="1" cursorPos="56"/>
<name status="s92a87b" import="i613591" exported="True">Chapter One</name>
</item>
<item handle="88243afbe5ed8" parent="45e6b01ca35c1" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="2758" wordCount="404" paraCount="4" cursorPos="1528"/>
<name status="Finished" import="New" exported="True">Scene One</name>
<item handle="88243afbe5ed8" parent="45e6b01ca35c1" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="2758" wordCount="404" paraCount="4" cursorPos="1528"/>
<name status="sedd043" import="i613591" exported="True">Scene One</name>
</item>
<item handle="f96ec11c6a3da" parent="45e6b01ca35c1" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="4043" wordCount="600" paraCount="6" cursorPos="2335"/>
<name status="Finished" import="New" exported="True">Scene Two</name>
<item handle="f96ec11c6a3da" parent="45e6b01ca35c1" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="4043" wordCount="600" paraCount="6" cursorPos="2335"/>
<name status="sedd043" import="i613591" exported="True">Scene Two</name>
</item>
<item handle="846352075de7d" parent="b3643d0f92e32" order="5" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="631" wordCount="109" paraCount="3" cursorPos="376"/>
<name status="New" import="New" exported="False">Interlude</name>
<item handle="846352075de7d" parent="b3643d0f92e32" root="b3643d0f92e32" order="5" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="631" wordCount="109" paraCount="3" cursorPos="376"/>
<name status="sbaa94f" import="i613591" exported="False">Interlude</name>
</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"/>
<name status="Draft" import="New">Chapter Two</name>
<name status="s92a87b" import="i613591">Chapter Two</name>
</item>
<item handle="441420a886d82" parent="6bd935d2490cd" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="477" wordCount="70" paraCount="1" cursorPos="56"/>
<name status="Draft" import="New" exported="True">Chapter Two</name>
<item handle="441420a886d82" parent="6bd935d2490cd" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="477" wordCount="70" paraCount="1" cursorPos="56"/>
<name status="s92a87b" import="i613591" exported="True">Chapter Two</name>
</item>
<item handle="eb103bc70c90c" parent="6bd935d2490cd" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="3006" wordCount="439" paraCount="4" cursorPos="57"/>
<name status="Finished" import="New" exported="True">Scene Three</name>
<item handle="eb103bc70c90c" parent="6bd935d2490cd" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="3006" wordCount="439" paraCount="4" cursorPos="57"/>
<name status="sedd043" import="i613591" exported="True">Scene Three</name>
</item>
<item handle="f8c0562e50f1b" parent="6bd935d2490cd" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="3839" wordCount="563" paraCount="6" cursorPos="56"/>
<name status="Finished" import="New" exported="True">Scene Four</name>
<item handle="f8c0562e50f1b" parent="6bd935d2490cd" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="3839" wordCount="563" paraCount="6" cursorPos="56"/>
<name status="sedd043" import="i613591" exported="True">Scene Four</name>
</item>
<item handle="47666c91c7ccf" parent="6bd935d2490cd" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="3644" wordCount="543" paraCount="5" cursorPos="351"/>
<name status="Finished" import="New" exported="True">Scene Five</name>
<item handle="47666c91c7ccf" parent="6bd935d2490cd" root="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="3644" wordCount="543" paraCount="5" cursorPos="351"/>
<name status="sedd043" import="i613591" exported="True">Scene Five</name>
</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"/>
<name status="New" import="New">Characters</name>
<name status="sbaa94f" import="i613591">Characters</name>
</item>
<item handle="4c4f28287af27" parent="67a8707f2f249" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta charCount="1864" wordCount="284" paraCount="3" cursorPos="1883"/>
<name status="New" import="New" exported="True">Mr. Nobody</name>
<item handle="4c4f28287af27" parent="67a8707f2f249" root="67a8707f2f249" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="False" charCount="1864" wordCount="284" paraCount="3" cursorPos="1883"/>
<name status="sbaa94f" import="i613591" exported="True">Mr. Nobody</name>
</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"/>
<name status="New" import="New">Plot</name>
<name status="sbaa94f" import="i613591">Plot</name>
</item>
<item handle="2426c6f0ca922" parent="6c6afb1247750" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta charCount="1369" wordCount="195" paraCount="2" cursorPos="1387"/>
<name status="New" import="New" exported="True">Main</name>
<item handle="2426c6f0ca922" parent="6c6afb1247750" root="6c6afb1247750" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta expanded="False" charCount="1369" wordCount="195" paraCount="2" cursorPos="1387"/>
<name status="sbaa94f" import="i613591" exported="True">Main</name>
</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"/>
<name status="New" import="New">World</name>
<name status="sbaa94f" import="i613591">World</name>
</item>
<item handle="04468803b92e1" parent="60bdf227455cc" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta charCount="1770" wordCount="259" paraCount="3" cursorPos="1792"/>
<name status="New" import="New" exported="True">Ancient Europe</name>
<item handle="04468803b92e1" parent="60bdf227455cc" root="60bdf227455cc" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="False" charCount="1770" wordCount="259" paraCount="3" cursorPos="1792"/>
<name status="sbaa94f" import="i613591" exported="True">Ancient Europe</name>
</item>
</content>
</novelWriterXML>
+30 -30
View File
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-03 21:36:18">
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-23 15:34:21">
<project>
<name>Test Minimal</name>
<title>Minimal</title>
<author>Jane Doe</author>
<author>John Doh</author>
<saveCount>14</saveCount>
<saveCount>17</saveCount>
<autoCount>2</autoCount>
<editTime>135</editTime>
<editTime>150</editTime>
</project>
<settings>
<doBackup>True</doBackup>
@@ -29,50 +29,50 @@
<section></section>
</titleFormat>
<status>
<entry red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Note</entry>
<entry red="200" green="150" blue="0">Draft</entry>
<entry red="50" green="200" blue="0">Finished</entry>
<entry key="s72322d" count="5" red="100" green="100" blue="100">New</entry>
<entry key="sb6adc7" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s2ae76d" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s541953" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Minor</entry>
<entry red="200" green="150" blue="0">Major</entry>
<entry red="50" green="200" blue="0">Main</entry>
<entry key="iffcacb" count="3" red="100" green="100" blue="100">New</entry>
<entry key="i6b130b" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="iece803" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i5ba06e" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<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"/>
<name status="New" import="New">Novel</name>
<name status="s72322d" import="iffcacb">Novel</name>
</item>
<item handle="a35baf2e93843" parent="a508bb932959c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="28" wordCount="6" paraCount="1" cursorPos="33"/>
<name status="New" import="New" exported="True">Title Page</name>
<item handle="a35baf2e93843" parent="a508bb932959c" root="a508bb932959c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="28" wordCount="6" paraCount="1" cursorPos="33"/>
<name status="s72322d" import="iffcacb" exported="True">Title Page</name>
</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"/>
<name status="New" import="New">New Chapter</name>
<name status="s72322d" import="iffcacb">New Chapter</name>
</item>
<item handle="f5ab3e30151e1" parent="a6d311a93600a" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="11" wordCount="2" paraCount="0" cursorPos="16"/>
<name status="New" import="New" exported="True">New Chapter</name>
<item handle="f5ab3e30151e1" parent="a6d311a93600a" root="a508bb932959c" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="11" wordCount="2" paraCount="0" cursorPos="16"/>
<name status="s72322d" import="iffcacb" exported="True">New Chapter</name>
</item>
<item handle="8c659a11cd429" parent="a6d311a93600a" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="9" wordCount="2" paraCount="0" cursorPos="15"/>
<name status="New" import="New" exported="True">New Scene</name>
<item handle="8c659a11cd429" parent="a6d311a93600a" root="a508bb932959c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="9" wordCount="2" paraCount="0" cursorPos="15"/>
<name status="s72322d" import="iffcacb" exported="True">New Scene</name>
</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"/>
<name status="New" import="New">Plot</name>
<name status="s72322d" import="iffcacb">Plot</name>
</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"/>
<name status="New" import="New">Characters</name>
<name status="s72322d" import="iffcacb">Characters</name>
</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"/>
<name status="New" import="New">World</name>
<name status="s72322d" import="iffcacb">World</name>
</item>
</content>
</novelWriterXML>
-1
View File
@@ -29,7 +29,6 @@ class MockGuiMain():
def __init__(self):
self.mainConf = None
self.hasProject = True
self.theIndex = None
self.theProject = None
self.statusBar = MockStatusBar()
+121 -95
View File
@@ -1,99 +1,125 @@
{
"tagIndex": {
"Bod": [3, "4c4f28287af27", "CHARACTER", "T000001"],
"Main": [3, "2426c6f0ca922", "PLOT", "T000001"],
"Europe": [3, "04468803b92e1", "WORLD", "T000001"]
},
"refIndex": {
"fb609cd8319dc": {
"T000001": [[3, "@pov", "Bod"], [4, "@plot", "Main"], [5, "@location", "Europe"]]
"tagsIndex": {
"Bod": {"handle": "4c4f28287af27", "heading": "T000001", "class": "CHARACTER"},
"Main": {"handle": "2426c6f0ca922", "heading": "T000001", "class": "PLOT"},
"Europe": {"handle": "04468803b92e1", "heading": "T000001", "class": "WORLD"}
},
"88243afbe5ed8": {
"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"]]
"itemIndex": {
"7a992350f3eb6": {
"level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Lorem Ipsum", "tag": "", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""}
}
},
"8c58a65414c23": {
"level": "H0",
"headings": {
"T000000": {"level": "H0", "title": "", "tag": "", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""}
}
},
"88d59a277361b": {
"level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Prologue", "tag": "", "cCount": 584, "wCount": 92, "pCount": 1, "synopsis": "Explanation from the lipsum.com website."}
}
},
"db7e733775d4d": {
"level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Act One", "tag": "", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""}
}
},
"fb609cd8319dc": {
"level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Chapter One", "tag": "", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
},
"88243afbe5ed8": {
"level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene One", "tag": "", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."},
"T000013": {"level": "H4", "title": "Scene One, Section Two", "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
},
"f96ec11c6a3da": {
"level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene Two", "tag": "", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."},
"T000015": {"level": "H4", "title": "Scene Two, Section Two", "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
},
"846352075de7d": {
"level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Why do we use it?", "tag": "", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""}
}
},
"441420a886d82": {
"level": "H2",
"headings": {
"T000001": {"level": "H2", "title": "Chapter Two", "tag": "", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
},
"eb103bc70c90c": {
"level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene Three", "tag": "", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
},
"f8c0562e50f1b": {
"level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene Four", "tag": "", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
},
"47666c91c7ccf": {
"level": "H3",
"headings": {
"T000001": {"level": "H3", "title": "Scene Five", "tag": "", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."}
},
"references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
}
},
"4c4f28287af27": {
"level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Nobody Owens", "tag": "Bod", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""}
},
"references": {
"T000001": {"Main": "@plot"}
}
},
"2426c6f0ca922": {
"level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Main Plot", "tag": "Main", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""}
}
},
"04468803b92e1": {
"level": "H1",
"headings": {
"T000001": {"level": "H1", "title": "Ancient Europe", "tag": "Europe", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""}
}
}
}
},
"fileIndex": {
"7a992350f3eb6": {
"T000001": {"level": "H1", "title": "Lorem Ipsum", "layout": "DOCUMENT", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""}
},
"8c58a65414c23": {
"T000000": {"level": "H0", "title": "", "layout": "DOCUMENT", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""}
},
"88d59a277361b": {
"T000001": {"level": "H2", "title": "Prologue", "layout": "DOCUMENT", "cCount": 584, "wCount": 92, "pCount": 1, "synopsis": "Explanation from the lipsum.com website."}
},
"db7e733775d4d": {
"T000001": {"level": "H1", "title": "Act One", "layout": "DOCUMENT", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""}
},
"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."}
},
"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."},
"T000013": {"level": "H4", "title": "Scene One, Section Two", "layout": "DOCUMENT", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""}
},
"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."},
"T000015": {"level": "H4", "title": "Scene Two, Section Two", "layout": "DOCUMENT", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""}
},
"846352075de7d": {
"T000001": {"level": "H2", "title": "Why do we use it?", "layout": "DOCUMENT", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""}
},
"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."}
},
"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."}
},
"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."}
},
"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."}
},
"4c4f28287af27": {
"T000001": {"level": "H1", "title": "Nobody Owens", "layout": "NOTE", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""}
},
"2426c6f0ca922": {
"T000001": {"level": "H1", "title": "Main Plot", "layout": "NOTE", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""}
},
"04468803b92e1": {
"T000001": {"level": "H1", "title": "Ancient Europe", "layout": "NOTE", "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'?>
<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>
<name>Test Custom</name>
<title>Test Novel</title>
@@ -29,110 +29,106 @@
<section></section>
</titleFormat>
<status>
<entry red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Note</entry>
<entry red="200" green="150" blue="0">Draft</entry>
<entry red="50" green="200" blue="0">Finished</entry>
<entry key="s000008" count="15" red="100" green="100" blue="100">New</entry>
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Minor</entry>
<entry red="200" green="150" blue="0">Major</entry>
<entry red="50" green="200" blue="0">Main</entry>
<entry key="i00000c" count="7" red="100" green="100" blue="100">New</entry>
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="23">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL">
<content count="22">
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="New" import="None">Novel</name>
<name status="s000008" import="i00000c">Novel</name>
</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"/>
<name status="New" import="None">Plot</name>
<name status="s000008" import="i00000c">Plot</name>
</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"/>
<name status="New" import="None">Characters</name>
<name status="s000008" import="i00000c">Characters</name>
</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"/>
<name status="New" import="None">Locations</name>
<name status="s000008" import="i00000c">Locations</name>
</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"/>
<name status="New" import="None">Timeline</name>
<name status="s000008" import="i00000c">Archive</name>
</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"/>
<name status="New" import="None">Objects</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>
<name status="s000008" import="i00000c">Trash</name>
</item>
</content>
</novelWriterXML>
@@ -1,5 +1,5 @@
<?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>
<name>Test Custom</name>
<title>Test Novel</title>
@@ -29,74 +29,82 @@
<section></section>
</titleFormat>
<status>
<entry red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Note</entry>
<entry red="200" green="150" blue="0">Draft</entry>
<entry red="50" green="200" blue="0">Finished</entry>
<entry key="s000008" count="9" red="100" green="100" blue="100">New</entry>
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Minor</entry>
<entry red="200" green="150" blue="0">Major</entry>
<entry red="50" green="200" blue="0">Main</entry>
<entry key="i00000c" count="7" red="100" green="100" blue="100">New</entry>
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="14">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL">
<content count="16">
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="New" import="None">Novel</name>
<name status="s000008" import="i00000c">Novel</name>
</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"/>
<name status="New" import="None">Plot</name>
<name status="s000008" import="i00000c">Plot</name>
</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"/>
<name status="New" import="None">Characters</name>
<name status="s000008" import="i00000c">Characters</name>
</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"/>
<name status="New" import="None">Locations</name>
<name status="s000008" import="i00000c">Locations</name>
</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"/>
<name status="New" import="None">Timeline</name>
<name status="s000008" import="i00000c">Archive</name>
</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"/>
<name status="New" import="None">Objects</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>
<name status="s000008" import="i00000c">Trash</name>
</item>
</content>
</novelWriterXML>
@@ -1,8 +1,9 @@
<?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>
<name>New Project</name>
<title></title>
<title>New Novel</title>
<author>Jane Doe</author>
<saveCount>2</saveCount>
<autoCount>1</autoCount>
<editTime>0</editTime>
@@ -27,58 +28,58 @@
<section></section>
</titleFormat>
<status>
<entry red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Note</entry>
<entry red="200" green="150" blue="0">Draft</entry>
<entry red="50" green="200" blue="0">Finished</entry>
<entry key="s000008" count="5" red="100" green="100" blue="100">New</entry>
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Minor</entry>
<entry red="200" green="150" blue="0">Major</entry>
<entry red="50" green="200" blue="0">Main</entry>
<entry key="i00000c" count="3" red="100" green="100" blue="100">New</entry>
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<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"/>
<name status="New" import="New">Novel</name>
<name status="s000008" import="i00000c">Novel</name>
</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"/>
<name status="New" import="New">Plot</name>
<name status="s000008" import="i00000c">Plot</name>
</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"/>
<name status="New" import="New">Characters</name>
<name status="s000008" import="i00000c">Characters</name>
</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"/>
<name status="New" import="New">World</name>
<name status="s000008" import="i00000c">World</name>
</item>
<item handle="25fc0e7096fc6" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="New" exported="True">Title Page</name>
<item handle="0000000000014" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Title Page</name>
</item>
<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"/>
<name status="New" import="New">New Chapter</name>
<name status="s000008" import="i00000c">New Chapter</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 handle="0000000000016" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Chapter</name>
</item>
<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 handle="0000000000017" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Scene</name>
</item>
<item handle="1a6562590ef19" parent="31489056e0916" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Hello</name>
<item handle="0000000000028" parent="31489056e0916" root="None" order="0" type="FILE" class="NO_CLASS" layout="NO_LAYOUT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="None" import="None" exported="True">Hello</name>
</item>
<item handle="031b4af5197ec" parent="71ee45a3c0db9" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="None" exported="True">Jane</name>
<item handle="0000000000029" parent="71ee45a3c0db9" root="None" order="0" type="FILE" class="NO_CLASS" layout="NO_LAYOUT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="None" import="None" exported="True">Jane</name>
</item>
</content>
</novelWriterXML>
@@ -1,9 +1,9 @@
<?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>
<name>New Project</name>
<title></title>
<saveCount>1</saveCount>
<saveCount>2</saveCount>
<autoCount>1</autoCount>
<editTime>0</editTime>
</project>
@@ -27,50 +27,50 @@
<section></section>
</titleFormat>
<status>
<entry red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Note</entry>
<entry red="200" green="150" blue="0">Draft</entry>
<entry red="50" green="200" blue="0">Finished</entry>
<entry key="s000008" count="5" red="100" green="100" blue="100">New</entry>
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Minor</entry>
<entry red="200" green="150" blue="0">Major</entry>
<entry red="50" green="200" blue="0">Main</entry>
<entry key="i00000c" count="3" red="100" green="100" blue="100">New</entry>
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<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"/>
<name status="New" import="New">Novel</name>
<name status="s000008" import="i00000c">Novel</name>
</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"/>
<name status="New" import="New">Plot</name>
<name status="s000008" import="i00000c">Plot</name>
</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"/>
<name status="New" import="New">Characters</name>
<name status="s000008" import="i00000c">Characters</name>
</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"/>
<name status="New" import="New">World</name>
<name status="s000008" import="i00000c">Locations</name>
</item>
<item handle="25fc0e7096fc6" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<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">
<item handle="0000000000017" parent="None" root="0000000000017" order="0" type="ROOT" class="ARCHIVE">
<meta expanded="False"/>
<name status="New" import="New">New Chapter</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>
<name status="s000008" import="i00000c">Archive</name>
</item>
</content>
</novelWriterXML>
@@ -1,8 +1,9 @@
<?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>
<name>New Project</name>
<title></title>
<title>New Novel</title>
<author>Jane Doe</author>
<saveCount>2</saveCount>
<autoCount>1</autoCount>
<editTime>0</editTime>
@@ -27,66 +28,82 @@
<section></section>
</titleFormat>
<status>
<entry red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Note</entry>
<entry red="200" green="150" blue="0">Draft</entry>
<entry red="50" green="200" blue="0">Finished</entry>
<entry key="s000008" count="6" red="100" green="100" blue="100">New</entry>
<entry key="s000009" count="0" red="200" green="50" blue="0">Note</entry>
<entry key="s00000a" count="0" red="200" green="150" blue="0">Draft</entry>
<entry key="s00000b" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry red="100" green="100" blue="100">New</entry>
<entry red="200" green="50" blue="0">Minor</entry>
<entry red="200" green="150" blue="0">Major</entry>
<entry red="50" green="200" blue="0">Main</entry>
<entry key="i00000c" count="10" red="100" green="100" blue="100">New</entry>
<entry key="i00000d" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i00000e" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i00000f" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content count="12">
<item handle="73475cb40a568" parent="None" order="0" type="ROOT" class="NOVEL">
<content count="16">
<item handle="0000000000010" parent="None" root="0000000000010" order="0" type="ROOT" class="NOVEL">
<meta expanded="False"/>
<name status="New" import="New">Novel</name>
<name status="s000008" import="i00000c">Novel</name>
</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"/>
<name status="New" import="New">Plot</name>
<name status="s000008" import="i00000c">Plot</name>
</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"/>
<name status="New" import="New">Characters</name>
<name status="s000008" import="i00000c">Characters</name>
</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"/>
<name status="New" import="New">World</name>
<name status="s000008" import="i00000c">World</name>
</item>
<item handle="25fc0e7096fc6" parent="73475cb40a568" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="New" import="New" exported="True">Title Page</name>
<item handle="0000000000014" parent="0000000000010" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">Title Page</name>
</item>
<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"/>
<name status="New" import="New">New Chapter</name>
<name status="s000008" import="i00000c">New Chapter</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 handle="0000000000016" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Chapter</name>
</item>
<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 handle="0000000000017" parent="0000000000015" root="0000000000010" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="0" wordCount="0" paraCount="0" cursorPos="0"/>
<name status="s000008" import="i00000c" exported="True">New Scene</name>
</item>
<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"/>
<name status="New" import="None">Timeline</name>
<name status="s000008" import="i00000c">Novel</name>
</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"/>
<name status="New" import="None">Object</name>
<name status="s000008" import="i00000c">Plot</name>
</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"/>
<name status="New" import="None">Custom1</name>
<name status="s000008" import="i00000c">Characters</name>
</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"/>
<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>
</content>
</novelWriterXML>
@@ -1,5 +1,5 @@
%%~name: New Scene
%%~path: 31489056e0916/0e17daca5f3e1
%%~path: 000000000000d/000000000000f
%%~kind: NOVEL/DOCUMENT
# Novel
@@ -1,5 +1,5 @@
%%~name: New File
%%~path: 71ee45a3c0db9/1a6562590ef19
%%~name: New Note
%%~path: 000000000000a/0000000000020
%%~kind: CHARACTER/NOTE
# Jane Doe
@@ -1,5 +1,5 @@
%%~name: New File
%%~path: 44cb730c42048/031b4af5197ec
%%~name: New Note
%%~path: 0000000000009/0000000000021
%%~kind: PLOT/NOTE
# Main Plot
@@ -1,5 +1,5 @@
%%~name: New File
%%~path: 811786ad1ae74/41cfc0d1f2d12
%%~name: New Note
%%~path: 000000000000b/0000000000022
%%~kind: WORLD/NOTE
# Main Location

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