Bring patch 1.6.4 changes into main

This commit is contained in:
Veronica Berglyd Olsen
2022-09-29 19:59:21 +02:00
179 changed files with 17501 additions and 9764 deletions
+2 -2
View File
@@ -29,5 +29,5 @@ jobs:
flake8 tests --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Coding Style Violations
run: |
flake8 novelwriter --count --max-line-length=99 --ignore E221,E226,E228,E241 --show-source --statistics
flake8 tests --count --max-line-length=99 --ignore E221,E226,E228,E241 --show-source --statistics
flake8 novelwriter --count --max-line-length=99 --ignore E133,E221,E226,E228,E241,W503 --show-source --statistics
flake8 tests --count --max-line-length=99 --ignore E133,E221,E226,E228,E241,W503 --show-source --statistics
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
testLinux:
strategy:
matrix:
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
python-version: ["3.7", "3.8", "3.9", "3.10"]
runs-on: ubuntu-latest
steps:
- name: Python Setup
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- name: Python Setup
uses: actions/setup-python@v2
with:
python-version: 3.9
python-version: "3.10"
architecture: x64
- name: Install Packages
run: |
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- name: Python Setup
uses: actions/setup-python@v2
with:
python-version: 3.9
python-version: "3.10"
architecture: x64
- name: Checkout Source
uses: actions/checkout@v2
+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.4 [2022-09-29]
### Release Notes
+103 -47
View File
@@ -2,75 +2,131 @@
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
Examples of behavior that contributes to a positive environment for our
community include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior by participants include:
Examples of unacceptable behavior include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
## Enforcement Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project leader at issues (at) novelwriter.io. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
reported to the community leaders responsible for enforcement at issues (at)
novelwriter.io. All complaints will be reviewed and investigated promptly and
fairly.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
+5 -2
View File
@@ -11,11 +11,14 @@
## Translations
The default language is English (UK) with English (US) as an option.
* Dutch: Martijn van der Kleijn (@mvdkleijn)
* French: Jan Lüdke (@jyhelle)
* 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)
## Libraries
@@ -31,7 +34,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)
+5 -3
View File
@@ -29,11 +29,13 @@ 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 in Python 3 (3.6+) using Qt5 and PyQt5 (5.3+). It is developed on Linux,
but should in principle work fine on other operating systems as well as long as dependencies are
met. It is regularly tested on Debian and Ubuntu Linux, Windows, and macOS.
The application is written with Python 3 (3.7+) using Qt5 and PyQt5 (5.3+). It is developed on
Linux, but should in principle work fine on other operating systems as well as long as dependencies
are met. It is regularly tested on Debian and Ubuntu Linux, Windows, and macOS.
## Installation
+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
+1 -1
View File
@@ -115,7 +115,7 @@ Windows
-------
First, make sure you have Python installed on your system. If you don't, you can download it from
`python.org`_. Python 3.6 or higher is required, but it is recommended that you install the latest
`python.org`_. Python 3.7 or higher is required, but it is recommended that you install the latest
version.
Make sure you select the "Add Python to PATH" option during installation, otherwise the ``python``
+22 -3
View File
@@ -11,13 +11,32 @@ changes require minor actions from the user.
The key changes in the formats are listed below, as well as the user actions required where
applicable.
.. caution::
When you update a project from one format version to the next, the project can no longer be
opened by a version of novelWriter prior to the version where the new file format was
introduced. You will get a notification about any updates to your project file format and will
have the option to decline the upgrade.
.. _a_prjfmt_1_4:
Format 1.4 Changes
==================
This project format was introduced in novelWriter version 1.7.
This format changes the way project items (folders, documents and notes) are stored. It is a more
compact format that is simpler and faster to parse, and easier to extend. The conversion is done
automatically the first time a project is loaded. No user action is required.
.. _a_prjfmt_1_3:
Format 1.3 Changes
==================
This project format vas introduces in novelWriter version 1.5.
This project format was introduced in novelWriter version 1.5.
With this format, the number of document layouts was reduced from 8 to 2. The conversion of
document layouts is performed automatically when the project is opened.
@@ -55,7 +74,7 @@ should be used only a few places in any given project. These are as follows:
Format 1.2 Changes
==================
This project format was introduces in novelWriter version 0.10.
This project format was introduced in novelWriter version 0.10.
With this format, the way auto-replace entries were stored in the main project XML file changed.
Opening an old project automatically converts the storage format up to and including version 1.1.1.
@@ -69,7 +88,7 @@ auto-replace is not being used, can still be opened in novelWriter as of version
Format 1.1 Changes
==================
This project format was introduces in novelWriter version 0.7.
This project format was introduced in novelWriter version 0.7.
With this format, the ``content`` folder was introduced in the project storage. Previously, all
novelWriter documents were saved in a series of folders numbered from ``data_0`` to ``data_f``.
+2 -6
View File
@@ -25,7 +25,7 @@ The main shorcuts are as follows:
":kbd:`Alt`:kbd:`4`", "Switch focus to outline view. On Windows, use :kbd:`Ctrl`:kbd:`Alt`:kbd:`4`."
":kbd:`Alt`:kbd:`Left`", "Move backward in the view history of the document viewer."
":kbd:`Alt`:kbd:`Right`", "Move forward in the view history of the document viewer."
":kbd:`Ctrl`:kbd:`.`", "Open menu to correct word under cursor."
":kbd:`Ctrl`:kbd:`.`", "Open the context menu in the project tree or the document editor."
":kbd:`Ctrl`:kbd:`,`", "Open the :guilabel:`Preferences` dialog."
":kbd:`Ctrl`:kbd:`/`", "Toggle block format as comment."
":kbd:`Ctrl`:kbd:`0`", "Remove block formatting for block under cursor."
@@ -42,13 +42,12 @@ The main shorcuts are as follows:
":kbd:`Ctrl`:kbd:`B`", "Format selected text, or word under cursor, with strong emphasis (bold)."
":kbd:`Ctrl`:kbd:`C`", "Copy selected text to clipboard."
":kbd:`Ctrl`:kbd:`D`", "Strikethrough selected text, or word under cursor."
":kbd:`Ctrl`:kbd:`E`", "If in the project tree, edit a document or folder settings."
":kbd:`Ctrl`:kbd:`F`", "Open the search bar and search for the selected word, if any is selected."
":kbd:`Ctrl`:kbd:`G`", "Find next occurrence of search word in current document."
":kbd:`Ctrl`:kbd:`H`", "Open the search and replace bar and search for the selected word, if any is selected. (On Mac, this is :kbd:`Cmd`:kbd:`=`.)"
":kbd:`Ctrl`:kbd:`I`", "Format selected text, or word under cursor, with emphasis (italic)."
":kbd:`Ctrl`:kbd:`K`", "Activate the insert commands. The commands are listed in :ref:`a_kb_ins`."
":kbd:`Ctrl`:kbd:`N`", "Create new document."
":kbd:`Ctrl`:kbd:`N`", "Create new project item."
":kbd:`Ctrl`:kbd:`O`", "Open selected document."
":kbd:`Ctrl`:kbd:`Q`", "Exit novelWriter."
":kbd:`Ctrl`:kbd:`R`", "If in the project tree, open a document for viewing. If the editor has focus, open current document for viewing."
@@ -59,7 +58,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."
@@ -73,7 +71,6 @@ The main shorcuts are as follows:
":kbd:`Ctrl`:kbd:`Shift`:kbd:`A`", "Select all text in current paragraph."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`G`", "Find previous occurrence of search word in current document."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`I`", "Import text to the current document from a text file."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`N`", "Create new folder."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`O`", "Open a project."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`R`", "Close the document viewer."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`S`", "Save the current project."
@@ -88,7 +85,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
+4521
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -60,9 +60,9 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__version__ = "1.6.4"
__hexversion__ = "0x010604f0"
__date__ = "2022-09-29"
__version__ = "1.7-beta1"
__hexversion__ = "0x010700b1"
__date__ = "2022-05-17"
__status__ = "Stable"
__domain__ = "novelwriter.io"
__url__ = "https://novelwriter.io"
@@ -209,9 +209,9 @@ def main(sysArgs=None):
# Check Packages and Versions
errorData = []
errorCode = 0
if sys.hexversion < 0x030600f0:
if sys.hexversion < 0x030700f0:
errorData.append(
"At least Python 3.6 is required, found %s" % CONFIG.verPyString
"At least Python 3.7 is required, found %s" % CONFIG.verPyString
)
errorCode |= 0x04
if CONFIG.verQtValue < 50300:
+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,15 @@ 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
deco_doc_h0 = nw_deco-h0.svg
deco_doc_h1 = nw_deco-h1.svg
deco_doc_h2 = nw_deco-h2.svg
deco_doc_h3 = nw_deco-h3.svg
deco_doc_h4 = nw_deco-h4.svg
deco_doc_more = nw_deco-noveltree-more.svg
@@ -0,0 +1,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,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="4"
height="24"
viewBox="0 0 4 24"
id="svg2161"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata2167">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs2165" />
<rect
style="fill:#848484;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
id="rect1955"
width="4"
height="22"
x="0"
y="1"
rx="2"
ry="2" />
</svg>

After

Width:  |  Height:  |  Size: 902 B

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="4"
height="24"
viewBox="0 0 4 24"
id="svg2161"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata2167">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs2165" />
<rect
style="fill:#99cc99;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
id="rect1955"
width="4"
height="22"
x="0"
y="1"
rx="2"
ry="2" />
</svg>

After

Width:  |  Height:  |  Size: 902 B

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="20"
height="24"
viewBox="0 0 20 24"
id="svg2161"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata2167">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs2165" />
<rect
style="fill:#d64848;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
id="rect1955"
width="4"
height="22"
x="16"
y="1"
rx="2"
ry="2" />
</svg>

After

Width:  |  Height:  |  Size: 905 B

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="36"
height="24"
viewBox="0 0 36 24"
id="svg2161"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata2167">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs2165" />
<rect
style="fill:#6699cc;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
id="rect1955"
width="4"
height="22"
x="32"
y="1"
rx="2"
ry="2" />
</svg>

After

Width:  |  Height:  |  Size: 905 B

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="52"
height="24"
viewBox="0 0 52 24"
id="svg2161"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata2167">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs2165" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
id="rect1955"
width="4"
height="22"
x="48"
y="1"
rx="2"
ry="2" />
</svg>

After

Width:  |  Height:  |  Size: 905 B

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

After

Width:  |  Height:  |  Size: 928 B

@@ -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,15 @@ 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
deco_doc_h0 = nw_deco-h0.svg
deco_doc_h1 = nw_deco-h1.svg
deco_doc_h2 = nw_deco-h2.svg
deco_doc_h3 = nw_deco-h3.svg
deco_doc_h4 = nw_deco-h4.svg
deco_doc_more = nw_deco-noveltree-more.svg
@@ -0,0 +1,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,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="4"
height="24"
viewBox="0 0 4 24"
id="svg2161"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata2167">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs2165" />
<rect
style="fill:#848484;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
id="rect1955"
width="4"
height="22"
x="0"
y="1"
rx="2"
ry="2" />
</svg>

After

Width:  |  Height:  |  Size: 902 B

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="4"
height="24"
viewBox="0 0 4 24"
id="svg2161"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata2167">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs2165" />
<rect
style="fill:#718c00;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
id="rect1955"
width="4"
height="22"
x="0"
y="1"
rx="2"
ry="2" />
</svg>

After

Width:  |  Height:  |  Size: 902 B

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="20"
height="24"
viewBox="0 0 20 24"
id="svg2161"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata2167">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs2165" />
<rect
style="fill:#c82829;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
id="rect1955"
width="4"
height="22"
x="16"
y="1"
rx="2"
ry="2" />
</svg>

After

Width:  |  Height:  |  Size: 905 B

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="36"
height="24"
viewBox="0 0 36 24"
id="svg2161"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata2167">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs2165" />
<rect
style="fill:#4271ae;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
id="rect1955"
width="4"
height="22"
x="32"
y="1"
rx="2"
ry="2" />
</svg>

After

Width:  |  Height:  |  Size: 905 B

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.2"
width="52"
height="24"
viewBox="0 0 52 24"
id="svg2161"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<metadata
id="metadata2167">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs2165" />
<rect
style="fill:#333333;fill-opacity:1;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:0.2"
id="rect1955"
width="4"
height="22"
x="48"
y="1"
rx="2"
ry="2" />
</svg>

After

Width:  |  Height:  |  Size: 905 B

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

After

Width:  |  Height:  |  Size: 928 B

@@ -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

+100
View File
@@ -0,0 +1,100 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus diam nibh, tincidunt et quam at, fringilla malesuada risus. Nullam eleifend, sem nec varius tincidunt, urna mi varius dolor, sit amet gravida risus eros at purus. Etiam vehicula hendrerit elit, sit amet pulvinar dolor viverra sed. Curabitur metus ex, gravida at sodales sed, tristique eget diam. Suspendisse ultricies lorem sed ullamcorper rutrum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis tempus magna mi, sit amet sagittis arcu viverra ut.
Fusce in pretium tellus. Donec finibus vitae arcu at consectetur. Suspendisse et ultricies nisl. Integer et nisi vel sem aliquam scelerisque. Curabitur placerat massa et mi feugiat vulputate. In ac orci vitae risus dictum porttitor quis ut dui. Vivamus egestas condimentum risus quis fermentum. Nam lorem ante, rhoncus vel porttitor in, sodales in purus. Mauris ultricies ex metus, molestie ullamcorper arcu suscipit id.
Cras dapibus porta eros, a tincidunt sapien mattis sit amet. Pellentesque ac metus in dui accumsan tristique eget eu tellus. Nam laoreet sapien vitae hendrerit ullamcorper. Mauris accumsan semper dui vitae pellentesque. Donec non velit cursus, eleifend nulla a, porttitor tellus. Donec elementum lobortis imperdiet. Etiam fermentum pretium arcu, sed consequat dui mattis nec. Mauris cursus fringilla magna, at pharetra justo. Sed tincidunt consequat urna, quis varius lacus dapibus id. Fusce sagittis lorem vitae sodales tempor. Ut vel feugiat lacus, in iaculis risus. Aliquam viverra tortor nec nibh tristique varius. Mauris interdum leo vitae massa sagittis venenatis.
Quisque cursus eu orci at viverra. Donec libero libero, sagittis a sagittis vitae, efficitur sed nibh. Donec imperdiet malesuada est. Etiam consequat quam arcu, quis egestas libero sagittis a. Donec purus urna, volutpat id ante nec, iaculis maximus velit. Vivamus malesuada lacus sed velit consequat fringilla. Pellentesque ornare accumsan aliquam. Suspendisse tempor vel nisi quis iaculis. Curabitur porta ligula ac libero molestie, nec viverra ex feugiat. Donec lacinia eget lorem eu lacinia. Maecenas vestibulum ornare dui a aliquet.
Proin id lobortis nunc, ut feugiat urna. Nam dictum odio tortor, bibendum sollicitudin erat suscipit eu. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Suspendisse eu imperdiet lectus. Nullam faucibus elit leo, sit amet pulvinar diam iaculis ac. Pellentesque accumsan vulputate orci sed ornare. Fusce at tortor ac libero volutpat tempus nec a diam. Nam eu libero tristique, rutrum magna suscipit, malesuada ligula.
Praesent suscipit imperdiet arcu vitae faucibus. Phasellus massa mauris, pharetra at posuere vel, fringilla sed quam. Morbi nec congue ante, vel vulputate massa. Suspendisse imperdiet mollis dignissim. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis pulvinar odio tellus, non cursus turpis malesuada sit amet. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque ex lorem, hendrerit ac enim et, tempor sodales ante. Curabitur quis tristique mauris, quis imperdiet metus. Mauris tristique interdum congue. Phasellus finibus condimentum pretium. Sed et tellus vel quam molestie aliquet.
Phasellus sagittis ligula nibh, nec hendrerit libero maximus et. Morbi vestibulum, tortor non efficitur semper, velit lacus molestie augue, ac volutpat elit ante ac augue. Pellentesque ut tristique odio, vel ornare eros. Nunc at ullamcorper arcu, in ornare risus. Vivamus sed libero auctor, vulputate lectus ut, luctus lectus. Maecenas finibus at nulla vel congue. Nullam finibus mauris leo, ac volutpat sem pulvinar eget. Maecenas in posuere tellus, in placerat est. Ut at est semper, ullamcorper ligula ac, volutpat nisi. Donec tempus tellus sed lorem posuere, a rutrum odio faucibus. Donec porta sem ac tortor vulputate, sed pharetra sapien convallis. Praesent ac feugiat odio. In risus eros, pharetra sit amet porta eu, lacinia vel nunc.
Praesent consectetur tincidunt mauris ut dignissim. Aenean sagittis ornare mi at sagittis. Proin id suscipit ipsum. Mauris lacinia commodo molestie. Fusce nec pharetra purus. Nulla eget velit varius, tincidunt erat tincidunt, cursus odio. Aliquam accumsan risus non nibh hendrerit fermentum. Aliquam erat volutpat. Phasellus non tincidunt lacus. Quisque sagittis augue eu egestas maximus. Integer sed eleifend neque, nec finibus sapien. Aliquam malesuada urna nec justo sodales dignissim. Cras volutpat maximus euismod. Pellentesque consequat lorem non augue placerat dignissim. Ut dolor nisl, malesuada sed nunc quis, malesuada dignissim nibh. Nam hendrerit hendrerit sapien quis auctor.
Aenean feugiat enim eros, interdum tincidunt eros aliquam sit amet. Fusce hendrerit mi vulputate nisi vestibulum tempor. Vestibulum at dui et nisl viverra condimentum id sit amet leo. Phasellus vitae leo commodo, blandit magna ut, commodo enim. Donec ex elit, mattis non tincidunt quis, posuere ut sapien. Aliquam lacus mauris, commodo sed libero vitae, rhoncus sodales massa. Phasellus commodo, leo sit amet sollicitudin luctus, massa purus aliquet lectus, at rutrum nulla ante eget felis. Nulla facilisi. Aenean cursus ut diam non volutpat. Aliquam nec erat non ipsum ultricies hendrerit. Phasellus blandit ac lacus non laoreet. Etiam ultricies risus mauris, a volutpat nibh cursus in. Maecenas viverra odio sit amet libero feugiat tempor. Nullam eu tristique magna. Praesent sed lacus ligula.
Sed iaculis viverra mollis. Phasellus sed eros sit amet lectus vulputate ornare sed non dui. Donec pretium dui quis felis feugiat elementum. Quisque eleifend eget nunc eu cursus. Etiam finibus lectus non ipsum ornare malesuada. Integer nec eros ullamcorper, pharetra neque at, suscipit ex. Nam faucibus sapien est, vel viverra ante viverra a.
Ut quis euismod lorem. Fusce auctor quam eu velit semper, sed pretium odio semper. Fusce vehicula porta dignissim. Integer suscipit ultrices ultricies. Aenean pharetra cursus sem. Suspendisse ut porttitor neque. In luctus purus sagittis risus gravida pulvinar. Integer eu ante convallis, pharetra velit vel, sodales urna. Curabitur pretium sapien arcu, eget dignissim neque ornare in. Vivamus eget metus et diam pulvinar aliquam vel eu turpis. Donec augue metus, dapibus eu porta a, faucibus in felis. Etiam sodales arcu a ex fermentum, sit amet rutrum est auctor. Integer gravida lorem in mauris tincidunt lobortis.
Etiam lobortis dictum dapibus. Nulla tincidunt placerat aliquam. Mauris non ornare nisi. Integer hendrerit ornare nibh, non vehicula sapien maximus eu. Proin purus erat, dapibus id dui commodo, blandit sodales mauris. Sed a vulputate lectus. Curabitur vel tincidunt orci. Vivamus cursus augue non nisl dictum, eget venenatis mi eleifend. Integer in ultricies ligula. Phasellus et sodales est, eget mollis magna. Sed lacinia, ligula eget feugiat ullamcorper, eros ipsum dignissim sapien, in suscipit mauris odio at metus. Suspendisse eget felis eu nulla porttitor dignissim at eu felis. Duis pulvinar est mauris, sit amet volutpat lacus hendrerit non. Aliquam finibus rhoncus mauris. Nunc dui nunc, lacinia aliquam finibus ut, maximus quis odio. Sed tempus dui vel orci vehicula, a tristique risus consectetur.
Ut egestas velit eu urna imperdiet cursus. Aenean blandit pretium turpis, quis sollicitudin lacus dignissim eu. In eu orci posuere, volutpat velit et, varius velit. Mauris rutrum sem nunc, nec sagittis lectus accumsan at. Morbi vestibulum est ut dolor maximus, non euismod magna consequat. Morbi pharetra gravida velit ac suscipit. Fusce vitae turpis eget ante tempus bibendum. Integer sollicitudin vulputate neque eu pulvinar. Etiam consequat pulvinar lorem eu dignissim. Fusce luctus id lacus ut fermentum. Vestibulum ligula neque, finibus et aliquam vel, consequat id lacus. Quisque pretium elit a tincidunt aliquam.
Maecenas varius magna in dictum dapibus. Vestibulum eget mattis velit, ac accumsan tellus. Donec luctus lorem in nisl convallis, eget egestas erat varius. Phasellus et lacinia dui. Praesent id nunc elementum, ultricies dolor a, pulvinar tellus. In a auctor mi, in fermentum justo. Proin sit amet vulputate leo, eu ultrices sem. Integer at sagittis ex, eu scelerisque velit.
Curabitur auctor mollis nunc quis venenatis. Cras lectus ligula, auctor in tellus eget, aliquet molestie odio. Vivamus vel venenatis sem. Sed ac purus suscipit, pulvinar sapien sed, commodo arcu. Donec eget metus ipsum. Quisque posuere congue hendrerit. Phasellus eu scelerisque ipsum, ut vulputate diam. Curabitur sollicitudin tortor sapien, a finibus arcu consequat non. Suspendisse tempus magna porttitor mauris laoreet, quis tincidunt lectus iaculis. Fusce nibh magna, finibus ac enim at, volutpat dapibus metus. Duis iaculis sapien non erat vestibulum auctor.
Maecenas nec porta leo. Fusce aliquam massa sit amet blandit interdum. Donec malesuada enim in eros laoreet iaculis. Integer fermentum faucibus nunc, et congue massa mattis eu. Pellentesque sed sodales orci. Praesent pulvinar lacus eget turpis condimentum imperdiet. Nullam molestie, ipsum a molestie accumsan, lectus quam sodales eros, malesuada faucibus ex leo malesuada velit. Fusce molestie ligula at sapien dictum semper non rutrum sapien. Ut facilisis gravida ante nec faucibus. Sed aliquet luctus auctor. In tempor libero at eleifend convallis. Phasellus in tristique ligula. Vestibulum eget quam dapibus, luctus enim vel, sagittis eros. Cras laoreet sapien diam, eu posuere nisl blandit quis.
Vestibulum bibendum a massa ac faucibus. Maecenas vestibulum arcu id diam mattis, non varius orci aliquet. Vivamus sit amet ultrices velit. Donec vitae lorem vel lectus dignissim porta. Sed ac dui dui. Suspendisse lectus ante, pellentesque non euismod vel, finibus ut erat. Vestibulum accumsan facilisis velit, at posuere velit malesuada at. Mauris aliquam tortor sed pretium viverra. Ut et porttitor ex. In quis tellus vitae neque dignissim finibus nec eu dolor. Phasellus viverra nulla a vestibulum auctor. Praesent in lorem gravida, mattis enim commodo, volutpat tellus. Quisque non urna ac eros sollicitudin blandit sit amet ut risus. Integer sed diam a metus tristique consectetur vitae non velit. Aliquam justo est, pharetra vel libero in, molestie varius enim.
Fusce vestibulum auctor varius. Maecenas malesuada, purus quis congue vehicula, arcu purus congue nunc, non convallis felis magna quis nisi. Curabitur nisi diam, imperdiet et sollicitudin quis, ultricies nec enim. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus rutrum, lectus at condimentum condimentum, felis justo lacinia orci, non venenatis nisl ex sed enim. Aenean semper ligula a diam cursus, ac sollicitudin ligula pretium. Etiam eleifend lorem nec massa tempor sollicitudin. Phasellus dignissim velit dignissim mauris vestibulum, sit amet sagittis sem blandit. Donec lobortis varius nunc sit amet posuere. Cras eu est lobortis, aliquet nisi in, varius mauris. Curabitur vitae leo laoreet sem condimentum vestibulum et eget enim. Nullam sed turpis ac nunc ornare tristique sit amet vel arcu. Cras efficitur ullamcorper lorem, et scelerisque lectus volutpat a. Maecenas ut urna in lacus rutrum volutpat.
Pellentesque pretium, neque quis cursus sagittis, justo mauris euismod turpis, non feugiat lacus augue a magna. Nam ornare dictum erat et consequat. Vivamus fringilla odio velit, vitae convallis arcu vulputate et. Maecenas tristique purus ac velit cursus fringilla. Suspendisse id blandit dui, eu facilisis metus. Proin a erat rutrum, tempus lectus ut, consequat nisi. In dui sem, bibendum nec nibh nec, sollicitudin varius ipsum. Nulla id lectus eu eros placerat faucibus eget nec nunc. Nullam urna lectus, tempus nec libero a, egestas malesuada enim.
Curabitur vestibulum a nibh eget varius. Sed lacus ipsum, porta sit amet egestas sit amet, pellentesque a dui. Etiam auctor mollis orci, eget pulvinar magna tristique quis. Proin condimentum ornare nibh, sed interdum orci congue at. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Maecenas quis lacinia tellus. Nullam ac nibh auctor, fermentum velit ac, rutrum ipsum. Nam egestas sodales hendrerit.
Suspendisse a feugiat mauris. Nunc eget nisi augue. Suspendisse pharetra justo sit amet sollicitudin tincidunt. Duis pretium, leo quis euismod sagittis, ex ante sodales purus, vel tempor odio nisi ut diam. Praesent odio erat, ultricies et eros ac, pulvinar aliquet justo. Pellentesque non vestibulum sem, a vulputate purus. Nunc rutrum nulla vitae mattis pharetra. Integer dui diam, pulvinar sit amet velit bibendum, aliquet dictum nulla. Nam ut magna et lorem ornare dignissim eget quis mauris. Vestibulum aliquet mi ac rutrum varius. Mauris a imperdiet eros. Curabitur lobortis lectus vitae leo aliquam auctor. Ut et tincidunt dui.
Mauris luctus risus eu tempor pulvinar. Mauris et mi nec ligula imperdiet vulputate a congue ex. Proin et venenatis mauris. Vivamus viverra accumsan lorem at gravida. Aliquam non pretium ante. Morbi aliquam risus sapien, quis consequat tellus maximus vitae. Sed consequat libero in molestie faucibus. Donec vestibulum arcu a sodales volutpat. Nullam ultricies ante quis quam accumsan rutrum. Quisque eu egestas velit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eu risus vestibulum enim iaculis pellentesque iaculis eu ligula. Nam rutrum, mauris sit amet efficitur eleifend, massa quam consequat metus, eget sodales leo sem eget risus. Aliquam dui libero, maximus at dui nec, consectetur maximus ligula. Sed gravida erat eget tristique lobortis.
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean turpis velit, vestibulum a consequat vitae, efficitur id urna. Cras dapibus dui et scelerisque luctus. Sed vel malesuada justo. Suspendisse molestie iaculis lacus eu tempor. Praesent laoreet lectus quis malesuada volutpat. Fusce consectetur mollis ligula, a lobortis justo suscipit id. Integer eget tellus in arcu viverra accumsan eu et odio. Praesent ut felis risus. Pellentesque convallis felis et quam condimentum, at pellentesque nulla eleifend. Sed eget efficitur dui. Vivamus congue, leo eu lacinia condimentum, erat mi dignissim purus, sit amet convallis lorem sem sed ligula. Duis viverra leo enim, in pharetra felis consectetur viverra. Integer euismod feugiat ipsum eget vestibulum. Morbi fermentum dui vitae tincidunt dictum.
Phasellus non eros ut ipsum pretium condimentum. Vestibulum tristique convallis aliquam. Phasellus tempor leo sit amet diam tristique, a auctor urna hendrerit. Mauris at velit euismod, sollicitudin mi at, scelerisque est. Mauris pulvinar consequat quam, eget varius augue tincidunt id. Curabitur turpis lectus, eleifend a egestas id, malesuada non diam. Proin aliquet tellus urna, venenatis consequat tortor finibus et. Quisque at tempor magna.
Donec nec congue erat. Donec mauris lorem, dignissim euismod massa non, egestas pulvinar est. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean aliquam venenatis eleifend. Vivamus consectetur ante et fringilla sodales. Nam vestibulum felis non sapien suscipit consectetur et interdum velit. Sed at placerat massa. Proin quis leo lectus. Integer diam velit, gravida quis rhoncus eget, tempor at ante. Aliquam pharetra dictum mi ac venenatis. Nunc volutpat id magna a pellentesque. In rutrum leo et ligula finibus, ut pretium lorem tristique. Curabitur vel orci iaculis, pellentesque justo et, ullamcorper orci. Quisque erat ante, venenatis ac lacinia ut, maximus ac odio. Aliquam varius bibendum varius.
Duis ipsum orci, semper sit amet volutpat quis, scelerisque a nunc. Donec iaculis, erat vitae malesuada rutrum, nisl leo gravida nisi, et dignissim eros eros at mi. Cras volutpat est erat. Maecenas ex felis, porttitor vitae egestas sit amet, tincidunt efficitur tortor. Curabitur ac purus massa. Etiam porta maximus tristique. Morbi placerat pulvinar lectus sed tempus. Morbi fringilla odio posuere, volutpat mi sed, congue lorem. Cras convallis volutpat eros mattis vestibulum. Ut sit amet sodales eros, ac condimentum purus. Vivamus et elit augue. Pellentesque efficitur gravida ullamcorper. Sed non faucibus tellus, et consectetur urna. Pellentesque at turpis fringilla ipsum interdum hendrerit eu in dui. Nullam dictum eget metus non sollicitudin. Proin facilisis tincidunt euismod.
Nunc et dui porta, suscipit mi vitae, pretium ante. Mauris a accumsan magna. Fusce tincidunt, nunc ut ullamcorper laoreet, nibh risus dignissim eros, a maximus arcu nunc congue arcu. Vivamus imperdiet quam lorem, vulputate consequat ante aliquet id. Sed porttitor mollis ullamcorper. Praesent dui justo, hendrerit quis vulputate sit amet, vulputate id libero. Etiam quam odio, dictum sit amet nisi in, semper placerat erat. Vivamus lacinia augue a dolor tincidunt dignissim. Curabitur ut massa sit amet elit tincidunt maximus.
Integer sed lorem ac lacus tincidunt condimentum quis vel diam. Etiam vitae justo interdum, accumsan ex vel, pretium magna. Aenean id turpis malesuada, semper nisi vel, hendrerit leo. Vestibulum feugiat neque nec lacus auctor efficitur. Ut quis lobortis lacus, sit amet tempor orci. Donec a fringilla sem, nec sollicitudin ex. Sed vel faucibus libero. Sed pharetra leo sed porta ullamcorper. In auctor semper metus, id semper nunc rutrum a. Morbi rhoncus nulla quis ex condimentum, vel congue lectus tempor. Duis a venenatis est. Etiam dolor justo, rhoncus at lobortis quis, malesuada at quam. Donec facilisis convallis mi vitae rhoncus. Integer est ipsum, sollicitudin eu nulla in, accumsan egestas nisi. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nunc finibus sagittis nisl, et luctus ex.
In molestie est luctus magna hendrerit, non tristique felis ultricies. Curabitur non lectus placerat, tincidunt ipsum quis, mollis nunc. Nam lacinia ullamcorper hendrerit. Fusce porttitor nisi laoreet, elementum velit vitae, venenatis mi. Aliquam venenatis dui ac justo varius fermentum. Etiam nec ligula eu neque aliquam placerat mollis ac dolor. Pellentesque gravida, enim commodo tempus faucibus, lorem nibh vulputate arcu, sodales pellentesque metus ligula volutpat sem. In scelerisque nisi ac ante lacinia auctor.
Nunc at sodales libero, ut rutrum elit. Etiam vitae venenatis metus. Nullam faucibus turpis nisl. Maecenas varius sit amet lectus a ornare. Cras eget tortor mauris. Phasellus mattis nec tortor non sagittis. Integer eget placerat massa. Praesent eu porttitor leo. Vivamus sed bibendum sapien, sit amet maximus libero. Donec at lobortis lacus.
Maecenas convallis eros nibh, nec pellentesque magna lacinia vulputate. Donec posuere neque ut lacinia iaculis. Quisque varius dictum condimentum. Aliquam lacinia elit sed sem porttitor tempus a ac ipsum. Aliquam suscipit ex vel tellus mollis, nec suscipit nunc ullamcorper. Cras urna magna, feugiat a odio a, pharetra congue velit. Proin quis hendrerit odio. Proin non orci ut sem ultricies sodales. Morbi dictum orci non enim tempor, ut condimentum erat maximus. Nam sagittis ornare lacus, at sodales dolor porttitor nec. Mauris dignissim ipsum in massa rutrum blandit. Nulla sit amet ante vestibulum, pharetra tellus eu, elementum tortor. Donec nec fringilla mi.
Donec rhoncus odio a iaculis ullamcorper. Nam condimentum volutpat ante quis viverra. Vestibulum malesuada nibh tincidunt rutrum interdum. Sed dapibus et nisl vel ornare. Nunc ullamcorper nibh nec commodo eleifend. Nulla facilisi. Nullam ut est auctor ligula viverra dictum pellentesque id odio. Aenean eleifend ullamcorper mollis. Morbi quis rutrum dolor, vel dignissim purus. In vel metus scelerisque, auctor est quis, efficitur orci. Donec volutpat dui eu vulputate maximus. In hac habitasse platea dictumst. Quisque vel vulputate lorem. Phasellus lacinia tempus lectus, sed tristique magna hendrerit eu. Nullam non metus euismod dolor fringilla imperdiet et vitae purus. Sed varius ex tortor, vel rutrum turpis sagittis ac.
Sed nec tincidunt turpis, sit amet suscipit nisi. Curabitur ultrices orci ligula, nec cursus eros placerat non. Pellentesque faucibus venenatis nisi in condimentum. Morbi facilisis mauris odio, id facilisis ex dictum ut. Duis bibendum ultricies elit, sit amet dapibus risus ultricies quis. Nulla eget ullamcorper risus, ac imperdiet nulla. Proin ultrices nisi eu turpis elementum hendrerit. Maecenas euismod pretium quam accumsan finibus. Morbi augue enim, laoreet nec nunc id, lobortis hendrerit lorem. Nunc mattis cursus dui vel lacinia. Aenean massa tellus, laoreet sit amet turpis vel, pharetra facilisis dui.
Nunc sit amet tellus vel ligula vulputate dapibus. Donec non congue nisi. Mauris risus elit, vehicula a imperdiet a, tempor gravida ipsum. Sed erat magna, volutpat ac nunc sit amet, aliquam lobortis est. Mauris quis urna sem. Sed risus justo, vestibulum ac pharetra in, dignissim id nibh. Etiam nibh justo, auctor in tellus euismod, rhoncus lobortis dolor. Proin felis lectus, hendrerit nec dignissim nec, molestie a lorem. Phasellus commodo pellentesque ligula nec imperdiet. In quis sem a erat euismod elementum at sit amet sem.
Praesent tortor enim, iaculis sit amet placerat in, sodales non erat. Phasellus porttitor, massa vel posuere tincidunt, mauris magna ultrices mi, in aliquet sapien sapien ut libero. Sed accumsan odio ut mollis dignissim. Aliquam varius egestas condimentum. Etiam id massa condimentum, scelerisque velit sed, vulputate augue. Duis mollis augue eu felis venenatis dapibus. Vivamus non gravida nulla. Integer a mi mollis, pharetra ipsum eget, tincidunt sapien.
Phasellus libero arcu, aliquet rutrum commodo vel, efficitur quis libero. Morbi sed eros ante. Quisque efficitur leo eget nulla lobortis, eget imperdiet sem vehicula. Nunc commodo lorem sit amet felis aliquam, non facilisis velit tristique. Aliquam dapibus rutrum dignissim. Phasellus elementum nulla a enim venenatis, at ornare sapien feugiat. Proin ut sem eu nulla rutrum volutpat non non quam. Fusce vel pellentesque lectus, vitae pellentesque est. Praesent sit amet dolor non purus feugiat pharetra. Donec ullamcorper diam vitae sem varius, eget imperdiet nisi viverra. Fusce fermentum dui risus. Etiam sed diam ut libero sollicitudin faucibus at in sapien. Suspendisse bibendum purus at urna vehicula, sed maximus nisi pharetra. Proin posuere nisl ac consectetur sollicitudin. Nam auctor in ex scelerisque imperdiet.
Donec consequat arcu non lacus ullamcorper, a fermentum felis lobortis. Nunc tempor est quam, ut finibus odio rutrum sit amet. Proin hendrerit tincidunt nunc, sed tincidunt nisi tempor eget. Phasellus maximus a risus ut luctus. Duis libero enim, varius a tellus vitae, dignissim facilisis felis. Vestibulum iaculis tempor condimentum. Sed mollis velit justo, quis mollis lectus imperdiet sed. Quisque laoreet eget eros porta imperdiet. Curabitur pretium velit quis sapien placerat, in blandit metus suscipit. Ut pretium urna lectus, sed interdum arcu fermentum et.
Sed euismod nunc quam, nec tincidunt dui tempus at. Mauris molestie, quam vel lobortis dignissim, justo sem porta orci, nec commodo enim orci eget dolor. Pellentesque eros risus, fermentum non urna et, ultrices pharetra augue. Curabitur at tincidunt arcu. Etiam vel sollicitudin libero. Nulla dapibus orci odio, sit amet mattis quam sodales ac. Sed iaculis at eros volutpat egestas. In at fringilla massa. Proin at mattis turpis. Phasellus non metus et leo fermentum pretium non nec odio. Ut ante orci, interdum vel gravida eget, mollis in tortor. Sed rutrum ornare arcu in efficitur. Nulla facilisi.
Aenean vestibulum nisl sed eleifend ullamcorper. Suspendisse viverra imperdiet nisi, sed imperdiet tortor vulputate ut. Nunc consequat nisi eu laoreet mollis. Quisque metus ante, fringilla at rutrum id, consectetur vel enim. Pellentesque posuere tempor urna, sit amet vulputate urna. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; In hac habitasse platea dictumst. Nullam quis elementum purus. Cras eleifend sed justo vitae faucibus. Morbi interdum neque vitae elementum sollicitudin. Etiam eleifend, sapien at interdum consectetur, ex mauris dictum sem, in convallis dolor felis sollicitudin augue. Curabitur id tortor ex. Mauris finibus, lectus convallis euismod congue, est tortor suscipit velit, ac dictum augue risus ut lectus. Vestibulum ultricies luctus hendrerit. Integer elit nibh, condimentum id tempus nec, euismod nec nunc.
Sed porttitor hendrerit eros vitae suscipit. Vestibulum quis est elit. Sed neque leo, condimentum quis condimentum sed, posuere ut ex. Suspendisse mattis est nunc, in ullamcorper felis pellentesque sed. In hac habitasse platea dictumst. Maecenas tempus et turpis vestibulum porttitor. Quisque nulla odio, rhoncus et porta ut, hendrerit non sapien. Proin consequat, ex sed consequat iaculis, magna dolor consequat dui, eget rhoncus magna lorem in turpis. Cras urna sem, tincidunt ac aliquet id, imperdiet vitae neque. Fusce ac risus nisi. In erat urna, feugiat ac purus non, volutpat pulvinar ipsum. Morbi ultricies eros at nulla ullamcorper, nec lobortis arcu convallis. In maximus efficitur quam id maximus.
Etiam posuere urna non diam accumsan tempor. Donec facilisis blandit leo sed convallis. Nullam ut nisl vel dolor varius accumsan vitae a dui. Nunc ullamcorper nunc ac rhoncus pretium. Mauris ac lectus urna. Phasellus quis interdum nisi. Suspendisse mi ex, mollis a euismod vel, pretium et est. Mauris condimentum in ipsum quis lacinia. Mauris scelerisque molestie nibh, auctor lobortis felis convallis ac. Praesent vestibulum luctus urna non tincidunt. Quisque id nisl pretium, bibendum nisl nec, gravida quam. Curabitur eget cursus purus, non commodo felis. Vestibulum non lectus nec quam auctor dictum. Curabitur molestie elit mi, non tempor nisi aliquam in. Etiam fringilla lacinia est.
Pellentesque eleifend pulvinar eros, quis pulvinar arcu fermentum sed. Nam augue lectus, malesuada id mattis nec, ullamcorper sed nisi. Phasellus volutpat nisl eu commodo feugiat. Morbi eget sapien iaculis, consectetur est in, tempus metus. Donec hendrerit lectus aliquam ex faucibus pellentesque. Praesent lobortis libero sit amet metus commodo faucibus. Sed gravida eget mi at finibus.
Vivamus nec ligula vitae augue auctor viverra. Sed vulputate eget libero a porta. Aenean nisl turpis, tincidunt et placerat sit amet, bibendum quis libero. Nullam ultrices a ex sit amet dapibus. Suspendisse id congue lorem. In viverra convallis neque. In facilisis dui quis rhoncus semper. Ut id imperdiet libero. Fusce condimentum, sem id elementum aliquam, quam augue tincidunt felis, at suscipit lorem eros eget augue. Suspendisse eros dui, consectetur a arcu in, hendrerit dapibus risus. Proin purus erat, tincidunt eget ante sit amet, consequat convallis erat. Praesent et ornare nisl, id vehicula nulla.
Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aenean dignissim pellentesque mi, consectetur ornare leo egestas vitae. Suspendisse dignissim sapien orci, sed vestibulum neque maximus at. Sed lobortis urna consequat, sollicitudin lectus eget, finibus ante. Phasellus accumsan dolor non porttitor gravida. Duis eget eros ac lectus blandit posuere. Quisque sagittis nibh nunc, eu egestas nisi cursus at. Praesent accumsan fringilla neque eget iaculis. Nunc vulputate tempus tellus non porta. Sed faucibus vel dui in posuere. Sed quam arcu, accumsan id placerat sollicitudin, congue in diam.
Cras sed nunc in turpis dignissim vehicula non a libero. Sed iaculis nulla fermentum lacinia porta. Pellentesque sit amet efficitur est. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis et ultrices justo. Etiam ut blandit nibh. In tempus lorem nec ultrices sagittis. Vivamus vel nulla faucibus, bibendum sapien ut, tristique diam. Donec pulvinar nisi nec semper blandit. Duis porttitor nibh enim, iaculis efficitur est sagittis a. Maecenas at eros ac quam pellentesque tempus nec ac magna. Donec pulvinar enim non congue facilisis. Donec eros orci, finibus vel lectus molestie, interdum scelerisque libero. Proin congue arcu lorem, in lobortis libero placerat vel. Nullam cursus est id lectus laoreet tempus et sed ex. Sed erat odio, molestie mattis egestas iaculis, sollicitudin eu ex.
Donec sodales metus ut mi suscipit, quis pretium lorem ultrices. Donec placerat ante at pellentesque vulputate. Etiam id augue vel eros facilisis interdum. Mauris imperdiet, arcu sit amet tincidunt posuere, risus quam luctus diam, eget tincidunt ipsum magna a mauris. In hac habitasse platea dictumst. Fusce lacinia sem ut ullamcorper tristique. Nunc mattis massa tellus, vitae lacinia sem tincidunt ut. Phasellus sodales justo ut ligula iaculis, sit amet ultricies arcu lacinia. Nam ac dignissim orci. Nullam nec orci nec est bibendum ultrices. Nam pulvinar consequat eros, ut laoreet massa blandit nec. Proin tristique venenatis pretium. Curabitur vitae dui est. Integer fermentum, ligula in faucibus porttitor, sem nunc rhoncus turpis, a faucibus lectus risus quis eros. Pellentesque et lorem eu dolor placerat interdum. Aliquam luctus scelerisque leo eu faucibus.
Vivamus id hendrerit odio, nec pellentesque magna. Proin augue eros, egestas nec eleifend ac, varius a sapien. Pellentesque viverra orci at condimentum euismod. Nam placerat lacus et augue porttitor blandit. Fusce scelerisque metus mollis nisl sollicitudin congue. Phasellus dictum velit arcu. Duis lacinia quam mauris, rhoncus porta enim dignissim posuere. Curabitur tempor urna eget ex cursus iaculis. Sed semper nisi nec nibh tempor pellentesque. Nulla vel turpis ac ipsum maximus porttitor eu sed nisl. Aliquam accumsan elit risus, eget sollicitudin ipsum lobortis at. Nulla sagittis faucibus sodales. Curabitur viverra pharetra quam quis efficitur. Integer eget justo maximus, dignissim magna vitae, consectetur metus. Suspendisse tincidunt, nunc eu vehicula faucibus, arcu felis accumsan velit, id rhoncus dui sapien vel purus.
Nullam lacinia urna commodo vehicula fringilla. Donec tellus est, bibendum a quam in, pellentesque auctor odio. Donec vehicula velit et leo consequat, sed eleifend mauris aliquam. Vivamus nibh ligula, blandit eu leo quis, pretium malesuada augue. Pellentesque at lacinia magna. Sed nec dolor porttitor, finibus erat ut, fringilla leo. Phasellus sed ex pellentesque, semper elit auctor, fermentum turpis. Vestibulum dictum sodales augue volutpat pretium. Integer pharetra volutpat nisl at dignissim. Mauris mattis, turpis nec volutpat maximus, sapien dolor elementum urna, ac iaculis neque dui non urna. Duis scelerisque magna nec nunc mattis maximus. In lobortis massa ante, non sagittis turpis viverra nec. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Integer viverra velit a fringilla sodales.
Suspendisse id arcu ut nisl ultrices convallis ornare in lectus. Nunc ultrices nulla vitae libero rutrum lobortis. Nullam hendrerit consectetur augue sit amet ornare. Cras dui est, pharetra at augue rhoncus, pharetra congue enim. Suspendisse maximus magna ut neque porta, in porttitor lorem pretium. Ut a dignissim magna, non tincidunt tortor. Vestibulum posuere tempus leo vitae blandit. Cras convallis ex a mi maximus fringilla a ac metus. Nunc sodales tincidunt semper. Praesent at risus eu neque tempus consectetur vel a odio.
Fusce bibendum ex a risus feugiat tincidunt. In ac volutpat quam, vestibulum molestie nibh. Maecenas pretium ultricies augue vitae luctus. Ut congue leo ante, consequat ullamcorper mi commodo sit amet. Duis et libero leo. Nam eu dictum sem, quis commodo tellus. Sed et dolor eu nisi varius sodales sed eu quam. Aenean eget auctor mauris, vitae faucibus nunc. Nam ultrices elit eu libero porta, vitae cursus odio aliquet. Vivamus sit amet interdum tellus. Suspendisse suscipit fringilla nisi, id scelerisque nisi aliquet vitae. Aliquam est nibh, commodo nec neque et, consequat ultrices nibh. Suspendisse efficitur id massa sed bibendum. Phasellus vitae posuere est.
Ut nec aliquet ex. Integer sapien nunc, tincidunt eu dolor ut, dictum pulvinar lorem. Donec nec augue nibh. Curabitur euismod, lorem ut mattis volutpat, nunc dui pharetra mauris, sed rutrum magna lacus non lacus. Nam ac rhoncus turpis, et dignissim erat. Fusce suscipit blandit mauris vel aliquam. Aenean id orci sed augue dapibus pellentesque ac ac sem. Nulla vel purus sapien. Integer vestibulum porttitor posuere. Mauris tincidunt elit nec risus tincidunt ornare. Duis bibendum, magna eu interdum bibendum, lectus nulla dictum dui, non condimentum sapien tortor sit amet orci. Integer ligula ligula, sodales vel nulla non, commodo semper eros. Quisque cursus tempus fringilla. Sed at nisl odio. Ut malesuada turpis ac libero consequat aliquet. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Suspendisse tempus magna et massa euismod faucibus. Pellentesque sollicitudin nisl in pharetra aliquam. Vestibulum blandit massa ut turpis ornare, nec imperdiet metus interdum. Fusce molestie aliquet justo, ac condimentum justo. Praesent quis dolor vestibulum, posuere arcu eget, molestie nisi. Donec ac urna tempor, tempus lectus non, scelerisque est. Aliquam imperdiet dictum urna, non malesuada urna. Proin et tellus quis sapien dignissim vehicula. Vestibulum tortor metus, iaculis ac eros ut, laoreet vulputate nibh. Cras tempor vitae erat eget consequat. Sed ut nisl convallis, placerat quam quis, tincidunt turpis. Suspendisse sit amet rutrum augue. Duis sem justo, dictum eu dui at, eleifend commodo orci. Curabitur at erat odio. Phasellus porttitor interdum nibh, sit amet aliquet libero. Etiam convallis mattis massa, a rhoncus ligula condimentum et.
Duis ut diam ac lectus viverra volutpat et eget dolor. Nam tincidunt mauris vitae aliquet laoreet. Nam id ipsum eget ante euismod vulputate. Mauris elementum, tellus vitae rutrum vestibulum, libero orci finibus risus, in lacinia libero massa vitae elit. Nam et vestibulum justo, sed gravida orci. In ultrices mollis ultricies. Pellentesque odio leo, sagittis lacinia odio eu, accumsan auctor nibh. In non sem elementum, mollis justo sed, ornare nisi. Fusce interdum lobortis turpis ut dictum. Integer lacinia mollis nunc, nec condimentum quam hendrerit in.
Donec condimentum, dolor in aliquet facilisis, leo ex feugiat nisi, a commodo libero urna id enim. Donec faucibus urna eget pulvinar dapibus. Curabitur ultricies justo at ligula aliquam blandit. Mauris nisi urna, porttitor eu bibendum vitae, ultrices eu risus. In ac pulvinar nisl. Vestibulum viverra tellus purus, eget rutrum arcu venenatis et. Pellentesque nibh risus, sagittis eu hendrerit eget, sollicitudin sit amet enim. Quisque molestie ornare tellus sit amet placerat.
Ut finibus metus sit amet velit posuere, in egestas massa congue. Nulla sagittis, nisi eget pretium vestibulum, mauris libero elementum mauris, vel sollicitudin ligula metus vitae elit. Sed feugiat lectus sed ante maximus pharetra. Praesent felis eros, gravida sed varius in, faucibus ut metus. Nulla eget pretium nulla. Aliquam ultricies viverra magna, vel semper ligula accumsan ut. Pellentesque sollicitudin bibendum pretium. Vivamus volutpat commodo eleifend. Maecenas ornare ac nisl at tristique. Vivamus tellus lectus, euismod nec pellentesque non, lobortis ut nunc. Quisque hendrerit mi eget hendrerit ultrices.
Nunc rhoncus ligula ac libero consequat, semper commodo eros feugiat. Phasellus iaculis neque vitae luctus porttitor. Pellentesque suscipit mi ac ipsum bibendum mollis id at velit. Curabitur consectetur sollicitudin tincidunt. In a enim convallis, consequat justo nec, dignissim urna. Sed nisl ex, semper vitae lectus non, laoreet volutpat purus. Suspendisse ut mattis urna, quis hendrerit odio. Sed tincidunt libero pulvinar sem rhoncus, eu elementum enim ornare. Proin eu placerat ex. Cras euismod sem at ullamcorper scelerisque.
Nam fringilla, velit in ultrices pellentesque, nisl magna tempor felis, vel congue purus lectus sed metus. Cras consectetur quis massa id bibendum. Praesent sed purus odio. Fusce volutpat dolor ut magna congue auctor. In at magna lacinia, bibendum nisi in, pellentesque nisl. Pellentesque metus ligula, malesuada non turpis et, vulputate ultrices lectus. Sed erat leo, maximus id elementum sagittis, lacinia a metus. Proin mattis id lorem non malesuada. Aliquam sodales quam id gravida condimentum.
Phasellus quis bibendum risus, at porttitor sem. Nulla aliquet molestie eros quis sagittis. Ut lorem massa, ullamcorper vitae odio in, hendrerit vehicula urna. Quisque et viverra ex. Aliquam erat volutpat. Curabitur at interdum elit, ut tincidunt erat. Pellentesque quis augue pellentesque, finibus ante eget, feugiat quam. Cras nec efficitur ligula. Cras porta ac ipsum in ullamcorper. Nulla nunc erat, egestas egestas rhoncus viverra, mattis quis purus. Morbi convallis fringilla iaculis. Ut a sem mi. Praesent at velit ac dui euismod pulvinar. Donec mollis vehicula lorem vel tempus. Duis et scelerisque magna.
Suspendisse sollicitudin a libero ut convallis. Donec interdum faucibus dolor, id imperdiet nibh mollis eget. Sed sem augue, bibendum nec risus quis, mollis scelerisque magna. Cras molestie velit eget est pulvinar, a pharetra sapien tempor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla accumsan semper arcu id porttitor. Sed lacinia massa ut nibh gravida consectetur. Donec semper ante sed scelerisque varius. Vestibulum ullamcorper varius dui, sed aliquam elit aliquam nec. Sed auctor, dui sit amet mollis egestas, libero neque interdum est, efficitur elementum nibh lacus sed libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nam blandit lacus in lacinia tristique.
Suspendisse scelerisque posuere nunc, non consectetur orci facilisis in. Donec aliquet fermentum mattis. Morbi fringilla id urna eget consectetur. Vivamus turpis est, efficitur a tempus ac, ornare vel lorem. Aenean bibendum metus non ante rutrum sollicitudin. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam erat volutpat. Suspendisse dui eros, sagittis nec ante in, gravida varius ante. Mauris sapien neque, lobortis et euismod vel, venenatis at risus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Interdum et malesuada fames ac ante ipsum primis in faucibus. Maecenas vestibulum metus arcu, sed faucibus lacus ultrices eget. Integer porta aliquam bibendum.
Etiam tempus pulvinar dictum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed sed nulla vehicula, iaculis nibh sit amet, blandit urna. Maecenas metus ante, posuere vitae bibendum quis, consequat sit amet magna. Cras venenatis nunc urna, vel posuere diam congue eu. Cras sit amet ipsum laoreet, tincidunt nunc ac, convallis elit. Praesent lacinia enim nibh, et efficitur leo convallis vel. Etiam varius diam arcu, sit amet convallis dui interdum vitae. In hac habitasse platea dictumst. Nullam commodo maximus turpis, quis congue mauris fermentum quis. Donec laoreet elit non lorem gravida pharetra. Ut aliquam purus a semper sodales.
Donec nec sollicitudin nisi. Proin ipsum est, rhoncus vel commodo ut, tristique eget justo. Praesent tristique massa sed odio sodales viverra. Aliquam at nisi vel turpis pulvinar volutpat. Phasellus rhoncus scelerisque mollis. Nunc et cursus nulla, in consectetur magna. Praesent et mollis nibh, non tristique dolor. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas malesuada odio velit, eget finibus mauris congue eget. Praesent vestibulum mollis ex id ultricies. Integer sodales tempus vulputate. Phasellus blandit quam quis tortor blandit convallis. Donec tempor mi id urna venenatis fermentum.
Vestibulum ut sapien non tellus eleifend vestibulum eu vitae magna. Donec in lectus eget nisi ornare efficitur sed ut ex. Cras volutpat metus rhoncus hendrerit fringilla. Vivamus rutrum rutrum neque, eu imperdiet augue malesuada a. Aenean tristique massa vel diam vehicula pulvinar sit amet vel velit. Aenean faucibus luctus magna, non auctor nunc tempor ut. Etiam eleifend auctor metus, ac rhoncus massa. Aenean dignissim ornare tincidunt. Donec pulvinar sapien ante, vel dignissim sapien dictum sed. In eu ante in tortor fringilla viverra sit amet quis metus. Aenean in ex elementum, gravida nunc non, pretium nibh. Donec laoreet arcu sit amet dolor tempus pulvinar. Nullam at pharetra nisl. Donec sed justo sed arcu luctus ultrices. Fusce at venenatis eros.
Maecenas placerat quam ut massa suscipit lacinia. Maecenas et posuere risus. Proin interdum, libero sit amet consectetur pulvinar, dolor magna dignissim velit, sed hendrerit nulla ex sed nisi. Morbi dictum lobortis velit. Proin molestie mi at tortor finibus, quis ullamcorper enim maximus. Nullam varius, urna id volutpat volutpat, ligula lectus placerat eros, id sollicitudin dolor ligula eget sem. Curabitur quis augue vitae neque egestas tristique. Nam lacus libero, viverra faucibus risus et, placerat vestibulum neque.
In et convallis ante. Nulla hendrerit turpis eget consequat molestie. Fusce pharetra nunc ornare leo commodo, eu consequat odio dictum. Cras est diam, consequat et eleifend sed, faucibus quis neque. Morbi fermentum sem non ipsum mollis, id tempus risus blandit. Phasellus vulputate, ante finibus molestie finibus, velit enim mattis neque, et posuere felis risus a nisl. Cras a risus eu eros porttitor malesuada. Maecenas in mattis diam, sed tempor ex. In hac habitasse platea dictumst. Aliquam in magna quis arcu ultrices fringilla. Cras rhoncus tortor sed lacus blandit commodo. Nullam placerat augue vitae diam rutrum, ut eleifend ligula pellentesque. Aenean blandit lectus orci, egestas dapibus nisi finibus at.
Quisque id dapibus nisi, in placerat ante. Sed ut feugiat arcu. Etiam facilisis augue in nisi placerat facilisis. Donec vitae porttitor nibh. Nulla et nisl id purus egestas lobortis non id lorem. Nam viverra vulputate sapien, et posuere ex tincidunt a. Pellentesque venenatis turpis non purus rutrum faucibus. Nam non dapibus sem, sit amet rutrum neque. Vestibulum tempor, libero et faucibus feugiat, metus tellus condimentum nunc, sit amet pretium diam est a ipsum.
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Sed nulla neque, vulputate in aliquet quis, mollis non tellus. Ut aliquet lacus tellus, sed vehicula lectus aliquet ut. Nullam mattis in felis in condimentum. Vivamus facilisis, justo ac volutpat posuere, turpis ligula hendrerit tortor, eu ullamcorper quam dui non ex. Praesent feugiat, ligula eu aliquam interdum, mauris magna sagittis enim, eget facilisis felis lectus eget velit. In accumsan pharetra tincidunt. Sed mi massa, sodales nec nisi sed, lobortis fermentum ipsum. Nulla facilisi. Sed at accumsan felis. Donec sed condimentum metus, mollis gravida velit.
Nulla at lobortis nisl. Sed eu laoreet felis. Maecenas velit erat, mattis a condimentum eu, fermentum vel elit. Aenean mattis rutrum risus, in interdum metus volutpat sed. Nulla at dignissim lorem, in convallis tellus. Suspendisse ac risus sit amet diam ullamcorper feugiat. Maecenas sed lectus id erat cursus aliquam vel eget nisi. Ut tempus, risus id mollis lobortis, sem metus suscipit nibh, ut dapibus lectus tellus quis ipsum. In nec hendrerit nibh. In vel consectetur ex, a ullamcorper sem. Donec mauris massa, dictum non luctus a, feugiat eu metus. Donec maximus ex sit amet sem vehicula finibus egestas eget ex. Etiam volutpat ut nulla quis euismod.
Nunc consequat ut augue quis cursus. Aliquam placerat, enim eget scelerisque rhoncus, massa erat placerat tortor, ut varius dui felis in neque. Suspendisse potenti. Aliquam et magna tristique, volutpat sem eget, elementum lectus. Vivamus nunc velit, sagittis sed massa vel, vestibulum molestie urna. Cras at erat pharetra, porttitor turpis mattis, tempus magna. Nulla commodo enim mi, eu porttitor sapien pellentesque id. Aenean quis ligula eu mauris scelerisque porttitor quis ut orci. Praesent tincidunt risus mi. Nunc dignissim, arcu quis dictum consectetur, est massa imperdiet felis, ut scelerisque magna mi vel nisi. Mauris imperdiet, diam sit amet ultricies porttitor, odio sapien placerat ligula, in condimentum sem augue non justo. Duis a lorem augue.
Aenean vitae venenatis dolor, at ultricies purus. Vivamus velit urna, tempus vitae ornare id, semper in metus. In sed mi et odio pulvinar ultricies ut quis enim. Duis erat dolor, aliquam sed sodales id, porttitor vitae turpis. Fusce feugiat venenatis ex quis aliquet. Nulla pretium elit vel nisi suscipit condimentum. Nunc dapibus, mi id venenatis ultrices, libero purus rhoncus erat, a eleifend metus nisl at elit.
Aenean eget porttitor risus. Donec ac lacus feugiat, faucibus ante sed, convallis neque. Donec pharetra in sem eget congue. Quisque ac neque in urna varius interdum. Donec justo nisi, volutpat nec lobortis vitae, pulvinar sit amet lorem. Maecenas porttitor magna orci. In eleifend risus ut lectus facilisis aliquam. Nullam nibh nulla, sodales quis tempus sed, condimentum quis nulla. Proin vitae hendrerit ante. Morbi tincidunt pharetra metus, quis lobortis elit viverra sit amet. Vivamus mattis eros erat, ut semper velit pretium sed. Nulla ac vulputate leo.
Praesent at ex at lacus dictum viverra vel nec nisi. Duis maximus nisi et eleifend fringilla. Cras lacinia arcu id turpis dignissim posuere. Sed faucibus nisi dignissim, sagittis orci quis, dapibus metus. Nullam ultricies libero eu auctor finibus. Vestibulum auctor odio in tortor molestie interdum. Proin nec libero mi. Donec tempor dignissim velit a molestie. Duis feugiat cursus turpis, in dignissim justo imperdiet non. Suspendisse faucibus dui non viverra interdum. Nullam dignissim interdum egestas. Phasellus vitae eros quis lectus bibendum scelerisque. Donec dui mauris, iaculis non molestie a, elementum sed felis. Donec iaculis congue tempor. Vestibulum dolor nisi, maximus quis cursus ut, vulputate non orci. Maecenas nisi ex, maximus ut aliquam commodo, interdum sed lorem.
Cras sed vehicula risus, sed suscipit eros. Suspendisse scelerisque sapien sit amet volutpat porta. Sed sagittis orci eget feugiat eleifend. Nulla viverra ex vitae mauris lacinia imperdiet. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum arcu augue, euismod non elit non, feugiat aliquet ligula. Nulla ut neque vehicula, malesuada ex sit amet, finibus sem. Maecenas eleifend molestie justo a lobortis.
Aliquam at erat vitae neque porttitor ullamcorper. Integer et rutrum leo. Vivamus dui erat, luctus at purus et, auctor tincidunt felis. Proin fermentum maximus dui quis tempor. Nullam pellentesque at ex eget mollis. In hac habitasse platea dictumst. Maecenas efficitur turpis malesuada vulputate dapibus. Integer id faucibus diam. Aliquam lectus turpis, mollis vitae dolor eget, mattis faucibus est. Vestibulum id libero at est congue facilisis. Pellentesque tellus mauris, bibendum eget dui id, sagittis maximus enim. Sed condimentum ac lectus vel vulputate. Praesent lectus orci, pharetra id metus nec, fermentum congue nulla.
Nunc vel sem sem. Integer varius tincidunt lorem, id porta massa venenatis porta. Maecenas dictum mollis pharetra. In ex justo, auctor ac tempus id, lacinia et erat. Fusce dignissim purus in metus facilisis, vitae congue nunc tempor. Nam a euismod elit. Vestibulum tincidunt sed orci et porta. Vestibulum eget dui id mi efficitur fermentum.
Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut quis lorem vitae dui ullamcorper iaculis eget sed purus. Maecenas at ex lectus. Donec ut ligula eros. Nulla consectetur sed ligula quis volutpat. Integer rhoncus, justo egestas molestie scelerisque, nibh nibh finibus neque, dapibus vulputate quam lectus interdum tortor. Donec ultrices, tellus et suscipit tempor, urna lectus congue lorem, porta laoreet nibh purus id nisl. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Suspendisse malesuada hendrerit turpis vel feugiat. Suspendisse at mi cursus, dictum ipsum in, tristique mi. Cras diam nisi, aliquam vel lacinia sed, vehicula id diam. Sed consectetur tempus massa, a pellentesque dolor posuere vel. Curabitur et finibus quam. Fusce orci magna, faucibus ac tellus id, fermentum tempor neque. Ut libero augue, egestas eget fringilla eget, suscipit vitae orci.
Nunc posuere nisi purus, a bibendum elit interdum id. Donec accumsan metus non rutrum porttitor. Donec in sollicitudin lorem. Sed porta magna et lobortis consequat. Sed eget tempus nibh. Ut sed ante vel ex interdum commodo. Fusce eget nisi nec mauris tristique scelerisque. Ut eleifend nunc ultrices mattis ultricies. Vivamus id sodales lacus. Vivamus dignissim elementum ante, sit amet ornare ipsum lacinia sit amet. Proin maximus pulvinar nibh vitae luctus. Sed at gravida metus, at ultricies eros. Integer porta velit auctor, dictum elit non, dictum dolor.
Suspendisse potenti. Morbi lobortis orci nisi, ac sollicitudin libero cursus sed. Integer vestibulum sem ac ante rutrum, at pellentesque tortor bibendum. Etiam at arcu et sapien molestie sodales. Nam fringilla turpis eget mi convallis, quis iaculis metus varius. Vivamus finibus purus ut nunc sodales pulvinar. Praesent varius, odio sed luctus tempus, orci augue sodales orci, a posuere nisi ante et justo. Ut ultricies rhoncus feugiat. Ut arcu massa, scelerisque vitae metus rhoncus, aliquet ultricies urna.
Nulla facilisi. Aliquam at porttitor quam. Pellentesque eu faucibus nunc. Sed nec leo leo. Integer cursus ex a magna lacinia interdum. Vivamus luctus lacinia odio in sollicitudin. Curabitur sem felis, condimentum sed nisi at, posuere ullamcorper arcu. Vivamus convallis vel nisl id suscipit. Maecenas semper dictum nibh, nec malesuada dolor semper id. Maecenas turpis massa, vulputate ac euismod quis, viverra id lorem. Integer imperdiet tincidunt bibendum. Donec laoreet viverra volutpat. Aliquam arcu elit, euismod vitae felis quis, venenatis commodo lectus.
Fusce mattis mi est. Proin augue dui, ultricies quis facilisis tristique, vehicula quis ex. Nunc blandit aliquet aliquet. Aliquam nibh sem, tempus a justo sit amet, viverra ullamcorper sem. Suspendisse potenti. Pellentesque quis risus eget ipsum tincidunt ornare ultrices at sapien. Phasellus viverra nec urna auctor dapibus. Curabitur id porttitor justo. Vestibulum risus sapien, dignissim vel libero ac, scelerisque fringilla risus. Donec porta at ex quis volutpat. Suspendisse potenti. Phasellus leo metus, pellentesque et laoreet sed, tempus ut ligula. Sed convallis elit libero, sed ornare odio mattis et.
Ut lobortis libero vel nibh dignissim ullamcorper. Curabitur sed tortor id leo bibendum dignissim at sit amet dolor. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed vitae urna justo. Quisque in fringilla velit. Nullam venenatis laoreet tellus, sagittis sodales dolor facilisis ut. Integer tincidunt at est aliquet commodo. Cras sed urna ut sapien varius rhoncus. Duis pharetra a magna ut tincidunt.
Sed mattis nunc ut pulvinar sodales. Nullam bibendum, ex vulputate volutpat porttitor, sapien mi gravida augue, at scelerisque mi ipsum sit amet risus. Phasellus non lacus molestie, faucibus nibh vitae, bibendum ex. Mauris et elit ut augue tincidunt pellentesque. Vestibulum vehicula pretium magna in faucibus. In fringilla tincidunt nisi. Integer porta vehicula risus eu commodo. Quisque iaculis laoreet vestibulum. Donec vitae aliquam metus, a gravida purus. Integer sit amet ligula vitae tortor lacinia mollis. Phasellus leo tellus, mattis ut maximus quis, ultricies in mauris. Praesent a scelerisque quam.
Nunc tempor feugiat accumsan. Aliquam erat volutpat. Cras ut mi odio. Aenean sed purus sed nunc luctus tempor sit amet id ligula. Morbi nec faucibus neque, a luctus nisl. Maecenas aliquam lorem sed blandit maximus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
In hac habitasse platea dictumst. Cras blandit nibh lacus, nec venenatis nulla elementum nec. Nam tincidunt posuere augue rhoncus mattis. Cras interdum tincidunt massa, ac tristique libero porta in. Integer hendrerit gravida nisi, ut fringilla ligula ullamcorper nec. Fusce erat nulla, ullamcorper vel pulvinar eget, feugiat molestie velit. Integer viverra sollicitudin velit, vitae accumsan orci aliquet ac. Suspendisse aliquet augue ac mollis hendrerit. Proin euismod sagittis metus ac pretium. Nam augue nulla, posuere ac tortor nec, ornare interdum risus. Proin non massa eros. Morbi aliquet ante et purus placerat faucibus.
Quisque pharetra dolor vitae magna vehicula fermentum quis nec odio. In commodo tincidunt turpis non scelerisque. Nam tempor consequat justo suscipit convallis. Sed sit amet augue at lorem porta eleifend sit amet sit amet velit. Fusce eu eros quis risus mattis vehicula eu at quam. Nulla faucibus elementum tincidunt. Sed in diam accumsan, blandit felis a, cursus mauris.
Nunc in hendrerit augue. Phasellus vel purus ullamcorper, auctor ante sed, auctor nisi. Nunc rutrum est erat, ut consequat elit mattis ac. Suspendisse potenti. Maecenas purus libero, pharetra quis vestibulum at, egestas eget enim. Vestibulum ut condimentum tortor. Aliquam vestibulum mattis placerat. Morbi non leo eu nisl maximus lobortis eget eget sapien. Donec sollicitudin ipsum nulla, non dignissim massa pretium sed. Nulla et molestie nibh, nec aliquet arcu.
Cras euismod ligula justo, at dignissim nibh mattis vitae. Integer bibendum sit amet urna vitae dapibus. Fusce gravida ut enim eget molestie. Quisque finibus nisl ut odio mattis, ut viverra justo pretium. Nunc convallis cursus tincidunt. Integer vel hendrerit orci. Donec leo orci, elementum at cursus a, eleifend a nisl. Nunc massa nunc, blandit non ex pretium, volutpat feugiat tortor. Phasellus gravida nibh ipsum, ac consequat ante eleifend eu. Nulla venenatis auctor efficitur. Vestibulum eleifend eros id nibh interdum, id tempus tortor molestie. Nam id finibus quam. Aenean eu vulputate ante. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Curabitur hendrerit mi ac venenatis suscipit.
Vestibulum lobortis maximus metus, a commodo massa ultricies et. Etiam ut lectus quis lacus euismod consequat. Maecenas tincidunt, felis dapibus aliquam gravida, est sapien tempus nibh, et rutrum dolor orci in dui. Proin tellus nisl, imperdiet mollis elit ac, gravida lacinia sapien. Aenean quis lectus id ante gravida lobortis. In sollicitudin et ex et facilisis. Curabitur in velit at augue commodo placerat. Nulla facilisi. Fusce facilisis est eu dolor rhoncus, a bibendum felis tincidunt. Sed sit amet feugiat ante. Sed accumsan, ipsum nec bibendum semper, justo elit dignissim magna, eu lacinia sem lorem sed justo. Etiam posuere sollicitudin nisi ac placerat. Duis maximus nisl nec nisi accumsan posuere.
Aliquam erat volutpat. Suspendisse tincidunt ut neque eu tincidunt. Nulla porttitor eu mi sed gravida. Maecenas in cursus nisl, et fringilla quam. Nulla posuere, turpis vel vestibulum condimentum, magna dui eleifend orci, nec semper metus risus id augue. Proin pretium risus id elit consequat tristique. Phasellus eget tortor eleifend, euismod arcu at, ullamcorper nibh. Etiam id ultricies dui, non mattis purus. Morbi sit amet mi diam. Nullam dictum erat vitae tortor lobortis imperdiet. Etiam venenatis ante non laoreet gravida.
Vestibulum eget egestas mauris. Nulla facilisi. Vivamus ut dignissim turpis. Nulla quis tincidunt libero. Ut eget metus eleifend, volutpat enim ac, semper metus. Nunc vel ex nec neque maximus rhoncus vel et orci. Ut vitae congue sem, et porttitor diam.
Duis sit amet est nec nibh scelerisque pellentesque ac in quam. Donec eget erat nec diam mattis feugiat eget a ex. Morbi interdum est non tortor accumsan porta. In hendrerit libero sit amet ex blandit, ut vestibulum quam dapibus. Integer quis velit id nibh tempor volutpat. Suspendisse euismod, sapien nec tempor malesuada, augue nunc tincidunt diam, et viverra dui massa sit amet velit. Proin efficitur, nisl non vulputate maximus, sapien nulla pharetra odio, vel eleifend neque velit at est. Phasellus leo sem, pharetra placerat lorem faucibus, pellentesque euismod mi. Nam vel diam neque. Praesent mollis feugiat magna vitae pellentesque. Proin elementum rhoncus ante, eget eleifend arcu volutpat quis. Fusce id enim sed odio mollis pulvinar sed a nulla. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae;
Aliquam vulputate justo ut arcu lacinia accumsan. Phasellus quis convallis arcu, vel viverra urna. Curabitur tincidunt nibh id iaculis facilisis. In tellus arcu, elementum vel luctus at, tempor quis magna. Morbi maximus, elit in pharetra lacinia, ex lorem imperdiet risus, eget vehicula sapien enim sed nunc. Aliquam faucibus sodales tortor, vel fermentum quam finibus eu. Duis non dolor accumsan, lobortis neque a, pharetra purus. Donec a tincidunt urna. Cras eleifend elit ac tortor dictum rutrum. Donec ut est et diam tristique pellentesque. In mattis diam justo, sit amet rutrum ante varius nec. Sed in orci sit amet neque finibus fermentum. Aenean risus lectus, laoreet vitae finibus non, semper non sem. Donec vel porttitor mi. Duis efficitur posuere odio, in facilisis magna fringilla et. Donec quis tristique erat.
Suspendisse mattis mattis mi at tristique. Ut ultricies sagittis iaculis. Aliquam sagittis, diam in commodo sagittis, odio nisl venenatis enim, suscipit venenatis felis diam vitae enim. Morbi a molestie urna, ac lacinia nunc. Nam quis metus augue. Fusce tristique orci ut euismod cursus. Pellentesque sed elit ac nisl placerat porttitor. Quisque molestie pharetra eros sed hendrerit. In tincidunt tellus in consequat ultricies. Nam condimentum sollicitudin aliquam. Nulla non odio urna. Aliquam gravida tincidunt erat at semper. Sed quis tempor turpis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
Maecenas cursus massa tincidunt tortor placerat tempor. In justo nisl, posuere eu erat eget, vestibulum ultrices nulla. Vivamus vel est a erat gravida dapibus ut ac mauris. Integer eleifend gravida odio, aliquet tincidunt justo sollicitudin et. Ut ut ex in diam feugiat varius quis sit amet lectus. Aliquam sit amet nibh condimentum, sollicitudin neque id, vulputate orci. In vestibulum hendrerit libero, ut dapibus dui efficitur a. Aenean tristique at arcu id viverra. Maecenas cursus massa vel felis laoreet pretium. Praesent viverra tincidunt risus non sodales. Nulla id dignissim est, sed pharetra purus. Aliquam commodo, eros varius sagittis faucibus, diam lorem viverra erat, vel egestas ipsum dui sed turpis. Aenean placerat rhoncus nibh vel finibus.
Interdum et malesuada fames ac ante ipsum primis in faucibus. Ut porttitor, nulla sed pharetra laoreet, lacus orci molestie mi, sed ullamcorper felis arcu id sapien. Curabitur et commodo velit. In pharetra arcu at augue pulvinar, commodo elementum augue condimentum. Sed mattis ipsum sed tempor faucibus. Donec non dolor sed purus ultrices condimentum sit amet vel nisi. Cras dignissim tellus et sapien porta elementum. Maecenas in eros vel orci congue dignissim. Donec et luctus libero. Phasellus vitae porttitor nisi. Donec a nibh nisi. Nulla in nibh nec tellus accumsan pretium. Aenean ultrices id est blandit malesuada. Integer blandit metus sed suscipit scelerisque. In ac urna cursus, vehicula justo nec, facilisis metus. Vivamus id scelerisque diam.
In egestas nulla non tortor viverra laoreet. Fusce porttitor sem urna, sed varius justo maximus a. Duis ligula libero, elementum quis viverra mattis, vehicula nec sem. Aenean ut libero eu est auctor placerat. Nam quam dui, accumsan eget ipsum in, ornare volutpat purus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nunc blandit, justo ut fermentum tincidunt, ex dolor suscipit eros, sed pretium lacus dolor vel tortor. Aliquam erat volutpat.
Phasellus sagittis risus nec accumsan congue. Maecenas enim elit, mattis sit amet enim tristique, vestibulum sodales leo. Curabitur vel vehicula velit. Proin elementum dui at purus pulvinar, et dictum nisl sagittis. Nulla aliquet quam ultrices dapibus rutrum. Integer ultrices dapibus mauris, a vehicula elit posuere ut. Nullam lacinia mattis nulla, dignissim aliquam massa consequat at. Mauris ut mi dictum, iaculis urna rhoncus, euismod massa. Phasellus elementum libero id turpis imperdiet dictum. Nunc ac ipsum vitae orci ullamcorper tempus. Quisque lacinia quam sed molestie sagittis. Aenean sed rutrum sapien, et scelerisque sem.
Suspendisse tincidunt, leo in vulputate rhoncus, neque augue feugiat purus, ac tempor libero nisi vitae ante. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean at augue quis ante porttitor blandit vitae non magna. Donec vel ligula hendrerit, sodales massa et, placerat libero. Etiam lectus felis, luctus et varius hendrerit, dignissim at massa. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Phasellus velit tortor, rutrum ut tristique non, feugiat at turpis. In iaculis leo metus, eu laoreet nunc viverra vel. Nam porttitor vitae diam vel pellentesque. In ullamcorper nulla id purus cursus accumsan. Etiam et mi turpis. Praesent volutpat sem odio, vitae dapibus libero posuere ac. Mauris dignissim ultricies felis, eu vestibulum turpis fringilla vitae. Sed porttitor ornare consectetur. Nulla id justo mollis, iaculis leo sit amet, bibendum eros. Donec ac interdum sapien.
Praesent dolor urna, tincidunt a neque eu, hendrerit luctus odio. Pellentesque ac odio in est maximus maximus. Donec et lectus aliquet massa egestas euismod cursus et magna. Nam id mi hendrerit, rutrum eros iaculis, sodales enim. Nullam porttitor urna ac malesuada tincidunt. Suspendisse a turpis sapien. Curabitur rhoncus vel libero ut volutpat. Nam a sapien sem. Duis ac convallis eros, ut auctor nulla.
Interdum et malesuada fames ac ante ipsum primis in faucibus. In sagittis lorem vitae est laoreet dictum. Integer posuere est ac tortor sagittis, quis mattis ligula laoreet. Curabitur urna sem, finibus id euismod at, varius ac sem. Donec tristique tristique est, a dictum lorem ornare eu. Praesent tempor diam ligula, at placerat leo placerat nec. Nam eget dui vulputate, vestibulum nisi ut, vehicula dui. Aenean elementum tincidunt lectus at vestibulum. Nam tristique mauris sit amet sapien iaculis facilisis. Nam accumsan, nibh ac aliquam venenatis, dolor lacus pretium dolor, accumsan feugiat lectus tellus facilisis sem. Nullam a dui sit amet tortor elementum rhoncus. Aliquam mattis eget quam et lobortis. Interdum et malesuada fames ac ante ipsum primis in faucibus. Suspendisse ultricies, magna eu imperdiet efficitur, tellus sapien suscipit massa, sit amet finibus dolor libero vitae ante. Maecenas vel aliquam massa.
+7 -64
View File
@@ -2,73 +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>
<h3>Patch 1.6.3 &ndash; 18 August 2022</h3>
<p>This is a bugfix release that fixes a rare problem causing novelWriter to crash if the spell
checker language setting was configured to an empty value.</p>
<p>A few other minor issues have also been fixed: The project language setting is now properly
exported to ODT documents. Spaces are no longer inserted automatically in front of colons in
certain meta data settings when the feature is enabled (it is primarily used for French). Lastly,
the slider splitting the editor and viewer panels can no longer be dragged until the viewer
disappears. It was not necessarily obvious how the viewer panel could be restored in such cases.
</p>
<h3>Patch 1.6.4 &ndash; 29 September 2022</h3>
<p>This is a bugfix release that fixes a critical bug in the insert non-breaking spaces feature. It
basically no longer worked in the 1.6.3 release. This release also fixes a minor issue where the
text cursor sometimes disappears when reaching the right-hand edge of the text editor window.</p>
</body>
</html>
+1 -1
View File
@@ -10,7 +10,7 @@ licenseurl = https://creativecommons.org/licenses/by-sa/4.0/
window = 54, 54, 54
windowtext = 174, 174, 174
base = 62, 62, 62
alternatebase = 67, 67, 67
alternatebase = 78, 78, 78
text = 174, 174, 174
tooltipbase = 255, 255, 192
tooltiptext = 21, 21, 13
@@ -10,7 +10,7 @@ licenseurl = https://github.com/altercation/solarized/blob/master/LICENSE
window = 0, 43, 54
windowtext = 253, 246, 227
base = 7, 54, 66
alternatebase = 67, 67, 67
alternatebase = 0, 43, 54
text = 253, 246, 227
tooltipbase = 133, 153, 0
tooltiptext = 0, 43, 54
+13 -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.
@@ -241,23 +247,17 @@ def formatTime(tS):
return "ERROR"
def parseTimeStamp(theStamp, default, allowNone=False):
"""Parses a text representation of a timestamp and converts it into
a float. Note that negative timestamps cause an OSError on Windows.
See https://bugs.python.org/issue29097
"""
if str(theStamp).lower() == "none" and allowNone:
return None
try:
return datetime.strptime(theStamp, nwConst.FMT_TSTAMP).timestamp()
except Exception:
return default
# =============================================================================================== #
# 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.
+5 -31
View File
@@ -96,8 +96,6 @@ class Config:
# Sizes
self.winGeometry = [1200, 650]
self.prefGeometry = [700, 615]
self.treeColWidth = [200, 50, 30]
self.novelColWidth = [200, 50]
self.projColWidth = [200, 60, 140]
self.mainPanePos = [300, 800]
self.docPanePos = [400, 400]
@@ -117,7 +115,7 @@ class Config:
# Text Editor
self.textFont = None # Editor font
self.textSize = 12 # Editor font size
self.textWidth = 600 # Editor text width
self.textWidth = 700 # Editor text width
self.textMargin = 40 # Editor/viewer text margin
self.tabWidth = 40 # Editor tabulator width
@@ -460,8 +458,6 @@ class Config:
cnfSec = "Sizes"
self.winGeometry = theConf.rdIntList(cnfSec, "geometry", self.winGeometry)
self.prefGeometry = theConf.rdIntList(cnfSec, "preferences", self.prefGeometry)
self.treeColWidth = theConf.rdIntList(cnfSec, "treecols", self.treeColWidth)
self.novelColWidth = theConf.rdIntList(cnfSec, "novelcols", self.novelColWidth)
self.projColWidth = theConf.rdIntList(cnfSec, "projcols", self.projColWidth)
self.mainPanePos = theConf.rdIntList(cnfSec, "mainpane", self.mainPanePos)
self.docPanePos = theConf.rdIntList(cnfSec, "docpane", self.docPanePos)
@@ -581,8 +577,6 @@ class Config:
theConf["Sizes"] = {
"geometry": self._packList(self.winGeometry),
"preferences": self._packList(self.prefGeometry),
"treecols": self._packList(self.treeColWidth),
"novelcols": self._packList(self.novelColWidth),
"projcols": self._packList(self.projColWidth),
"mainpane": self._packList(self.mainPanePos),
"docpane": self._packList(self.docPanePos),
@@ -811,20 +805,6 @@ class Config:
self.confChanged = True
return True
def setTreeColWidths(self, colWidths):
"""Set the column widths of the main project tree.
"""
self.treeColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True
return True
def setNovelColWidths(self, colWidths):
"""Set the column widths of the novel tree.
"""
self.novelColWidth = [int(x/self.guiScale) for x in colWidths]
self.confChanged = True
return True
def setProjColWidths(self, colWidths):
"""Set the column widths of the Load Project dialog.
"""
@@ -910,12 +890,6 @@ class Config:
def getPreferencesSize(self):
return [int(x*self.guiScale) for x in self.prefGeometry]
def getTreeColWidths(self):
return [int(x*self.guiScale) for x in self.treeColWidth]
def getNovelColWidths(self):
return [int(x*self.guiScale) for x in self.novelColWidth]
def getProjColWidths(self):
return [int(x*self.guiScale) for x in self.projColWidth]
@@ -978,12 +952,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
+15 -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,16 @@ class nwRegEx():
# END Class nwRegEx
class nwFiles():
class nwHeaders:
H_VALID = ("H0", "H1", "H2", "H3", "H4")
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
TT_NONE = "T000000"
# END Class nwHeaders
class nwFiles:
PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt"
@@ -126,7 +116,7 @@ class nwKeyWords:
# END Class nwKeyWords
class nwLabels():
class nwLabels:
CLASS_NAME = {
nwItemClass.NO_CLASS: QT_TRANSLATE_NOOP("Constant", "None"),
@@ -204,7 +194,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
+782 -400
View File
File diff suppressed because it is too large Load Diff
+222 -78
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,11 +45,13 @@ 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
self._layout = nwItemLayout.NO_LAYOUT
self._status = None
self._import = None
self._expanded = False
self._exported = True
@@ -84,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
@@ -104,6 +110,10 @@ class NWItem():
def itemStatus(self):
return self._status
@property
def itemImport(self):
return self._import
@property
def isExpanded(self):
return self._expanded
@@ -139,24 +149,33 @@ class NWItem():
def packXML(self, xParent):
"""Pack all the data in the class instance into an XML object.
"""
xPack = etree.SubElement(xParent, "item", attrib={
"handle": str(self._handle),
"order": str(self._order),
"parent": str(self._parent),
})
self._subPack(xPack, "name", text=str(self._name))
self._subPack(xPack, "type", text=str(self._type.name))
self._subPack(xPack, "class", text=str(self._class.name))
self._subPack(xPack, "status", text=str(self._status))
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)
if self._type == nwItemType.FILE:
self._subPack(xPack, "exported", text=str(self._exported))
self._subPack(xPack, "layout", text=str(self._layout.name))
self._subPack(xPack, "charCount", text=str(self._charCount), none=False)
self._subPack(xPack, "wordCount", text=str(self._wordCount), none=False)
self._subPack(xPack, "paraCount", text=str(self._paraCount), none=False)
self._subPack(xPack, "cursorPos", text=str(self._cursorPos), none=False)
else:
self._subPack(xPack, "expanded", text=str(self._expanded))
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)
nameAttrib = {}
nameAttrib["status"] = str(self._status)
nameAttrib["import"] = str(self._import)
if self._type == nwItemType.FILE:
nameAttrib["exported"] = str(self._exported)
xPack = etree.SubElement(xParent, "item", attrib=itemAttrib)
self._subPack(xPack, "meta", attrib=metaAttrib)
self._subPack(xPack, "name", text=str(self._name), attrib=nameAttrib)
return
@@ -174,20 +193,34 @@ 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))
self.setLayout(xItem.attrib.get("layout", nwItemLayout.NO_LAYOUT))
tmpStatus = ""
for xValue in xItem:
if xValue.tag == "name":
if xValue.tag == "meta":
self.setExpanded(xValue.attrib.get("expanded", False))
self.setCharCount(xValue.attrib.get("charCount", 0))
self.setWordCount(xValue.attrib.get("wordCount", 0))
self.setParaCount(xValue.attrib.get("paraCount", 0))
self.setCursorPos(xValue.attrib.get("cursorPos", 0))
elif xValue.tag == "name":
self.setName(xValue.text)
self.setStatus(xValue.attrib.get("status", None))
self.setImport(xValue.attrib.get("import", None))
self.setExported(xValue.attrib.get("exported", True))
# Legacy Format (1.3 and earlier)
elif xValue.tag == "status":
self.setImportStatus(xValue.text)
elif xValue.tag == "type":
self.setType(xValue.text)
elif xValue.tag == "class":
self.setClass(xValue.text)
elif xValue.tag == "layout":
self.setLayout(xValue.text)
elif xValue.tag == "status":
tmpStatus = xValue.text
elif xValue.tag == "expanded":
self.setExpanded(xValue.text)
elif xValue.tag == "exported":
@@ -206,8 +239,16 @@ class NWItem():
# version of novelWriter that doesn't know the tag
logger.error("Unknown tag '%s'", xValue.tag)
# Guarantees that <status> is parsed after <class>
self.setStatus(tmpStatus)
# 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
@@ -225,7 +266,7 @@ class NWItem():
return
##
# Methods
# Lookup Methods
##
def describeMe(self, hLevel=None):
@@ -251,142 +292,245 @@ class NWItem():
return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, ""))
def getImportStatus(self):
"""Return the relevant importance or status label and icon for
the current item based on its class.
"""
if self.isNovelLike():
stName = self.theProject.statusItems.name(self._status)
stIcon = self.theProject.statusItems.icon(self._status)
else:
stName = self.theProject.importItems.name(self._import)
stIcon = self.theProject.importItems.icon(self._import)
return stName, stIcon
##
# Checker Methods
##
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 isRootType(self):
return self._type == nwItemType.ROOT
def isFolderType(self):
return self._type == nwItemType.FOLDER
def isFileType(self):
return self._type == nwItemType.FILE
def isNoteLayout(self):
return self._layout == nwItemLayout.NOTE
def isDocumentLayout(self):
return self._layout == nwItemLayout.DOCUMENT
##
# Special Setters
##
def setImportStatus(self, value):
"""Update the importance or status value based on class. This is
a wrapper setter for setStatus and setImport.
"""
if self.isNovelLike():
self.setStatus(value)
else:
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.
"""
if self._class in nwLists.CLS_NOVEL:
self._status = self.theProject.statusItems.checkEntry(theStatus)
else:
self._status = self.theProject.importItems.checkEntry(theStatus)
self._status = self.theProject.statusItems.check(value)
return
def setExpanded(self, expState):
def setImport(self, value):
"""Set the item importance by looking it up in the valid import
items of the current project.
"""
self._import = self.theProject.importItems.check(value)
return
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):
+20 -2
View File
@@ -28,6 +28,8 @@ import os
import json
import logging
from enum import Enum
from novelwriter.error import logException
from novelwriter.common import checkBool, checkFloat, checkInt, checkString
from novelwriter.constants import nwFiles
@@ -56,7 +58,8 @@ VALID_MAP = {
"winWidth", "winHeight", "widthCol0", "widthCol1", "widthCol2",
"widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble"
},
"GuiWordList": {"winWidth", "winHeight"}
"GuiWordList": {"winWidth", "winHeight"},
"GuiNovelView": {"lastCol"},
}
@@ -137,7 +140,10 @@ class OptionState():
if group not in self._theState:
self._theState[group] = {}
self._theState[group][name] = value
if isinstance(value, Enum):
self._theState[group][name] = value.name
else:
self._theState[group][name] = value
return True
@@ -186,4 +192,16 @@ class OptionState():
return checkBool(self._theState[group].get(name, default), default)
return default
def getEnum(self, group, name, lookup, default):
"""Return the value mapped to an enum. Otherwise return the
default value
"""
if issubclass(lookup, Enum):
if group in self._theState:
if name in self._theState[group]:
value = self._theState[group][name]
if value in lookup.__members__:
return lookup[value]
return default
# END Class OptionState
File diff suppressed because it is too large Load Diff
+207 -102
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,165 +24,269 @@ 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
from lxml import etree
from novelwriter.common import checkInt
from PyQt5.QtGui import QIcon, QPixmap, QColor
from novelwriter.common import checkInt, minmax, simplified
logger = logging.getLogger(__name__)
class NWStatus():
def __init__(self):
STATUS = 1
IMPORT = 2
self._theLabels = []
self._theColours = []
self._theCounts = []
self._theMap = {}
self._theLength = 0
self._theIndex = 0
def __init__(self, type):
self._type = type
self._store = {}
self._reverse = {}
self._default = None
self._iconSize = novelwriter.CONFIG.pxInt(32)
pixmap = QPixmap(self._iconSize, self._iconSize)
pixmap.fill(QColor(100, 100, 100))
self._defaultIcon = QIcon(pixmap)
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.lookupEntry(theLabel) is None:
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 lookupEntry(self, theLabel):
"""Look up a status entry in the object lists, and return it if
it exists.
def check(self, value):
"""Check the key against the stored status names.
"""
if theLabel is None:
return None
theLabel = theLabel.strip()
if theLabel in self._theMap.keys():
return self._theMap[theLabel]
return None
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 checkEntry(self, theStatus):
"""Check if a status value is valid, and returns the safe
reference to be used internally.
def name(self, key):
"""Return the name associated with a given key.
"""
if isinstance(theStatus, str):
theStatus = theStatus.strip()
if self.lookupEntry(theStatus) is not None:
return theStatus
theStatus = checkInt(theStatus, 0, False)
if theStatus >= 0 and theStatus < self._theLength:
return self._theLabels[theStatus]
return self._theLabels[0]
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._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.lookupEntry(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={
"blue": str(self._theColours[n][2]),
"green": str(self._theColours[n][1]),
"red": str(self._theColours[n][0]),
"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))
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)
if len(theLabels) > 0:
self._theLabels = []
self._theColours = []
self._theCounts = []
self._theMap = {}
self._theLength = 0
self._theIndex = 0
return True
for n in range(len(theLabels)):
self.addEntry(theLabels[n], theColours[n])
##
# Internal Functions
##
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.
"""
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]
return None, None, None
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 = self.__getitem__(self._theIndex)
self._theIndex += 1
return theLabel, theColour, theCount
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 ""
+10 -6
View File
@@ -1,7 +1,7 @@
"""
novelWriter Text Tokenizer
============================
Splits a piece of novelWriter markdown text into its elements
Split novelWriter plain text into its elements
File History:
Created: 2019-05-05 [0.0.1]
@@ -27,6 +27,7 @@ import re
import logging
import novelwriter
from abc import ABC, abstractmethod
from operator import itemgetter
from functools import partial
@@ -40,7 +41,7 @@ from novelwriter.core.document import NWDoc
logger = logging.getLogger(__name__)
class Tokenizer():
class Tokenizer(ABC):
# In-Text Format
FMT_B_B = 1 # Begin bold
@@ -81,7 +82,6 @@ class Tokenizer():
def __init__(self, theProject):
self.theProject = theProject
self.theParent = theProject.theParent
self.mainConf = novelwriter.CONFIG
# Data Variables
@@ -267,10 +267,14 @@ class Tokenizer():
# Class Methods
##
@abstractmethod
def doConvert(self):
raise NotImplementedError
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:
@@ -279,7 +283,7 @@ class Tokenizer():
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 = []
@@ -296,7 +300,7 @@ class Tokenizer():
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 ""
+2 -2
View File
@@ -330,7 +330,7 @@ class ToOdt(Tokenizer):
# Meta Data
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "creation-date"))
xMeta.text = datetime.now().strftime(r"%Y-%m-%dT%H:%M:%S")
xMeta.text = datetime.now().isoformat(sep="T", timespec="seconds")
xMeta = etree.SubElement(self._xMeta, _mkTag("meta", "generator"))
xMeta.text = f"novelWriter/{novelwriter.__version__}"
@@ -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 ""
+112 -140
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.enum import 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
@@ -96,20 +94,19 @@ class NWTree():
nwItem.setHandle(tHandle)
nwItem.setParent(pHandle)
if nwItem.itemType == nwItemType.ROOT:
if nwItem.isRootType():
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,21 +351,13 @@ 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.
"""
tItem = self.__getitem__(tHandle)
if tItem is None:
return False
if tItem.itemType != nwItemType.FILE:
if not tItem.isFileType():
logger.error("Item '%s' is not a file", tHandle)
return False
if not isinstance(itemLayout, nwItemLayout):
@@ -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
+2 -2
View File
@@ -22,7 +22,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from novelwriter.dialogs.about import GuiAbout
from novelwriter.dialogs.docmerge import GuiDocMerge
from novelwriter.dialogs.docsplit import GuiDocSplit
from novelwriter.dialogs.itemeditor import GuiItemEditor
from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.dialogs.preferences import GuiPreferences
from novelwriter.dialogs.projdetails import GuiProjectDetails
from novelwriter.dialogs.projload import GuiProjectLoad
@@ -35,7 +35,7 @@ __all__ = [
"GuiAbout",
"GuiDocMerge",
"GuiDocSplit",
"GuiItemEditor",
"GuiEditLabel",
"GuiPreferences",
"GuiProjectDetails",
"GuiProjectLoad",
+30 -29
View File
@@ -43,15 +43,15 @@ logger = logging.getLogger(__name__)
class GuiAbout(QDialog):
def __init__(self, theParent):
QDialog.__init__(self, theParent)
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
logger.debug("Initialising GuiAbout ...")
self.setObjectName("GuiAbout")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.outerBox = QVBoxLayout()
self.innerBox = QHBoxLayout()
@@ -63,7 +63,7 @@ class GuiAbout(QDialog):
nPx = self.mainConf.pxInt(96)
self.nwIcon = QLabel()
self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx)))
self.nwIcon.setPixmap(self.mainGui.mainTheme.getPixmap("novelwriter", (nPx, nPx)))
self.lblName = QLabel("<b>novelWriter</b>")
self.lblVers = QLabel(f"v{novelwriter.__version__}")
self.lblDate = QLabel(datetime.strptime(novelwriter.__date__, "%Y-%m-%d").strftime("%x"))
@@ -182,45 +182,46 @@ class GuiAbout(QDialog):
self.tr("Translations"),
self._wrapTable([
("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"),
])
)
theTheme = self.theParent.theTheme
theIcons = self.theParent.theTheme.theIcons
if theTheme.themeName and theTheme.themeAuthor != "N/A":
licURL = f"<a href='{theTheme.themeLicenseUrl}'>{theTheme.themeLicense}</a>"
mainTheme = self.mainGui.mainTheme
iconCache = self.mainGui.mainTheme.iconCache
if mainTheme.themeName and mainTheme.themeAuthor != "N/A":
licURL = f"<a href='{mainTheme.themeLicenseUrl}'>{mainTheme.themeLicense}</a>"
aboutMsg += "<h4>{0}</h4><p>{1}</p>".format(
self.tr("Theme: {0}").format(theTheme.themeName),
self.tr("Theme: {0}").format(mainTheme.themeName),
self._wrapTable([
(self.tr("Author"), theTheme.themeAuthor),
(self.tr("Credit"), theTheme.themeCredit),
(self.tr("Author"), mainTheme.themeAuthor),
(self.tr("Credit"), mainTheme.themeCredit),
(self.tr("Licence"), licURL),
])
)
if theIcons.themeName:
licURL = f"<a href='{theIcons.themeLicenseUrl}'>{theIcons.themeLicense}</a>"
if iconCache.themeName:
licURL = f"<a href='{iconCache.themeLicenseUrl}'>{iconCache.themeLicense}</a>"
aboutMsg += "<h4>{0}</h4><p>{1}</p>".format(
self.tr("Icons: {0}").format(theIcons.themeName),
self.tr("Icons: {0}").format(iconCache.themeName),
self._wrapTable([
(self.tr("Author"), theIcons.themeAuthor),
(self.tr("Credit"), theIcons.themeCredit),
(self.tr("Author"), iconCache.themeAuthor),
(self.tr("Credit"), iconCache.themeCredit),
(self.tr("Licence"), licURL),
])
)
if theTheme.syntaxName:
licURL = f"<a href='{theTheme.syntaxLicenseUrl}'>{theTheme.syntaxLicense}</a>"
if mainTheme.syntaxName:
licURL = f"<a href='{mainTheme.syntaxLicenseUrl}'>{mainTheme.syntaxLicense}</a>"
aboutMsg += "<h4>{0}</h4><p>{1}</p>".format(
self.tr("Syntax: {0}").format(theTheme.syntaxName),
self.tr("Syntax: {0}").format(mainTheme.syntaxName),
self._wrapTable([
(self.tr("Author"), theTheme.syntaxAuthor),
(self.tr("Credit"), theTheme.syntaxCredit),
(self.tr("Author"), mainTheme.syntaxAuthor),
(self.tr("Credit"), mainTheme.syntaxCredit),
(self.tr("Licence"), licURL),
])
)
@@ -278,12 +279,12 @@ class GuiAbout(QDialog):
" padding-right: 0.8em;"
"}}\n"
).format(
hColR=self.theParent.theTheme.colHead[0],
hColG=self.theParent.theTheme.colHead[1],
hColB=self.theParent.theTheme.colHead[2],
kColR=self.theTheme.colKey[0],
kColG=self.theTheme.colKey[1],
kColB=self.theTheme.colKey[2],
hColR=self.mainGui.mainTheme.colHead[0],
hColG=self.mainGui.mainTheme.colHead[1],
hColB=self.mainGui.mainTheme.colHead[2],
kColR=self.mainTheme.colKey[0],
kColG=self.mainTheme.colKey[1],
kColB=self.mainTheme.colKey[2],
)
self.pageAbout.document().setDefaultStyleSheet(styleSheet)
self.pageNotes.document().setDefaultStyleSheet(styleSheet)
+22 -21
View File
@@ -41,15 +41,15 @@ logger = logging.getLogger(__name__)
class GuiDocMerge(QDialog):
def __init__(self, theParent):
QDialog.__init__(self, theParent)
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
logger.debug("Initialising GuiDocMerge ...")
self.setObjectName("GuiDocMerge")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
self.mainGui = mainGui
self.theProject = mainGui.theProject
self.sourceItem = None
self.outerBox = QVBoxLayout()
@@ -57,7 +57,7 @@ class GuiDocMerge(QDialog):
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge")))
self.helpLabel = QHelpLabel(
self.tr("Drag and drop items to change the order."), self.theParent.theTheme.helpText
self.tr("Drag and drop items to change the order."), self.mainGui.mainTheme.helpText
)
self.listBox = QListWidget()
@@ -102,7 +102,7 @@ class GuiDocMerge(QDialog):
finalOrder.append(self.listBox.item(i).data(Qt.UserRole))
if len(finalOrder) == 0:
self.theParent.makeAlert(self.tr(
self.mainGui.makeAlert(self.tr(
"No source documents found. Nothing to do."
), nwAlert.ERROR)
return False
@@ -113,36 +113,37 @@ class GuiDocMerge(QDialog):
docText = inDoc.readDocument()
docErr = inDoc.getError()
if docText is None and docErr:
self.theParent.makeAlert([
self.mainGui.makeAlert([
self.tr("Failed to open document file."), docErr
], nwAlert.ERROR)
if docText:
theText += docText.rstrip("\n")+"\n\n"
if self.sourceItem is None:
self.theParent.makeAlert(self.tr(
self.mainGui.makeAlert(self.tr(
"No source folder selected. Nothing to do."
), 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)
self.mainGui.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)
outDoc = NWDoc(self.theProject, nHandle)
if not outDoc.writeDocument(theText):
self.theParent.makeAlert([
self.mainGui.makeAlert([
self.tr("Could not save document."), outDoc.getError()
], nwAlert.ERROR)
return False
self.theParent.treeView.revealNewTreeItem(nHandle)
self.theParent.openDocument(nHandle, doScroll=True)
self.mainGui.projView.revealNewTreeItem(nHandle)
self.mainGui.openDocument(nHandle, doScroll=True)
self._doClose()
@@ -164,25 +165,25 @@ class GuiDocMerge(QDialog):
are then added to the list view in order. The list itself can be
reordered by the user.
"""
tHandle = self.theParent.treeView.getSelectedHandle()
tHandle = self.mainGui.projView.getSelectedHandle()
self.sourceItem = tHandle
if tHandle is None:
return False
nwItem = self.theProject.projTree[tHandle]
nwItem = self.theProject.tree[tHandle]
if nwItem is None:
return False
if nwItem.itemType is not nwItemType.FOLDER:
self.theParent.makeAlert(self.tr(
self.mainGui.makeAlert(self.tr(
"Element selected in the project tree must be a folder."
), nwAlert.ERROR)
return False
for sHandle in self.theParent.treeView.getTreeFromHandle(tHandle):
for sHandle in self.mainGui.projView.getTreeFromHandle(tHandle):
newItem = QListWidgetItem()
nwItem = self.theProject.projTree[sHandle]
if nwItem.itemType is not nwItemType.FILE:
nwItem = self.theProject.tree[sHandle]
if not nwItem.isFileType():
continue
newItem.setText(nwItem.itemName)
newItem.setData(Qt.UserRole, sHandle)
+26 -43
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
from novelwriter.gui.custom import QHelpLabel
logger = logging.getLogger(__name__)
@@ -42,16 +41,15 @@ logger = logging.getLogger(__name__)
class GuiDocSplit(QDialog):
def __init__(self, theParent):
QDialog.__init__(self, theParent)
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
logger.debug("Initialising GuiDocSplit ...")
self.setObjectName("GuiDocSplit")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.mainGui = mainGui
self.theProject = mainGui.theProject
self.sourceItem = None
self.sourceText = []
@@ -62,7 +60,7 @@ class GuiDocSplit(QDialog):
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Document Headers")))
self.helpLabel = QHelpLabel(
self.tr("Select the maximum level to split into files."),
self.theParent.theTheme.helpText
self.mainGui.mainTheme.helpText
)
self.listBox = QListWidget()
@@ -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)
@@ -117,14 +115,14 @@ class GuiDocSplit(QDialog):
logger.verbose("GuiDocSplit split button clicked")
if self.sourceItem is None:
self.theParent.makeAlert(self.tr(
self.mainGui.makeAlert(self.tr(
"No source document selected. Nothing to do."
), 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(
self.mainGui.makeAlert(self.tr(
"Could not parse source document."
), nwAlert.ERROR)
return False
@@ -134,7 +132,7 @@ class GuiDocSplit(QDialog):
docErr = inDoc.getError()
if theText is None and docErr:
self.theParent.makeAlert([
self.mainGui.makeAlert([
self.tr("Failed to open document file."), docErr
], nwAlert.ERROR)
@@ -155,22 +153,12 @@ class GuiDocSplit(QDialog):
nFiles = len(finalOrder)
if nFiles == 0:
self.theParent.makeAlert(self.tr(
self.mainGui.makeAlert(self.tr(
"No headers found. Nothing to do."
), 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(
msgYes = self.mainGui.askQuestion(
self.tr("Split Document"),
"{0}<br><br>{1}".format(
self.tr(
@@ -186,23 +174,18 @@ class GuiDocSplit(QDialog):
return False
# Create the folder
fHandle = self.theProject.newFolder(
srcItem.itemName, srcItem.itemClass, srcItem.itemParent
)
self.theParent.treeView.revealNewTreeItem(fHandle)
fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent)
self.mainGui.projView.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(
"Creating new document '%s' with text from line %d to %d",
nHandle, iStart+1, iEnd
@@ -213,12 +196,12 @@ class GuiDocSplit(QDialog):
outDoc = NWDoc(self.theProject, nHandle)
if not outDoc.writeDocument(theText):
self.theParent.makeAlert([
self.mainGui.makeAlert([
self.tr("Could not save document."), outDoc.getError()
], nwAlert.ERROR)
return False
self.theParent.treeView.revealNewTreeItem(nHandle)
self.mainGui.projView.revealNewTreeItem(nHandle)
self._doClose()
@@ -227,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
@@ -243,17 +226,17 @@ class GuiDocSplit(QDialog):
"""
self.listBox.clear()
if self.sourceItem is None:
self.sourceItem = self.theParent.treeView.getSelectedHandle()
self.sourceItem = self.mainGui.projView.getSelectedHandle()
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
if nwItem.itemType is not nwItemType.FILE:
self.theParent.makeAlert(self.tr(
if not nwItem.isFileType():
self.mainGui.makeAlert(self.tr(
"Element selected in the project tree must be a file."
), nwAlert.ERROR)
return False
@@ -265,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
+84
View File
@@ -0,0 +1,84 @@
"""
novelWriter Edit Label Dialog
===============================
A simple dialog for editing a label
File History:
Created: 2022-06-11 [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.QtWidgets import (
QDialog, QVBoxLayout, QLineEdit, QLabel, QDialogButtonBox, QHBoxLayout
)
logger = logging.getLogger(__name__)
class GuiEditLabel(QDialog):
def __init__(self, parent, text=""):
QDialog.__init__(self, parent=parent)
self.setObjectName("GuiEditLabel")
self.setWindowTitle(self.tr("Item Label"))
mVd = novelwriter.CONFIG.pxInt(220)
mSp = novelwriter.CONFIG.pxInt(12)
# Item Label
self.labelValue = QLineEdit()
self.labelValue.setMinimumWidth(mVd)
self.labelValue.setMaxLength(200)
self.labelValue.setText(text)
self.labelValue.selectAll()
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
# Assemble
self.innerBox = QHBoxLayout()
self.innerBox.addWidget(QLabel(self.tr("Label")), 0)
self.innerBox.addWidget(self.labelValue, 1)
self.innerBox.setSpacing(mSp)
self.outerBox = QVBoxLayout()
self.outerBox.setSpacing(mSp)
self.outerBox.addLayout(self.innerBox, 1)
self.outerBox.addWidget(self.buttonBox, 0)
self.setLayout(self.outerBox)
return
@property
def itemLabel(self):
return self.labelValue.text()
@classmethod
def getLabel(cls, parent, text):
cls = GuiEditLabel(parent, text=text)
cls.exec_()
return cls.itemLabel, cls.result() == QDialog.Accepted
# END Class GuiEditLabel
-202
View File
@@ -1,202 +0,0 @@
"""
novelWriter GUI Item Editor
=============================
GUI class for the item editor dialog
File History:
Created: 2019-04-27 [0.0.1]
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 pyqtSlot
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QGridLayout, QLineEdit, QComboBox, QLabel,
QDialogButtonBox
)
from novelwriter.enum import nwItemLayout, nwItemType
from novelwriter.constants import trConst, nwLists, nwLabels
from novelwriter.gui.custom import QSwitch
logger = logging.getLogger(__name__)
class GuiItemEditor(QDialog):
def __init__(self, theParent, tHandle):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiItemEditor ...")
self.setObjectName("GuiItemEditor")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
##
# Build GUI
##
self.theItem = self.theProject.projTree[tHandle]
if self.theItem is None:
self.close()
return
self.setWindowTitle(self.tr("Item Settings"))
mVd = self.mainConf.pxInt(220)
mSp = self.mainConf.pxInt(16)
vSp = self.mainConf.pxInt(4)
# Item Label
self.editName = QLineEdit()
self.editName.setMinimumWidth(mVd)
self.editName.setMaxLength(200)
# Item Status
self.editStatus = QComboBox()
self.editStatus.setMinimumWidth(mVd)
if self.theItem.itemClass in nwLists.CLS_NOVEL:
for sLabel, _, _ in self.theProject.statusItems:
self.editStatus.addItem(
self.theParent.statusIcons[sLabel], sLabel, sLabel
)
else:
for sLabel, _, _ in self.theProject.importItems:
self.editStatus.addItem(
self.theParent.importIcons[sLabel], sLabel, sLabel
)
# Item Layout
self.editLayout = QComboBox()
self.editLayout.setMinimumWidth(mVd)
validLayouts = []
if self.theItem.itemType == nwItemType.FILE:
if self.theItem.itemClass in nwLists.CLS_NOVEL:
validLayouts.append(nwItemLayout.DOCUMENT)
validLayouts.append(nwItemLayout.NOTE)
else:
validLayouts.append(nwItemLayout.NO_LAYOUT)
self.editLayout.setEnabled(False)
for itemLayout in nwItemLayout:
if itemLayout in validLayouts:
self.editLayout.addItem(trConst(nwLabels.LAYOUT_NAME[itemLayout]), itemLayout)
# Export Switch
self.textExport = QLabel(self.tr("Include when building project"))
self.editExport = QSwitch()
if self.theItem.itemType == nwItemType.FILE:
self.editExport.setEnabled(True)
self.editExport.setChecked(self.theItem.isExported)
else:
self.editExport.setEnabled(False)
self.editExport.setChecked(False)
# Buttons
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doSave)
self.buttonBox.rejected.connect(self._doClose)
# Set Current Values
self.editName.setText(self.theItem.itemName)
self.editName.selectAll()
statusIdx = self.editStatus.findData(self.theItem.itemStatus)
if statusIdx != -1:
self.editStatus.setCurrentIndex(statusIdx)
layoutIdx = self.editLayout.findData(self.theItem.itemLayout)
if layoutIdx != -1:
self.editLayout.setCurrentIndex(layoutIdx)
##
# Assemble
##
nameLabel = QLabel(self.tr("Label"))
statusLabel = QLabel(self.tr("Status"))
layoutLabel = QLabel(self.tr("Layout"))
self.mainForm = QGridLayout()
self.mainForm.setVerticalSpacing(vSp)
self.mainForm.setHorizontalSpacing(mSp)
self.mainForm.addWidget(nameLabel, 0, 0, 1, 1)
self.mainForm.addWidget(self.editName, 0, 1, 1, 2)
self.mainForm.addWidget(statusLabel, 1, 0, 1, 1)
self.mainForm.addWidget(self.editStatus, 1, 1, 1, 2)
self.mainForm.addWidget(layoutLabel, 2, 0, 1, 1)
self.mainForm.addWidget(self.editLayout, 2, 1, 1, 2)
self.mainForm.addWidget(self.textExport, 3, 0, 1, 2)
self.mainForm.addWidget(self.editExport, 3, 2, 1, 1)
self.mainForm.setColumnStretch(0, 0)
self.mainForm.setColumnStretch(1, 1)
self.mainForm.setColumnStretch(2, 0)
self.outerBox = QVBoxLayout()
self.outerBox.setSpacing(mSp)
self.outerBox.addLayout(self.mainForm)
self.outerBox.addStretch(1)
self.outerBox.addWidget(self.buttonBox)
self.setLayout(self.outerBox)
self.rejected.connect(self._doClose)
logger.debug("GuiItemEditor initialisation complete")
return
##
# Slots
##
@pyqtSlot()
def _doSave(self):
"""Save the setting to the item.
"""
logger.verbose("ItemEditor save button clicked")
itemName = self.editName.text()
itemStatus = self.editStatus.currentData()
itemLayout = self.editLayout.currentData()
isExported = self.editExport.isChecked()
self.theItem.setName(itemName)
self.theItem.setStatus(itemStatus)
self.theItem.setLayout(itemLayout)
self.theItem.setExported(isExported)
self.theProject.setProjectChanged(True)
self.accept()
self.close()
return
@pyqtSlot()
def _doClose(self):
"""Close the dialog without saving the settings.
"""
logger.verbose("ItemEditor cancel button clicked")
self.close()
return
# END Class GuiItemEditor
+74 -78
View File
@@ -43,25 +43,25 @@ logger = logging.getLogger(__name__)
class GuiPreferences(PagedDialog):
def __init__(self, theParent):
PagedDialog.__init__(self, theParent)
def __init__(self, mainGui):
PagedDialog.__init__(self, mainGui)
logger.debug("Initialising GuiPreferences ...")
self.setObjectName("GuiPreferences")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
self.mainGui = mainGui
self.theProject = mainGui.theProject
self.setWindowTitle(self.tr("Preferences"))
self.tabGeneral = GuiPreferencesGeneral(self.theParent)
self.tabProjects = GuiPreferencesProjects(self.theParent)
self.tabDocs = GuiPreferencesDocuments(self.theParent)
self.tabEditor = GuiPreferencesEditor(self.theParent)
self.tabSyntax = GuiPreferencesSyntax(self.theParent)
self.tabAuto = GuiPreferencesAutomation(self.theParent)
self.tabQuote = GuiPreferencesQuotes(self.theParent)
self.tabGeneral = GuiPreferencesGeneral(self.mainGui)
self.tabProjects = GuiPreferencesProjects(self.mainGui)
self.tabDocs = GuiPreferencesDocuments(self.mainGui)
self.tabEditor = GuiPreferencesEditor(self.mainGui)
self.tabSyntax = GuiPreferencesSyntax(self.mainGui)
self.tabAuto = GuiPreferencesAutomation(self.mainGui)
self.tabQuote = GuiPreferencesQuotes(self.mainGui)
self.addTab(self.tabGeneral, self.tr("General"))
self.addTab(self.tabProjects, self.tr("Projects"))
@@ -102,12 +102,12 @@ class GuiPreferences(PagedDialog):
self.tabQuote.saveValues()
if needsRestart:
self.theParent.makeAlert(self.tr(
self.mainGui.makeAlert(self.tr(
"Some changes will not be applied until novelWriter has been restarted."
), nwAlert.INFO)
if refreshTree:
self.theParent.treeView.buildTree()
self.mainGui.projView.populateTree()
self._saveWindowSize()
self.accept()
@@ -138,16 +138,16 @@ class GuiPreferences(PagedDialog):
class GuiPreferencesGeneral(QWidget):
def __init__(self, theParent):
QWidget.__init__(self, theParent)
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
self.mainForm.setHelpTextStyle(self.theTheme.helpText)
self.mainForm.setHelpTextStyle(self.mainTheme.helpText)
self.setLayout(self.mainForm)
# Look and Feel
@@ -174,7 +174,7 @@ class GuiPreferencesGeneral(QWidget):
# Select Theme
self.guiTheme = QComboBox()
self.guiTheme.setMinimumWidth(minWidth)
self.theThemes = self.theTheme.listThemes()
self.theThemes = self.mainTheme.listThemes()
for themeDir, themeName in self.theThemes:
self.guiTheme.addItem(themeName, themeDir)
themeIdx = self.guiTheme.findData(self.mainConf.guiTheme)
@@ -190,8 +190,8 @@ class GuiPreferencesGeneral(QWidget):
# Select Icon Theme
self.guiIcons = QComboBox()
self.guiIcons.setMinimumWidth(minWidth)
self.theIcons = self.theTheme.theIcons.listThemes()
for iconDir, iconName in self.theIcons:
self.iconCache = self.mainTheme.iconCache.listThemes()
for iconDir, iconName in self.iconCache:
self.guiIcons.addItem(iconName, iconDir)
iconIdx = self.guiIcons.findData(self.mainConf.guiIcons)
if iconIdx != -1:
@@ -203,13 +203,29 @@ class GuiPreferencesGeneral(QWidget):
self.tr("Requires restart.")
)
# Editor Theme
self.guiSyntax = QComboBox()
self.guiSyntax.setMinimumWidth(self.mainConf.pxInt(200))
self.theSyntaxes = self.mainTheme.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)
self.guiFont.setFixedWidth(self.mainConf.pxInt(162))
self.guiFont.setText(self.mainConf.guiFont)
self.fontButton = QPushButton("...")
self.fontButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("...")))
self.fontButton.setMaximumWidth(int(2.5*self.mainTheme.getTextWidth("...")))
self.fontButton.clicked.connect(self._selectFont)
self.mainForm.addRow(
self.tr("Font family"),
@@ -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
@@ -326,16 +344,16 @@ class GuiPreferencesGeneral(QWidget):
class GuiPreferencesProjects(QWidget):
def __init__(self, theParent):
QWidget.__init__(self, theParent)
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
self.mainForm.setHelpTextStyle(self.theTheme.helpText)
self.mainForm.setHelpTextStyle(self.mainTheme.helpText)
self.setLayout(self.mainForm)
# Automatic Save
@@ -487,16 +505,16 @@ class GuiPreferencesProjects(QWidget):
class GuiPreferencesDocuments(QWidget):
def __init__(self, theParent):
QWidget.__init__(self, theParent)
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
self.mainForm.setHelpTextStyle(self.theTheme.helpText)
self.mainForm.setHelpTextStyle(self.mainTheme.helpText)
self.setLayout(self.mainForm)
# Text Style
@@ -509,7 +527,7 @@ class GuiPreferencesDocuments(QWidget):
self.textFont.setFixedWidth(self.mainConf.pxInt(162))
self.textFont.setText(self.mainConf.textFont)
self.fontButton = QPushButton("...")
self.fontButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("...")))
self.fontButton.setMaximumWidth(int(2.5*self.mainTheme.getTextWidth("...")))
self.fontButton.clicked.connect(self._selectFont)
self.mainForm.addRow(
self.tr("Font family"),
@@ -648,16 +666,16 @@ class GuiPreferencesDocuments(QWidget):
class GuiPreferencesEditor(QWidget):
def __init__(self, theParent):
QWidget.__init__(self, theParent)
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
self.mainForm.setHelpTextStyle(self.theTheme.helpText)
self.mainForm.setHelpTextStyle(self.mainTheme.helpText)
self.setLayout(self.mainForm)
mW = self.mainConf.pxInt(250)
@@ -670,7 +688,7 @@ class GuiPreferencesEditor(QWidget):
self.spellLanguage = QComboBox(self)
self.spellLanguage.setMaximumWidth(mW)
langAvail = self.theParent.docEditor.spEnchant.listDictionaries()
langAvail = self.mainGui.docEditor.spEnchant.listDictionaries()
if self.mainConf.hasEnchant:
if langAvail:
for spTag, spProv in langAvail:
@@ -822,37 +840,18 @@ class GuiPreferencesEditor(QWidget):
class GuiPreferencesSyntax(QWidget):
def __init__(self, theParent):
QWidget.__init__(self, theParent)
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
self.mainForm.setHelpTextStyle(self.theTheme.helpText)
self.mainForm.setHelpTextStyle(self.mainTheme.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()
@@ -947,16 +943,16 @@ class GuiPreferencesSyntax(QWidget):
class GuiPreferencesAutomation(QWidget):
def __init__(self, theParent):
QWidget.__init__(self, theParent)
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
self.mainForm.setHelpTextStyle(self.theTheme.helpText)
self.mainForm.setHelpTextStyle(self.mainTheme.helpText)
self.setLayout(self.mainForm)
# Automatic Features
@@ -1104,16 +1100,16 @@ class GuiPreferencesAutomation(QWidget):
class GuiPreferencesQuotes(QWidget):
def __init__(self, theParent):
QWidget.__init__(self, theParent)
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
# The Form
self.mainForm = QConfigLayout()
self.mainForm.setHelpTextStyle(self.theTheme.helpText)
self.mainForm.setHelpTextStyle(self.mainTheme.helpText)
self.setLayout(self.mainForm)
# Quotation Style
@@ -1121,7 +1117,7 @@ class GuiPreferencesQuotes(QWidget):
self.mainForm.addGroupLabel(self.tr("Quotation Style"))
qWidth = self.mainConf.pxInt(40)
bWidth = int(2.5*self.theTheme.getTextWidth("..."))
bWidth = int(2.5*self.mainTheme.getTextWidth("..."))
self.quoteSym = {}
# Single Quote Style
+45 -45
View File
@@ -43,31 +43,31 @@ logger = logging.getLogger(__name__)
class GuiProjectDetails(PagedDialog):
def __init__(self, theParent):
PagedDialog.__init__(self, theParent)
def __init__(self, mainGui):
PagedDialog.__init__(self, mainGui)
logger.debug("Initialising GuiProjectDetails ...")
self.setObjectName("GuiProjectDetails")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.mainGui = mainGui
self.theProject = mainGui.theProject
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)
self.tabContents = GuiProjectDetailsContents(self.theParent, self.theProject)
self.tabMain = GuiProjectDetailsMain(self.mainGui, self.theProject)
self.tabContents = GuiProjectDetailsContents(self.mainGui, self.theProject)
self.addTab(self.tabMain, self.tr("Overview"))
self.addTab(self.tabContents, self.tr("Contents"))
@@ -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
@@ -138,17 +139,16 @@ class GuiProjectDetails(PagedDialog):
class GuiProjectDetailsMain(QWidget):
def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent)
def __init__(self, mainGui, theProject):
QWidget.__init__(self, mainGui)
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theProject
self.theTheme = theParent.theTheme
self.theIndex = theParent.theIndex
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
fPx = self.theTheme.fontPixelSize
fPt = self.theTheme.fontPointSize
fPx = self.mainTheme.fontPixelSize
fPt = self.mainTheme.fontPointSize
vPx = self.mainConf.pxInt(4)
hPx = self.mainConf.pxInt(12)
@@ -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}")
@@ -270,22 +271,21 @@ class GuiProjectDetailsContents(QWidget):
C_PAGE = 3
C_PROG = 4
def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent)
def __init__(self, mainGui, theProject):
QWidget.__init__(self, mainGui)
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theProject
self.theTheme = theParent.theTheme
self.theIndex = theParent.theIndex
self.optState = theProject.optState
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
# Internal
self._theToC = []
iPx = self.theTheme.baseIconSize
iPx = self.mainTheme.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
@@ -469,7 +469,7 @@ class GuiProjectDetailsContents(QWidget):
if tTitle.strip() == "":
tTitle = self.tr("Untitled")
newItem.setIcon(self.C_TITLE, self.theTheme.getIcon("doc_h%d" % tLevel))
newItem.setIcon(self.C_TITLE, self.mainTheme.getIcon("doc_h%d" % tLevel))
newItem.setText(self.C_TITLE, tTitle)
newItem.setText(self.C_WORDS, f"{wCount:n}")
newItem.setText(self.C_PAGES, f"{pCount:n}")
+10 -10
View File
@@ -53,21 +53,21 @@ class GuiProjectLoad(QDialog):
C_COUNT = 1
C_TIME = 2
def __init__(self, theParent):
QDialog.__init__(self, theParent)
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
logger.debug("Initialising GuiProjectLoad ...")
self.setObjectName("GuiProjectLoad")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.openState = self.NONE_STATE
self.openPath = None
sPx = self.mainConf.pxInt(16)
nPx = self.mainConf.pxInt(96)
iPx = self.theTheme.baseIconSize
iPx = self.mainTheme.baseIconSize
self.outerBox = QVBoxLayout()
self.innerBox = QHBoxLayout()
@@ -80,7 +80,7 @@ class GuiProjectLoad(QDialog):
self.setModal(True)
self.nwIcon = QLabel()
self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx)))
self.nwIcon.setPixmap(self.mainGui.mainTheme.getPixmap("novelwriter", (nPx, nPx)))
self.innerBox.addWidget(self.nwIcon, 0, Qt.AlignTop)
self.projectForm = QGridLayout()
@@ -110,7 +110,7 @@ class GuiProjectLoad(QDialog):
self.selPath.setReadOnly(True)
self.browseButton = QPushButton("...")
self.browseButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("...")))
self.browseButton.setMaximumWidth(int(2.5*self.mainTheme.getTextWidth("...")))
self.browseButton.clicked.connect(self._doBrowse)
self.projectForm.addWidget(self.lblRecent, 0, 0, 1, 3)
@@ -225,7 +225,7 @@ class GuiProjectLoad(QDialog):
selList = self.listBox.selectedItems()
if selList:
projName = selList[0].text(self.C_NAME)
msgYes = self.theParent.askQuestion(
msgYes = self.mainGui.askQuestion(
self.tr("Remove Entry"),
self.tr(
"Remove '{0}' from the recent projects list? "
@@ -280,7 +280,7 @@ class GuiProjectLoad(QDialog):
sortList = sorted(dataList, key=lambda x: x[1], reverse=True)
for theTitle, theTime, theWords, projPath in sortList:
newItem = QTreeWidgetItem([""]*4)
newItem.setIcon(self.C_NAME, self.theParent.theTheme.getIcon("proj_nwx"))
newItem.setIcon(self.C_NAME, self.mainGui.mainTheme.getIcon("proj_nwx"))
newItem.setText(self.C_NAME, theTitle)
newItem.setData(self.C_NAME, Qt.UserRole, projPath)
newItem.setText(self.C_COUNT, formatInt(theWords))
@@ -288,7 +288,7 @@ class GuiProjectLoad(QDialog):
newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter)
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter)
newItem.setTextAlignment(self.C_TIME, Qt.AlignRight | Qt.AlignVCenter)
newItem.setFont(self.C_TIME, self.theTheme.guiFontFixed)
newItem.setFont(self.C_TIME, self.mainTheme.guiFontFixed)
self.listBox.addTopLevelItem(newItem)
if self.listBox.topLevelItemCount() > 0:
+142 -107
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__)
@@ -42,34 +43,34 @@ logger = logging.getLogger(__name__)
class GuiProjectSettings(PagedDialog):
def __init__(self, theParent):
PagedDialog.__init__(self, theParent)
def __init__(self, mainGui):
PagedDialog.__init__(self, mainGui)
logger.debug("Initialising GuiProjectSettings ...")
self.setObjectName("GuiProjectSettings")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.mainGui = mainGui
self.theProject = mainGui.theProject
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)
self.tabStatus = GuiProjectEditStatus(self.theParent, self.theProject, True)
self.tabImport = GuiProjectEditStatus(self.theParent, self.theProject, False)
self.tabReplace = GuiProjectEditReplace(self.theParent, self.theProject)
self.tabMain = GuiProjectEditMain(self.mainGui, self.theProject)
self.tabStatus = GuiProjectEditStatus(self.mainGui, self.theProject, True)
self.tabImport = GuiProjectEditStatus(self.mainGui, self.theProject, False)
self.tabReplace = GuiProjectEditReplace(self.mainGui, self.theProject)
self.addTab(self.tabMain, self.tr("Settings"))
self.addTab(self.tabStatus, self.tr("Status"))
@@ -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,19 +107,21 @@ 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()
self.mainGui.rebuildTrees()
if self.tabReplace.arChanged:
newList = self.tabReplace.getNewList()
@@ -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
@@ -159,29 +166,29 @@ class GuiProjectSettings(PagedDialog):
class GuiProjectEditMain(QWidget):
def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent)
def __init__(self, mainGui, theProject):
QWidget.__init__(self, mainGui)
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.mainGui = mainGui
self.theProject = theProject
# The Form
self.mainForm = QConfigLayout()
self.mainForm.setHelpTextStyle(self.theParent.theTheme.helpText)
self.mainForm.setHelpTextStyle(self.mainGui.mainTheme.helpText)
self.setLayout(self.mainForm)
self.mainForm.addGroupLabel(self.tr("Project Settings"))
xW = self.mainConf.pxInt(250)
xH = round(4.8*self.theParent.theTheme.fontPixelSize)
xH = round(4.8*self.mainGui.mainTheme.fontPixelSize)
self.editName = QLineEdit()
self.editName.setMaxLength(200)
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.")
)
@@ -209,7 +216,7 @@ class GuiProjectEditMain(QWidget):
self.spellLang = QComboBox(self)
self.spellLang.setMaximumWidth(xW)
self.spellLang.addItem(self.tr("Default"), "None")
langAvail = self.theParent.docEditor.spEnchant.listDictionaries()
langAvail = self.mainGui.docEditor.spEnchant.listDictionaries()
for spTag, spProv in langAvail:
qLocal = QLocale(spTag)
spLang = qLocal.nativeLanguageName().title()
@@ -245,14 +252,17 @@ class GuiProjectEditStatus(QWidget):
COL_LABEL = 0
COL_USAGE = 1
def __init__(self, theParent, theProject, isStatus):
QWidget.__init__(self, theParent)
KEY_ROLE = Qt.UserRole
COL_ROLE = Qt.UserRole + 1
NUM_ROLE = Qt.UserRole + 2
def __init__(self, mainGui, theProject, isStatus):
QWidget.__init__(self, mainGui)
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.mainGui = mainGui
self.theProject = theProject
self.optState = theProject.optState
self.theTheme = theParent.theTheme
self.mainTheme = mainGui.mainTheme
if isStatus:
self.theStatus = self.theProject.statusItems
@@ -264,15 +274,14 @@ 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
self.iPx = self.mainTheme.baseIconSize
# The List
# ========
@@ -285,18 +294,24 @@ 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
# =============
self.addButton = QPushButton(self.theTheme.getIcon("add"), "")
self.addButton = QPushButton(self.mainTheme.getIcon("add"), "")
self.addButton.clicked.connect(self._newItem)
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
self.delButton = QPushButton(self.mainTheme.getIcon("remove"), "")
self.delButton.clicked.connect(self._delItem)
self.upButton = QPushButton(self.mainTheme.getIcon("up"), "")
self.upButton.clicked.connect(lambda: self._moveItem(-1))
self.dnButton = QPushButton(self.mainTheme.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:
self.theParent.makeAlert(self.tr(
if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0:
self.mainGui.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.
"""
@@ -491,18 +527,17 @@ class GuiProjectEditReplace(QWidget):
COL_KEY = 0
COL_REPL = 1
def __init__(self, theParent, theProject):
QWidget.__init__(self, theParent)
def __init__(self, mainGui, theProject):
QWidget.__init__(self, mainGui)
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
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")
@@ -528,10 +563,10 @@ class GuiProjectEditReplace(QWidget):
# List Controls
# =============
self.addButton = QPushButton(self.theTheme.getIcon("add"), "")
self.addButton = QPushButton(self.mainTheme.getIcon("add"), "")
self.addButton.clicked.connect(self._addEntry)
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
self.delButton = QPushButton(self.mainTheme.getIcon("remove"), "")
self.delButton.clicked.connect(self._delEntry)
# Edit Form
+2 -2
View File
@@ -42,8 +42,8 @@ class GuiQuoteSelect(QDialog):
selectedQuote = ""
def __init__(self, theParent=None, currentQuote='"'):
QDialog.__init__(self, parent=theParent)
def __init__(self, parent=None, currentQuote='"'):
QDialog.__init__(self, parent=parent)
self.mainConf = novelwriter.CONFIG
+5 -5
View File
@@ -43,14 +43,14 @@ logger = logging.getLogger(__name__)
class GuiUpdates(QDialog):
def __init__(self, theParent):
QDialog.__init__(self, theParent)
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
logger.debug("Initialising GuiUpdates ...")
self.setObjectName("GuiUpdates")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.setWindowTitle(self.tr("Check for Updates"))
@@ -61,7 +61,7 @@ class GuiUpdates(QDialog):
# Left Box
self.nwIcon = QLabel()
self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx)))
self.nwIcon.setPixmap(self.mainGui.mainTheme.getPixmap("novelwriter", (nPx, nPx)))
self.leftBox = QVBoxLayout()
self.leftBox.addWidget(self.nwIcon)
+15 -14
View File
@@ -42,29 +42,29 @@ logger = logging.getLogger(__name__)
class GuiWordList(QDialog):
def __init__(self, theParent):
QDialog.__init__(self, theParent)
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
logger.debug("Initialising GuiWordList ...")
self.setObjectName("GuiWordList")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject
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
@@ -78,10 +78,10 @@ class GuiWordList(QDialog):
self.newEntry = QLineEdit()
self.addButton = QPushButton(self.theTheme.getIcon("add"), "")
self.addButton = QPushButton(self.mainTheme.getIcon("add"), "")
self.addButton.clicked.connect(self._doAdd)
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
self.delButton = QPushButton(self.mainTheme.getIcon("remove"), "")
self.delButton.clicked.connect(self._doDelete)
self.editBox = QHBoxLayout()
@@ -121,13 +121,13 @@ class GuiWordList(QDialog):
"""
newWord = self.newEntry.text().strip()
if newWord == "":
self.theParent.makeAlert(self.tr(
self.mainGui.makeAlert(self.tr(
"Cannot add a blank word."
), nwAlert.ERROR)
return False
if self.listBox.findItems(newWord, Qt.MatchExactly):
self.theParent.makeAlert(self.tr(
self.mainGui.makeAlert(self.tr(
"The word '{0}' is already in the word list."
).format(newWord), nwAlert.ERROR)
return False
@@ -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 +++",
+8 -8
View File
@@ -23,12 +23,12 @@ from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.gui.docviewer import GuiDocViewer, GuiDocViewDetails
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.noveltree import GuiNovelView
from novelwriter.gui.outline import GuiOutlineView
from novelwriter.gui.projtree import GuiProjectView
from novelwriter.gui.statusbar import GuiMainStatus
from novelwriter.gui.theme import GuiTheme
from novelwriter.gui.viewsbar import GuiViewsBar
__all__ = [
"GuiDocEditor",
@@ -37,9 +37,9 @@ __all__ = [
"GuiItemDetails",
"GuiMainMenu",
"GuiMainStatus",
"GuiNovelTree",
"GuiOutline",
"GuiOutlineDetails",
"GuiProjectTree",
"GuiNovelView",
"GuiOutlineView",
"GuiProjectView",
"GuiTheme",
"GuiViewsBar",
]
+9 -9
View File
@@ -376,8 +376,8 @@ class QSwitch(QAbstractButton):
class PagedDialog(QDialog):
def __init__(self, theParent=None):
QDialog.__init__(self, parent=theParent)
def __init__(self, parent=None):
QDialog.__init__(self, parent=parent)
self._tabBar = VerticalTabBar(self)
self._tabBar.setExpanding(False)
@@ -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):
@@ -426,20 +426,20 @@ class PagedDialog(QDialog):
class VerticalTabBar(QTabBar):
def __init__(self, theParent=None):
QTabBar.__init__(self, parent=theParent)
def __init__(self, parent=None):
QTabBar.__init__(self, parent=parent)
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.
"""
+123 -113
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, nwItemClass
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,18 +70,18 @@ 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)
def __init__(self, mainGui):
QTextEdit.__init__(self, mainGui)
logger.debug("Initialising GuiDocEditor ...")
# Class Variables
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theIndex = theParent.theIndex
self.theProject = theParent.theProject
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject
self._nwDocument = None
self._nwItem = None
@@ -124,7 +125,7 @@ class GuiDocEditor(QTextEdit):
# Syntax
self.spEnchant = NWSpellEnchant()
self.highLight = GuiDocHighlighter(qDoc, self.theParent, self.spEnchant)
self.highLight = GuiDocHighlighter(qDoc, self.mainGui, self.spEnchant)
# Context Menu
self.setContextMenuPolicy(Qt.CustomContextMenu)
@@ -132,8 +133,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(
@@ -238,10 +240,10 @@ class GuiDocEditor(QTextEdit):
if self.mainConf.textFont is None:
# If none is defined, set a default font
theFont = QFont()
if self.mainConf.osWindows and "Arial" in self.theTheme.guiFontDB.families():
if self.mainConf.osWindows and "Arial" in self.mainTheme.guiFontDB.families():
theFont.setFamily("Arial")
theFont.setPointSize(12)
elif self.mainConf.osDarwin and "Courier" in self.theTheme.guiFontDB.families():
elif self.mainConf.osDarwin and "Courier" in self.mainTheme.guiFontDB.families():
theFont.setFamily("Courier")
theFont.setPointSize(12)
else:
@@ -256,19 +258,23 @@ class GuiDocEditor(QTextEdit):
# Set the widget colours to match syntax theme
mainPalette = self.palette()
mainPalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack))
mainPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack))
mainPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText))
mainPalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack))
mainPalette.setColor(QPalette.Base, QColor(*self.mainTheme.colBack))
mainPalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText))
self.setPalette(mainPalette)
docPalette = self.viewport().palette()
docPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack))
docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText))
docPalette.setColor(QPalette.Base, QColor(*self.mainTheme.colBack))
docPalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText))
self.viewport().setPalette(docPalette)
self.docHeader.matchColours()
self.docFooter.matchColours()
<<<<<<< HEAD
=======
# Set default text margins
>>>>>>> main
# Due to cursor visibility, a part of the margin must be
# allocated to the document itself. See issue #1112.
cW = self.cursorWidth()
@@ -342,7 +348,7 @@ class GuiDocEditor(QTextEdit):
docSize = len(theDoc)
if docSize > nwConst.MAX_DOCSIZE:
self.theParent.makeAlert(self.tr(
self.mainGui.makeAlert(self.tr(
"The document you are trying to open is too big. "
"The document size is {0} MB. "
"The maximum size allowed is {1} MB."
@@ -403,7 +409,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()
@@ -418,7 +424,7 @@ class GuiDocEditor(QTextEdit):
# Update the status bar
if self._nwItem is not None:
self.theParent.setStatus(
self.mainGui.setStatus(
self.tr("Opened Document: {0}").format(self._nwItem.itemName)
)
@@ -443,7 +449,7 @@ class GuiDocEditor(QTextEdit):
"""
docSize = len(theText)
if docSize > nwConst.MAX_DOCSIZE:
self.theParent.makeAlert(self.tr(
self.mainGui.makeAlert(self.tr(
"The text you are trying to add is too big. "
"The text size is {0} MB. "
"The maximum size allowed is {1} MB."
@@ -489,7 +495,7 @@ class GuiDocEditor(QTextEdit):
if not self._nwDocument.writeDocument(docText):
saveOk = False
if self._nwDocument._currHash != self._nwDocument._prevHash:
msgYes = self.theParent.askQuestion(
msgYes = self.mainGui.askQuestion(
self.tr("File Changed on Disk"),
self.tr(
"This document has been changed outside of novelWriter "
@@ -500,7 +506,7 @@ class GuiDocEditor(QTextEdit):
saveOk = self._nwDocument.writeDocument(docText, forceWrite=True)
if not saveOk:
self.theParent.makeAlert([
self.mainGui.makeAlert([
self.tr("Could not save document."), self._nwDocument.getError()
], nwAlert.ERROR)
@@ -508,22 +514,24 @@ 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)
# ToDo: This should be a signal
if self._updateHeaders(checkLevel=True):
self.theParent.requestNovelTreeRefresh()
self.mainGui.requestNovelTreeRefresh()
else:
self.theParent.novelView.updateWordCounts(tHandle)
self.mainGui.novelView.updateWordCounts(tHandle)
# ToDo: This should be a signal
if oldHeader != newHeader:
self.theParent.treeView.setTreeItemValues(tHandle)
self.theParent.treeMeta.updateViewBox(tHandle)
self.mainGui.projView.setTreeItemValues(tHandle)
self.mainGui.itemDetails.updateViewBox(tHandle)
self.docFooter.updateInfo()
# Update the status bar
self.theParent.setStatus(
self.mainGui.setStatus(
self.tr("Saved Document: {0}").format(self._nwItem.itemName)
)
@@ -544,8 +552,13 @@ class GuiDocEditor(QTextEdit):
sH = hBar.height() if hBar.isVisible() else 0
tM = self._vpMargin
<<<<<<< HEAD
if self.mainConf.textWidth > 0 or self.theParent.isFocusMode:
tW = self.mainConf.getTextWidth(self.theParent.isFocusMode)
=======
if self.mainConf.textWidth > 0 or self.mainGui.isFocusMode:
tW = self.mainConf.getTextWidth(self.mainGui.isFocusMode)
>>>>>>> main
tM = max((wW - sW - tW)//2, self._vpMargin)
tB = self.frameWidth()
@@ -569,16 +582,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
##
@@ -714,7 +717,7 @@ class GuiDocEditor(QTextEdit):
if not self.mainConf.hasEnchant:
if theMode:
self.theParent.makeAlert(self.tr(
self.mainGui.makeAlert(self.tr(
"Spell checking requires the package PyEnchant. "
"It does not appear to be installed."
), nwAlert.INFO)
@@ -724,7 +727,7 @@ class GuiDocEditor(QTextEdit):
theMode = False
self._spellCheck = theMode
self.theParent.mainMenu.setSpellCheck(theMode)
self.mainGui.mainMenu.setSpellCheck(theMode)
self.theProject.setSpellCheck(theMode)
self.highLight.setSpellCheck(theMode)
if not self._bigDoc:
@@ -752,7 +755,7 @@ class GuiDocEditor(QTextEdit):
qApp.restoreOverrideCursor()
afTime = time()
logger.debug("Document highlighted in %.3f ms", 1000*(afTime-bfTime))
self.theParent.statusBar.setStatus(self.tr("Spell check complete"))
self.mainGui.statusBar.setStatus(self.tr("Spell check complete"))
return True
@@ -1068,7 +1071,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)
@@ -1080,7 +1098,7 @@ class GuiDocEditor(QTextEdit):
self._lastFind = None
if self.document().characterCount() > nwConst.MAX_DOCSIZE:
self.theParent.makeAlert(self.tr(
self.mainGui.makeAlert(self.tr(
"The document has grown too big and you cannot add more text to it. "
"The maximum size of a single novelWriter document is {0} MB."
).format(
@@ -1240,7 +1258,7 @@ class GuiDocEditor(QTextEdit):
if time() - self._lastEdit < 5 * self.wcInterval:
logger.verbose("Running word counter")
self.theParent.threadPool.start(self.wCounterDoc)
self.mainGui.threadPool.start(self.wCounterDoc)
return
@@ -1297,7 +1315,7 @@ class GuiDocEditor(QTextEdit):
logger.verbose("Selection word counter is busy")
return
self.theParent.threadPool.start(self.wCounterSel)
self.mainGui.threadPool.start(self.wCounterSel)
return
@@ -1376,7 +1394,7 @@ class GuiDocEditor(QTextEdit):
self.docSearch.setResultCount(0, 0)
self._lastFind = None
if self.docSearch.doNextFile and not goBack:
self.theParent.openNextDocument(
self.mainGui.openNextDocument(
self._docHandle, wrapAround=self.docSearch.doLoop
)
self.beginSearch()
@@ -1396,7 +1414,7 @@ class GuiDocEditor(QTextEdit):
if resIdx > maxIdx:
if self.docSearch.doNextFile and not goBack:
self.theParent.openNextDocument(
self.mainGui.openNextDocument(
self._docHandle, wrapAround=self.docSearch.doLoop
)
self.beginSearch()
@@ -1640,7 +1658,7 @@ class GuiDocEditor(QTextEdit):
"""
theCursor = self.textCursor()
if not theCursor.hasSelection():
self.theParent.makeAlert(self.tr(
self.mainGui.makeAlert(self.tr(
"Please select some text before calling replace quotes."
), nwAlert.ERROR)
return False
@@ -1896,7 +1914,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)
@@ -2003,7 +2021,7 @@ class GuiDocEditor(QTextEdit):
def _allowSpaceBeforeColon(text, char):
"""Special checker function only used by the insert space
feature for French, Spanish, etc, so it doesn't insert a
sapce before colons in meta data lines.
space before colons in meta data lines. See issue #1090.
"""
if char == ":" and len(text) > 1:
if text[0] == "@":
@@ -2020,7 +2038,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]
@@ -2198,9 +2216,9 @@ class GuiDocEditSearch(QFrame):
self.mainConf = novelwriter.CONFIG
self.docEditor = docEditor
self.theParent = docEditor.theParent
self.mainGui = docEditor.mainGui
self.theProject = docEditor.theProject
self.theTheme = docEditor.theTheme
self.mainTheme = docEditor.mainTheme
self.repVisible = False
self.isCaseSense = self.mainConf.searchCase
@@ -2211,9 +2229,9 @@ class GuiDocEditSearch(QFrame):
self.doMatchCap = self.mainConf.searchMatchCap
mPx = self.mainConf.pxInt(6)
tPx = int(0.8*self.theTheme.fontPixelSize)
self.boxFont = self.theTheme.guiFont
self.boxFont.setPointSizeF(0.9*self.theTheme.fontPointSize)
tPx = int(0.8*self.mainTheme.fontPixelSize)
self.boxFont = self.mainTheme.guiFont
self.boxFont.setPointSizeF(0.9*self.mainTheme.fontPointSize)
self.setContentsMargins(0, 0, 0, 0)
self.setAutoFillBackground(True)
@@ -2247,38 +2265,38 @@ class GuiDocEditSearch(QFrame):
self.resultLabel = QLabel("?/?")
self.resultLabel.setFont(self.boxFont)
self.resultLabel.setMinimumWidth(self.theTheme.getTextWidth("?/?", self.boxFont))
self.resultLabel.setMinimumWidth(self.mainTheme.getTextWidth("?/?", self.boxFont))
self.toggleCase = QAction(self.tr("Case Sensitive"), self)
self.toggleCase.setIcon(self.theTheme.getIcon("search_case"))
self.toggleCase.setIcon(self.mainTheme.getIcon("search_case"))
self.toggleCase.setCheckable(True)
self.toggleCase.setChecked(self.isCaseSense)
self.toggleCase.toggled.connect(self._doToggleCase)
self.searchOpt.addAction(self.toggleCase)
self.toggleWord = QAction(self.tr("Whole Words Only"), self)
self.toggleWord.setIcon(self.theTheme.getIcon("search_word"))
self.toggleWord.setIcon(self.mainTheme.getIcon("search_word"))
self.toggleWord.setCheckable(True)
self.toggleWord.setChecked(self.isWholeWord)
self.toggleWord.toggled.connect(self._doToggleWord)
self.searchOpt.addAction(self.toggleWord)
self.toggleRegEx = QAction(self.tr("RegEx Mode"), self)
self.toggleRegEx.setIcon(self.theTheme.getIcon("search_regex"))
self.toggleRegEx.setIcon(self.mainTheme.getIcon("search_regex"))
self.toggleRegEx.setCheckable(True)
self.toggleRegEx.setChecked(self.isRegEx)
self.toggleRegEx.toggled.connect(self._doToggleRegEx)
self.searchOpt.addAction(self.toggleRegEx)
self.toggleLoop = QAction(self.tr("Loop Search"), self)
self.toggleLoop.setIcon(self.theTheme.getIcon("search_loop"))
self.toggleLoop.setIcon(self.mainTheme.getIcon("search_loop"))
self.toggleLoop.setCheckable(True)
self.toggleLoop.setChecked(self.doLoop)
self.toggleLoop.toggled.connect(self._doToggleLoop)
self.searchOpt.addAction(self.toggleLoop)
self.toggleProject = QAction(self.tr("Search Next File"), self)
self.toggleProject.setIcon(self.theTheme.getIcon("search_project"))
self.toggleProject.setIcon(self.mainTheme.getIcon("search_project"))
self.toggleProject.setCheckable(True)
self.toggleProject.setChecked(self.doNextFile)
self.toggleProject.toggled.connect(self._doToggleProject)
@@ -2287,7 +2305,7 @@ class GuiDocEditSearch(QFrame):
self.searchOpt.addSeparator()
self.toggleMatchCap = QAction(self.tr("Preserve Case"), self)
self.toggleMatchCap.setIcon(self.theTheme.getIcon("search_preserve"))
self.toggleMatchCap.setIcon(self.mainTheme.getIcon("search_preserve"))
self.toggleMatchCap.setCheckable(True)
self.toggleMatchCap.setChecked(self.doMatchCap)
self.toggleMatchCap.toggled.connect(self._doToggleMatchCap)
@@ -2296,7 +2314,7 @@ class GuiDocEditSearch(QFrame):
self.searchOpt.addSeparator()
self.cancelSearch = QAction(self.tr("Close Search"), self)
self.cancelSearch.setIcon(self.theTheme.getIcon("search_cancel"))
self.cancelSearch.setIcon(self.mainTheme.getIcon("search_cancel"))
self.cancelSearch.triggered.connect(self._doClose)
self.searchOpt.addAction(self.cancelSearch)
@@ -2311,12 +2329,12 @@ class GuiDocEditSearch(QFrame):
self.showReplace.setStyleSheet("QToolButton {border: none; background: transparent;}")
self.showReplace.toggled.connect(self._doToggleReplace)
self.searchButton = QPushButton(self.theTheme.getIcon("search"), "")
self.searchButton = QPushButton(self.mainTheme.getIcon("search"), "")
self.searchButton.setFixedSize(QSize(bPx, bPx))
self.searchButton.setToolTip(self.tr("Find in current document"))
self.searchButton.clicked.connect(self._doSearch)
self.replaceButton = QPushButton(self.theTheme.getIcon("search_replace"), "")
self.replaceButton = QPushButton(self.mainTheme.getIcon("search_replace"), "")
self.replaceButton.setFixedSize(QSize(bPx, bPx))
self.replaceButton.setToolTip(self.tr("Find and replace in current document"))
self.replaceButton.clicked.connect(self._doReplace)
@@ -2435,7 +2453,7 @@ class GuiDocEditSearch(QFrame):
"""
currRes = "?" if currRes is None else currRes
resCount = "?" if resCount is None else "1000+" if resCount > 1000 else resCount
minWidth = self.theTheme.getTextWidth(f"{resCount}//{resCount}", self.boxFont)
minWidth = self.mainTheme.getTextWidth(f"{resCount}//{resCount}", self.boxFont)
self.resultLabel.setText(f"{currRes}/{resCount}")
self.resultLabel.setMinimumWidth(minWidth)
self.adjustSize()
@@ -2586,13 +2604,13 @@ class GuiDocEditHeader(QWidget):
self.mainConf = novelwriter.CONFIG
self.docEditor = docEditor
self.theParent = docEditor.theParent
self.mainGui = docEditor.mainGui
self.theProject = docEditor.theProject
self.theTheme = docEditor.theTheme
self.mainTheme = docEditor.mainTheme
self._docHandle = None
fPx = int(0.9*self.theTheme.fontPixelSize)
fPx = int(0.9*self.mainTheme.fontPixelSize)
hSp = self.mainConf.pxInt(6)
# Main Widget Settings
@@ -2609,17 +2627,17 @@ class GuiDocEditHeader(QWidget):
self.theTitle.setFixedHeight(fPx)
lblFont = self.theTitle.font()
lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize)
lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize)
self.theTitle.setFont(lblFont)
buttonStyle = (
"QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
).format(*self.theTheme.colText)
).format(*self.mainTheme.colText)
# Buttons
self.editButton = QToolButton(self)
self.editButton.setIcon(self.theTheme.getIcon("edit"))
self.editButton.setIcon(self.mainTheme.getIcon("edit"))
self.editButton.setContentsMargins(0, 0, 0, 0)
self.editButton.setIconSize(QSize(fPx, fPx))
self.editButton.setFixedSize(fPx, fPx)
@@ -2630,7 +2648,7 @@ class GuiDocEditHeader(QWidget):
self.editButton.clicked.connect(self._editDocument)
self.searchButton = QToolButton(self)
self.searchButton.setIcon(self.theTheme.getIcon("search"))
self.searchButton.setIcon(self.mainTheme.getIcon("search"))
self.searchButton.setContentsMargins(0, 0, 0, 0)
self.searchButton.setIconSize(QSize(fPx, fPx))
self.searchButton.setFixedSize(fPx, fPx)
@@ -2641,7 +2659,7 @@ class GuiDocEditHeader(QWidget):
self.searchButton.clicked.connect(self._searchDocument)
self.minmaxButton = QToolButton(self)
self.minmaxButton.setIcon(self.theTheme.getIcon("maximise"))
self.minmaxButton.setIcon(self.mainTheme.getIcon("maximise"))
self.minmaxButton.setContentsMargins(0, 0, 0, 0)
self.minmaxButton.setIconSize(QSize(fPx, fPx))
self.minmaxButton.setFixedSize(fPx, fPx)
@@ -2652,7 +2670,7 @@ class GuiDocEditHeader(QWidget):
self.minmaxButton.clicked.connect(self._minmaxDocument)
self.closeButton = QToolButton(self)
self.closeButton.setIcon(self.theTheme.getIcon("close"))
self.closeButton.setIcon(self.mainTheme.getIcon("close"))
self.closeButton.setContentsMargins(0, 0, 0, 0)
self.closeButton.setIconSize(QSize(fPx, fPx))
self.closeButton.setFixedSize(fPx, fPx)
@@ -2695,9 +2713,9 @@ class GuiDocEditHeader(QWidget):
theme rather than the main GUI.
"""
thePalette = QPalette()
thePalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack))
thePalette.setColor(QPalette.WindowText, QColor(*self.theTheme.colText))
thePalette.setColor(QPalette.Text, QColor(*self.theTheme.colText))
thePalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack))
thePalette.setColor(QPalette.WindowText, QColor(*self.mainTheme.colText))
thePalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText))
self.setPalette(thePalette)
self.theTitle.setPalette(thePalette)
@@ -2719,15 +2737,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)
@@ -2744,10 +2762,10 @@ class GuiDocEditHeader(QWidget):
This function is called by the GuiMain class via the
toggleFocusMode function and should not be activated directly.
"""
if self.theParent.isFocusMode:
self.minmaxButton.setIcon(self.theTheme.getIcon("minimise"))
if self.mainGui.isFocusMode:
self.minmaxButton.setIcon(self.mainTheme.getIcon("minimise"))
else:
self.minmaxButton.setIcon(self.theTheme.getIcon("maximise"))
self.minmaxButton.setIcon(self.mainTheme.getIcon("maximise"))
return
##
@@ -2757,7 +2775,7 @@ class GuiDocEditHeader(QWidget):
def _editDocument(self):
"""Open the edit item dialog from the main GUI.
"""
self.theParent.editItem(self._docHandle)
self.mainGui.editItemLabel(self._docHandle)
return
def _searchDocument(self):
@@ -2769,7 +2787,7 @@ class GuiDocEditHeader(QWidget):
def _closeDocument(self):
"""Trigger the close editor on the main window.
"""
self.theParent.closeDocEditor()
self.mainGui.closeDocEditor()
self.editButton.setVisible(False)
self.searchButton.setVisible(False)
self.closeButton.setVisible(False)
@@ -2779,7 +2797,7 @@ class GuiDocEditHeader(QWidget):
def _minmaxDocument(self):
"""Switch on or off Focus Mode.
"""
self.theParent.toggleFocusMode()
self.mainGui.toggleFocusMode()
return
##
@@ -2790,7 +2808,7 @@ class GuiDocEditHeader(QWidget):
"""Capture a click on the title and ensure that the item is
selected in the project tree.
"""
self.theParent.treeView.setSelectedHandle(self._docHandle, doScroll=True)
self.mainGui.projView.setSelectedHandle(self._docHandle, doScroll=True)
return
# END Class GuiDocEditHeader
@@ -2810,23 +2828,22 @@ class GuiDocEditFooter(QWidget):
self.mainConf = novelwriter.CONFIG
self.docEditor = docEditor
self.theParent = docEditor.theParent
self.mainGui = docEditor.mainGui
self.theProject = docEditor.theProject
self.theTheme = docEditor.theTheme
self.optState = docEditor.theProject.optState
self.mainTheme = docEditor.mainTheme
self._theItem = None
self._docHandle = None
self._docSelection = False
self.sPx = int(round(0.9*self.theTheme.baseIconSize))
fPx = int(0.9*self.theTheme.fontPixelSize)
self.sPx = int(round(0.9*self.mainTheme.baseIconSize))
fPx = int(0.9*self.mainTheme.fontPixelSize)
bSp = self.mainConf.pxInt(4)
hSp = self.mainConf.pxInt(6)
lblFont = self.font()
lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize)
lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize)
# Main Widget Settings
self.setContentsMargins(0, 0, 0, 0)
@@ -2849,7 +2866,7 @@ class GuiDocEditFooter(QWidget):
# Lines
self.linesIcon = QLabel("")
self.linesIcon.setPixmap(self.theTheme.getPixmap("status_lines", (self.sPx, self.sPx)))
self.linesIcon.setPixmap(self.mainTheme.getPixmap("status_lines", (self.sPx, self.sPx)))
self.linesIcon.setContentsMargins(0, 0, 0, 0)
self.linesIcon.setFixedHeight(self.sPx)
self.linesIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop)
@@ -2865,7 +2882,7 @@ class GuiDocEditFooter(QWidget):
# Words
self.wordsIcon = QLabel("")
self.wordsIcon.setPixmap(self.theTheme.getPixmap("status_stats", (self.sPx, self.sPx)))
self.wordsIcon.setPixmap(self.mainTheme.getPixmap("status_stats", (self.sPx, self.sPx)))
self.wordsIcon.setContentsMargins(0, 0, 0, 0)
self.wordsIcon.setFixedHeight(self.sPx)
self.wordsIcon.setAlignment(Qt.AlignLeft | Qt.AlignTop)
@@ -2917,9 +2934,9 @@ class GuiDocEditFooter(QWidget):
theme rather than the main GUI.
"""
thePalette = QPalette()
thePalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack))
thePalette.setColor(QPalette.WindowText, QColor(*self.theTheme.colText))
thePalette.setColor(QPalette.Text, QColor(*self.theTheme.colText))
thePalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack))
thePalette.setColor(QPalette.WindowText, QColor(*self.mainTheme.colText))
thePalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText))
self.setPalette(thePalette)
self.statusText.setPalette(thePalette)
@@ -2936,7 +2953,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()
@@ -2958,17 +2975,10 @@ class GuiDocEditFooter(QWidget):
sIcon = QPixmap()
sText = ""
else:
iStatus = self._theItem.itemStatus
if self._theItem.itemClass == nwItemClass.NOVEL:
iStatus = self.theProject.statusItems.checkEntry(iStatus)
theIcon = self.theParent.statusIcons[iStatus]
else:
iStatus = self.theProject.importItems.checkEntry(iStatus)
theIcon = self.theParent.importIcons[iStatus]
theStatus, theIcon = self._theItem.getImportStatus()
sIcon = theIcon.pixmap(self.sPx, self.sPx)
hLevel = self.theParent.theIndex.getHandleHeaderLevel(self._docHandle)
sText = f"{self._theItem.itemStatus} / {self._theItem.describeMe(hLevel)}"
hLevel = self.theProject.index.getHandleHeaderLevel(self._docHandle)
sText = f"{theStatus} / {self._theItem.describeMe(hLevel)}"
self.statusIcon.setPixmap(sIcon)
self.statusText.setText(sText)
+22 -21
View File
@@ -46,16 +46,16 @@ class GuiDocHighlighter(QSyntaxHighlighter):
BLOCK_META = 2
BLOCK_TITLE = 4
def __init__(self, theDoc, theParent, spEnchant):
def __init__(self, theDoc, mainGui, spEnchant):
QSyntaxHighlighter.__init__(self, theDoc)
logger.debug("Initialising GuiDocHighlighter ...")
self.mainConf = novelwriter.CONFIG
self.theDoc = theDoc
self.spEnchant = spEnchant
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theIndex = theParent.theIndex
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject
self.theHandle = None
self.spellCheck = False
self.spellRx = None
@@ -87,24 +87,24 @@ class GuiDocHighlighter(QSyntaxHighlighter):
"""
logger.debug("Setting up highlighting rules")
self.colHead = QColor(*self.theTheme.colHead)
self.colHeadH = QColor(*self.theTheme.colHeadH)
self.colDialN = QColor(*self.theTheme.colDialN)
self.colDialD = QColor(*self.theTheme.colDialD)
self.colDialS = QColor(*self.theTheme.colDialS)
self.colHidden = QColor(*self.theTheme.colHidden)
self.colKey = QColor(*self.theTheme.colKey)
self.colVal = QColor(*self.theTheme.colVal)
self.colSpell = QColor(*self.theTheme.colSpell)
self.colError = QColor(*self.theTheme.colError)
self.colRepTag = QColor(*self.theTheme.colRepTag)
self.colMod = QColor(*self.theTheme.colMod)
self.colBreak = QColor(*self.theTheme.colEmph)
self.colHead = QColor(*self.mainTheme.colHead)
self.colHeadH = QColor(*self.mainTheme.colHeadH)
self.colDialN = QColor(*self.mainTheme.colDialN)
self.colDialD = QColor(*self.mainTheme.colDialD)
self.colDialS = QColor(*self.mainTheme.colDialS)
self.colHidden = QColor(*self.mainTheme.colHidden)
self.colKey = QColor(*self.mainTheme.colKey)
self.colVal = QColor(*self.mainTheme.colVal)
self.colSpell = QColor(*self.mainTheme.colSpell)
self.colError = QColor(*self.mainTheme.colError)
self.colRepTag = QColor(*self.mainTheme.colRepTag)
self.colMod = QColor(*self.mainTheme.colMod)
self.colBreak = QColor(*self.mainTheme.colEmph)
self.colBreak.setAlpha(64)
self.colEmph = None
if self.mainConf.highlightEmph:
self.colEmph = QColor(*self.theTheme.colEmph)
self.colEmph = QColor(*self.mainTheme.colEmph)
self.hStyles = {
"header1": self._makeFormat(self.colHead, "bold", 1.8),
@@ -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.mainGui.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]
+102 -115
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,16 +51,18 @@ logger = logging.getLogger(__name__)
class GuiDocViewer(QTextBrowser):
def __init__(self, theParent):
QTextBrowser.__init__(self, theParent)
loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, mainGui):
QTextBrowser.__init__(self, mainGui)
logger.debug("Initialising GuiDocViewer ...")
# Class Variables
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theParent.theProject
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject
# Internal Variables
self._docHandle = None
@@ -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)
@@ -113,14 +118,14 @@ class GuiDocViewer(QTextBrowser):
# Set the widget colours to match syntax theme
mainPalette = self.palette()
mainPalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack))
mainPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack))
mainPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText))
mainPalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack))
mainPalette.setColor(QPalette.Base, QColor(*self.mainTheme.colBack))
mainPalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText))
self.setPalette(mainPalette)
docPalette = self.viewport().palette()
docPalette.setColor(QPalette.Base, QColor(*self.theTheme.colBack))
docPalette.setColor(QPalette.Text, QColor(*self.theTheme.colText))
docPalette.setColor(QPalette.Base, QColor(*self.mainTheme.colBack))
docPalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText))
self.viewport().setPalette(docPalette)
self.docHeader.matchColours()
@@ -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
@@ -216,7 +221,7 @@ class GuiDocViewer(QTextBrowser):
self.updateDocMargins()
# Make sure the main GUI knows we changed the content
self.theParent.viewMeta.refreshReferences(tHandle)
self.mainGui.viewMeta.refreshReferences(tHandle)
# Since we change the content while it may still be rendering, we mark
# the document dirty again to make sure it's re-rendered properly.
@@ -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")
@@ -553,27 +539,27 @@ class GuiDocViewer(QTextBrowser):
" text-align: center;"
"}}\n"
).format(
tColR=self.theTheme.colText[0],
tColG=self.theTheme.colText[1],
tColB=self.theTheme.colText[2],
hColR=self.theTheme.colHead[0],
hColG=self.theTheme.colHead[1],
hColB=self.theTheme.colHead[2],
aColR=self.theTheme.colVal[0],
aColG=self.theTheme.colVal[1],
aColB=self.theTheme.colVal[2],
eColR=self.theTheme.colEmph[0],
eColG=self.theTheme.colEmph[1],
eColB=self.theTheme.colEmph[2],
kColR=self.theTheme.colKey[0],
kColG=self.theTheme.colKey[1],
kColB=self.theTheme.colKey[2],
cColR=self.theTheme.colHidden[0],
cColG=self.theTheme.colHidden[1],
cColB=self.theTheme.colHidden[2],
mColR=self.theTheme.colMod[0],
mColG=self.theTheme.colMod[1],
mColB=self.theTheme.colMod[2],
tColR=self.mainTheme.colText[0],
tColG=self.mainTheme.colText[1],
tColB=self.mainTheme.colText[2],
hColR=self.mainTheme.colHead[0],
hColG=self.mainTheme.colHead[1],
hColB=self.mainTheme.colHead[2],
aColR=self.mainTheme.colVal[0],
aColG=self.mainTheme.colVal[1],
aColB=self.mainTheme.colVal[2],
eColR=self.mainTheme.colEmph[0],
eColG=self.mainTheme.colEmph[1],
eColB=self.mainTheme.colEmph[2],
kColR=self.mainTheme.colKey[0],
kColG=self.mainTheme.colKey[1],
kColB=self.mainTheme.colKey[2],
cColR=self.mainTheme.colHidden[0],
cColG=self.mainTheme.colHidden[1],
cColB=self.mainTheme.colHidden[2],
mColR=self.mainTheme.colMod[0],
mColG=self.mainTheme.colMod[1],
mColB=self.mainTheme.colMod[2],
)
self.document().setDefaultStyleSheet(styleSheet)
@@ -728,14 +714,14 @@ class GuiDocViewHeader(QWidget):
self.mainConf = novelwriter.CONFIG
self.docViewer = docViewer
self.theParent = docViewer.theParent
self.mainGui = docViewer.mainGui
self.theProject = docViewer.theProject
self.theTheme = docViewer.theTheme
self.mainTheme = docViewer.mainTheme
# Internal Variables
self._docHandle = None
fPx = int(0.9*self.theTheme.fontPixelSize)
fPx = int(0.9*self.mainTheme.fontPixelSize)
hSp = self.mainConf.pxInt(6)
# Main Widget Settings
@@ -752,17 +738,17 @@ class GuiDocViewHeader(QWidget):
self.theTitle.setFixedHeight(fPx)
lblFont = self.theTitle.font()
lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize)
lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize)
self.theTitle.setFont(lblFont)
buttonStyle = (
"QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
).format(*self.theTheme.colText)
).format(*self.mainTheme.colText)
# Buttons
self.backButton = QToolButton(self)
self.backButton.setIcon(self.theTheme.getIcon("backward"))
self.backButton.setIcon(self.mainTheme.getIcon("backward"))
self.backButton.setContentsMargins(0, 0, 0, 0)
self.backButton.setIconSize(QSize(fPx, fPx))
self.backButton.setFixedSize(fPx, fPx)
@@ -773,7 +759,7 @@ class GuiDocViewHeader(QWidget):
self.backButton.clicked.connect(self.docViewer.navBackward)
self.forwardButton = QToolButton(self)
self.forwardButton.setIcon(self.theTheme.getIcon("forward"))
self.forwardButton.setIcon(self.mainTheme.getIcon("forward"))
self.forwardButton.setContentsMargins(0, 0, 0, 0)
self.forwardButton.setIconSize(QSize(fPx, fPx))
self.forwardButton.setFixedSize(fPx, fPx)
@@ -784,7 +770,7 @@ class GuiDocViewHeader(QWidget):
self.forwardButton.clicked.connect(self.docViewer.navForward)
self.refreshButton = QToolButton(self)
self.refreshButton.setIcon(self.theTheme.getIcon("refresh"))
self.refreshButton.setIcon(self.mainTheme.getIcon("refresh"))
self.refreshButton.setContentsMargins(0, 0, 0, 0)
self.refreshButton.setIconSize(QSize(fPx, fPx))
self.refreshButton.setFixedSize(fPx, fPx)
@@ -795,7 +781,7 @@ class GuiDocViewHeader(QWidget):
self.refreshButton.clicked.connect(self._refreshDocument)
self.closeButton = QToolButton(self)
self.closeButton.setIcon(self.theTheme.getIcon("close"))
self.closeButton.setIcon(self.mainTheme.getIcon("close"))
self.closeButton.setContentsMargins(0, 0, 0, 0)
self.closeButton.setIconSize(QSize(fPx, fPx))
self.closeButton.setFixedSize(fPx, fPx)
@@ -838,9 +824,9 @@ class GuiDocViewHeader(QWidget):
theme rather than the main GUI.
"""
thePalette = QPalette()
thePalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack))
thePalette.setColor(QPalette.WindowText, QColor(*self.theTheme.colText))
thePalette.setColor(QPalette.Text, QColor(*self.theTheme.colText))
thePalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack))
thePalette.setColor(QPalette.WindowText, QColor(*self.mainTheme.colText))
thePalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText))
self.setPalette(thePalette)
self.theTitle.setPalette(thePalette)
@@ -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)
@@ -896,14 +882,14 @@ class GuiDocViewHeader(QWidget):
def _closeDocument(self):
"""Trigger the close editor/viewer on the main window.
"""
self.theParent.closeDocViewer()
self.mainGui.closeDocViewer()
return
def _refreshDocument(self):
"""Reload the content of the document.
"""
if self.docViewer.docHandle() == self.theParent.docEditor.docHandle():
self.theParent.saveDocument()
if self.docViewer.docHandle() == self.mainGui.docEditor.docHandle():
self.mainGui.saveDocument()
self.docViewer.reloadText()
return
@@ -915,7 +901,7 @@ class GuiDocViewHeader(QWidget):
"""Capture a click on the title and ensure that the item is
selected in the project tree.
"""
self.theParent.treeView.setSelectedHandle(self._docHandle, doScroll=True)
self.mainGui.projView.setSelectedHandle(self._docHandle, doScroll=True)
return
# END Class GuiDocViewHeader
@@ -935,26 +921,26 @@ class GuiDocViewFooter(QWidget):
self.mainConf = novelwriter.CONFIG
self.docViewer = docViewer
self.theParent = docViewer.theParent
self.theTheme = docViewer.theTheme
self.viewMeta = docViewer.theParent.viewMeta
self.mainGui = docViewer.mainGui
self.mainTheme = docViewer.mainTheme
self.viewMeta = docViewer.mainGui.viewMeta
# Internal Variables
self._docHandle = None
fPx = int(0.9*self.theTheme.fontPixelSize)
fPx = int(0.9*self.mainTheme.fontPixelSize)
bSp = self.mainConf.pxInt(2)
hSp = self.mainConf.pxInt(8)
# Icons
stickyOn = self.theTheme.getPixmap("sticky-on", (fPx, fPx))
stickyOff = self.theTheme.getPixmap("sticky-off", (fPx, fPx))
stickyOn = self.mainTheme.getPixmap("sticky-on", (fPx, fPx))
stickyOff = self.mainTheme.getPixmap("sticky-off", (fPx, fPx))
stickyIcon = QIcon()
stickyIcon.addPixmap(stickyOn, QIcon.Normal, QIcon.On)
stickyIcon.addPixmap(stickyOff, QIcon.Normal, QIcon.Off)
bulletOn = self.theTheme.getPixmap("bullet-on", (fPx, fPx))
bulletOff = self.theTheme.getPixmap("bullet-off", (fPx, fPx))
bulletOn = self.mainTheme.getPixmap("bullet-on", (fPx, fPx))
bulletOff = self.mainTheme.getPixmap("bullet-off", (fPx, fPx))
bulletIcon = QIcon()
bulletIcon.addPixmap(bulletOn, QIcon.Normal, QIcon.On)
bulletIcon.addPixmap(bulletOff, QIcon.Normal, QIcon.Off)
@@ -966,13 +952,13 @@ class GuiDocViewFooter(QWidget):
buttonStyle = (
"QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
).format(*self.theTheme.colText)
).format(*self.mainTheme.colText)
# Show/Hide Details
self.showHide = QToolButton(self)
self.showHide.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.showHide.setStyleSheet(buttonStyle)
self.showHide.setIcon(self.theTheme.getIcon("reference"))
self.showHide.setIcon(self.mainTheme.getIcon("reference"))
self.showHide.setIconSize(QSize(fPx, fPx))
self.showHide.setFixedSize(QSize(fPx, fPx))
self.showHide.clicked.connect(self._doShowHide)
@@ -1053,7 +1039,7 @@ class GuiDocViewFooter(QWidget):
self.lblSynopsis.setAlignment(Qt.AlignLeft | Qt.AlignTop)
lblFont = self.font()
lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize)
lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize)
self.lblRefs.setFont(lblFont)
self.lblSticky.setFont(lblFont)
self.lblComments.setFont(lblFont)
@@ -1098,9 +1084,9 @@ class GuiDocViewFooter(QWidget):
theme rather than the main GUI.
"""
thePalette = QPalette()
thePalette.setColor(QPalette.Window, QColor(*self.theTheme.colBack))
thePalette.setColor(QPalette.WindowText, QColor(*self.theTheme.colText))
thePalette.setColor(QPalette.Text, QColor(*self.theTheme.colText))
thePalette.setColor(QPalette.Window, QColor(*self.mainTheme.colBack))
thePalette.setColor(QPalette.WindowText, QColor(*self.mainTheme.colText))
thePalette.setColor(QPalette.Text, QColor(*self.mainTheme.colText))
self.setPalette(thePalette)
self.lblRefs.setPalette(thePalette)
@@ -1154,14 +1140,14 @@ class GuiDocViewFooter(QWidget):
class GuiDocViewDetails(QScrollArea):
def __init__(self, theParent):
QScrollArea.__init__(self, theParent)
def __init__(self, mainGui):
QScrollArea.__init__(self, mainGui)
logger.debug("Initialising GuiDocViewDetails ...")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
self.theTheme = theParent.theTheme
self.mainGui = mainGui
self.theProject = mainGui.theProject
self.mainTheme = mainGui.mainTheme
self.refList = QLabel("")
self.refList.setWordWrap(True)
@@ -1170,7 +1156,7 @@ class GuiDocViewDetails(QScrollArea):
self.refList.linkActivated.connect(self._linkClicked)
self.linkStyle = "style='color: rgb({0},{1},{2})'".format(
*self.theTheme.colLink
*self.mainTheme.colLink
)
# Assemble
@@ -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")
@@ -1194,13 +1181,13 @@ class GuiDocViewDetails(QScrollArea):
"""Update the current list of document references from the
project index.
"""
if self.theParent.docViewer.stickyRef:
if self.mainGui.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
@@ -1222,7 +1209,7 @@ class GuiDocViewDetails(QScrollArea):
if len(theLink) == 21:
tHandle = theLink[:13]
tAnchor = theLink[13:]
self.theParent.viewDocument(tHandle, tAnchor)
self.mainGui.viewDocument(tHandle, tAnchor)
return
# END Class GuiDocViewDetails
+33 -34
View File
@@ -30,7 +30,6 @@ from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtGui import QFont, QPixmap
from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel
from novelwriter.enum import nwItemClass, nwItemType
from novelwriter.constants import trConst, nwLabels
logger = logging.getLogger(__name__)
@@ -38,14 +37,14 @@ logger = logging.getLogger(__name__)
class GuiItemDetails(QWidget):
def __init__(self, theParent):
QWidget.__init__(self, theParent)
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
logger.debug("Initialising GuiItemDetails ...")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
self.theTheme = theParent.theTheme
self.mainGui = mainGui
self.theProject = mainGui.theProject
self.mainTheme = mainGui.mainTheme
# Internal Variables
self._itemHandle = None
@@ -54,11 +53,11 @@ class GuiItemDetails(QWidget):
hSp = self.mainConf.pxInt(6)
vSp = self.mainConf.pxInt(1)
mPx = self.mainConf.pxInt(6)
iPx = self.theTheme.baseIconSize
fPt = self.theTheme.fontPointSize
iPx = self.mainTheme.baseIconSize
fPt = self.mainTheme.fontPointSize
self._expCheck = self.theTheme.getPixmap("check", (iPx, iPx))
self._expCross = self.theTheme.getPixmap("cross", (iPx, iPx))
self._expCheck = self.mainTheme.getPixmap("check", (iPx, iPx))
self._expCross = self.mainTheme.getPixmap("cross", (iPx, iPx))
fntLabel = QFont()
fntLabel.setBold(True)
@@ -115,6 +114,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"))
@@ -180,8 +180,8 @@ class GuiItemDetails(QWidget):
self.setLayout(self.mainBox)
# Make sure the columns for flags and counts don't resize too often
flagWidth = self.theTheme.getTextWidth("Mm", fntValue)
countWidth = self.theTheme.getTextWidth("99,999", fntValue)
flagWidth = self.mainTheme.getTextWidth("Mm", fntValue)
countWidth = self.mainTheme.getTextWidth("99,999", fntValue)
self.mainBox.setColumnMinimumWidth(1, flagWidth)
self.mainBox.setColumnMinimumWidth(4, countWidth)
@@ -214,6 +214,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,13 +231,13 @@ class GuiItemDetails(QWidget):
self.clearDetails()
return
nwItem = self.theProject.projTree[tHandle]
nwItem = self.theProject.tree[tHandle]
if nwItem is None:
self.clearDetails()
return
self._itemHandle = tHandle
iPx = int(round(0.8*self.theTheme.baseIconSize))
iPx = int(round(0.8*self.mainTheme.baseIconSize))
# Label
# =====
@@ -236,7 +246,7 @@ class GuiItemDetails(QWidget):
if len(theLabel) > 100:
theLabel = theLabel[:96].rstrip()+" ..."
if nwItem.itemType == nwItemType.FILE:
if nwItem.isFileType():
if nwItem.isExported:
self.labelIcon.setPixmap(self._expCheck)
else:
@@ -249,29 +259,22 @@ class GuiItemDetails(QWidget):
# Status
# ======
itStatus = nwItem.itemStatus
if nwItem.itemClass == nwItemClass.NOVEL:
itStatus = self.theProject.statusItems.checkEntry(itStatus) # Make sure it's valid
flagIcon = self.theParent.statusIcons[itStatus]
else:
itStatus = self.theProject.importItems.checkEntry(itStatus) # Make sure it's valid
flagIcon = self.theParent.importIcons[itStatus]
self.statusIcon.setPixmap(flagIcon.pixmap(iPx, iPx))
self.statusData.setText(nwItem.itemStatus)
theStatus, theIcon = nwItem.getImportStatus()
self.statusIcon.setPixmap(theIcon.pixmap(iPx, iPx))
self.statusData.setText(theStatus)
# Class
# =====
classIcon = self.theTheme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass])
classIcon = self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass])
self.classIcon.setPixmap(classIcon.pixmap(iPx, iPx))
self.classData.setText(trConst(nwLabels.CLASS_NAME[nwItem.itemClass]))
# Layout
# ======
hLevel = self.theParent.theIndex.getHandleHeaderLevel(tHandle)
usageIcon = self.theTheme.getItemIcon(
hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
usageIcon = self.mainTheme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
)
self.usageIcon.setPixmap(usageIcon.pixmap(iPx, iPx))
@@ -280,7 +283,7 @@ class GuiItemDetails(QWidget):
# Counts
# ======
if nwItem.itemType == nwItemType.FILE:
if nwItem.isFileType():
self.cCountData.setText(f"{nwItem.charCount:n}")
self.wCountData.setText(f"{nwItem.wordCount:n}")
self.pCountData.setText(f"{nwItem.paraCount:n}")
@@ -291,12 +294,8 @@ class GuiItemDetails(QWidget):
return
##
# Slots
##
@pyqtSlot(str, int, int, int)
def doUpdateCounts(self, tHandle, cC, wC, pC):
def updateCounts(self, tHandle, cC, wC, pC):
"""Update the counts if the handle is the same as the one we're
already showing. Otherwise, do nothing.
"""
+58 -149
View File
@@ -33,7 +33,7 @@ from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QMenuBar, QAction
from novelwriter.enum import nwItemType, nwItemClass, nwDocAction, nwDocInsert, nwWidget
from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget
from novelwriter.constants import trConst, nwKeyWords, nwLabels, nwUnicode
logger = logging.getLogger(__name__)
@@ -41,13 +41,13 @@ logger = logging.getLogger(__name__)
class GuiMainMenu(QMenuBar):
def __init__(self, theParent):
QMenuBar.__init__(self, theParent)
def __init__(self, mainGui):
QMenuBar.__init__(self, mainGui)
logger.debug("Initialising GuiMainMenu ...")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theProject = theParent.theProject
self.mainGui = mainGui
self.theProject = mainGui.theProject
# Build Menu
self._buildProjectMenu()
@@ -61,34 +61,14 @@ class GuiMainMenu(QMenuBar):
self._buildHelpMenu()
# Function Pointers
self._docAction = self.theParent.passDocumentAction
self._moveTreeItem = self.theParent.treeView.moveTreeItem
self._newTreeItem = self.theParent.treeView.newTreeItem
self._docInsert = self.theParent.docEditor.insertText
self._insertKeyWord = self.theParent.docEditor.insertKeyWord
self._docAction = self.mainGui.passDocumentAction
self._docInsert = self.mainGui.docEditor.insertText
self._insertKeyWord = self.mainGui.docEditor.insertKeyWord
logger.debug("GuiMainMenu initialisation complete")
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 +79,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.
"""
@@ -120,13 +94,7 @@ class GuiMainMenu(QMenuBar):
flag is handled by the document editor class, so we make no
decision, just pass a None to the function and let it decide.
"""
self.theParent.docEditor.toggleSpellCheck(None)
return True
def _toggleAutoOutline(self, theMode):
"""Toggle auto outline when the menu entry is checked.
"""
self.theProject.setAutoOutline(theMode)
self.mainGui.docEditor.toggleSpellCheck(None)
return True
def _openWebsite(self, theUrl):
@@ -155,25 +123,25 @@ class GuiMainMenu(QMenuBar):
# Project > New Project
self.aNewProject = QAction(self.tr("New Project"), self)
self.aNewProject.triggered.connect(lambda: self.theParent.newProject(None))
self.aNewProject.triggered.connect(lambda: self.mainGui.newProject(None))
self.projMenu.addAction(self.aNewProject)
# Project > Open Project
self.aOpenProject = QAction(self.tr("Open Project"), self)
self.aOpenProject.setShortcut("Ctrl+Shift+O")
self.aOpenProject.triggered.connect(lambda: self.theParent.showProjectLoadDialog())
self.aOpenProject.triggered.connect(lambda: self.mainGui.showProjectLoadDialog())
self.projMenu.addAction(self.aOpenProject)
# Project > Save Project
self.aSaveProject = QAction(self.tr("Save Project"), self)
self.aSaveProject.setShortcut("Ctrl+Shift+S")
self.aSaveProject.triggered.connect(lambda: self.theParent.saveProject())
self.aSaveProject.triggered.connect(lambda: self.mainGui.saveProject())
self.projMenu.addAction(self.aSaveProject)
# Project > Close Project
self.aCloseProject = QAction(self.tr("Close Project"), self)
self.aCloseProject.setShortcut("Ctrl+Shift+W")
self.aCloseProject.triggered.connect(lambda: self.theParent.closeProject(False))
self.aCloseProject.triggered.connect(lambda: self.mainGui.closeProject(False))
self.projMenu.addAction(self.aCloseProject)
# Project > Separator
@@ -182,78 +150,33 @@ class GuiMainMenu(QMenuBar):
# Project > Project Settings
self.aProjectSettings = QAction(self.tr("Project Settings"), self)
self.aProjectSettings.setShortcut("Ctrl+Shift+,")
self.aProjectSettings.triggered.connect(lambda: self.theParent.showProjectSettingsDialog())
self.aProjectSettings.triggered.connect(lambda: self.mainGui.showProjectSettingsDialog())
self.projMenu.addAction(self.aProjectSettings)
# Project > Project Details
self.aProjectDetails = QAction(self.tr("Project Details"), self)
self.aProjectDetails.setShortcut("Shift+F6")
self.aProjectDetails.triggered.connect(lambda: self.theParent.showProjectDetailsDialog())
self.aProjectDetails.triggered.connect(lambda: self.mainGui.showProjectDetailsDialog())
self.projMenu.addAction(self.aProjectDetails)
# Project > Separator
self.projMenu.addSeparator()
# Project > New Root
self.rootMenu = self.projMenu.addMenu(self.tr("Create Root Folder"))
self.rootItems = {}
self.rootItems[nwItemClass.NOVEL] = QAction(self.tr("Novel Root"), self.rootMenu)
self.rootItems[nwItemClass.PLOT] = QAction(self.tr("Plot Root"), self.rootMenu)
self.rootItems[nwItemClass.CHARACTER] = QAction(self.tr("Character Root"), self.rootMenu)
self.rootItems[nwItemClass.WORLD] = QAction(self.tr("Location Root"), self.rootMenu)
self.rootItems[nwItemClass.TIMELINE] = QAction(self.tr("Timeline Root"), self.rootMenu)
self.rootItems[nwItemClass.OBJECT] = QAction(self.tr("Object Root"), self.rootMenu)
self.rootItems[nwItemClass.ENTITY] = QAction(self.tr("Entity Root"), self.rootMenu)
self.rootItems[nwItemClass.CUSTOM] = QAction(self.tr("Custom Root"), self.rootMenu)
self.rootItems[nwItemClass.ARCHIVE] = QAction(self.tr("Archive Root"), self.rootMenu)
for n, itemClass in enumerate(self.rootItems.keys()):
self.rootItems[itemClass].triggered.connect(
lambda n, itemClass=itemClass: self._newTreeItem(nwItemType.ROOT, itemClass)
)
self.rootMenu.addAction(self.rootItems[itemClass])
# 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.projMenu.addAction(self.aCreateFolder)
# Project > Separator
self.projMenu.addSeparator()
# Project > Edit
self.aEditItem = QAction(self.tr("Edit Item"), self)
self.aEditItem.setShortcuts(["Ctrl+E", "F2"])
self.aEditItem.triggered.connect(lambda: self.theParent.editItem(None))
self.aEditItem = QAction(self.tr("Rename Item"), self)
self.aEditItem.setShortcuts(["F2"])
self.aEditItem.triggered.connect(lambda: self.mainGui.editItemLabel(None))
self.projMenu.addAction(self.aEditItem)
# Project > Delete
self.aDeleteItem = QAction(self.tr("Delete Item"), self)
self.aDeleteItem.setShortcut("Ctrl+Shift+Del")
self.aDeleteItem.triggered.connect(lambda: self.theParent.treeView.deleteItem(None))
self.aDeleteItem.triggered.connect(lambda: self.mainGui.projView.deleteItem(None))
self.projMenu.addAction(self.aDeleteItem)
# Project > Move Up
self.aMoveUp = QAction(self.tr("Move Item Up"), self)
self.aMoveUp.setShortcut("Ctrl+Up")
self.aMoveUp.triggered.connect(lambda: self._moveTreeItem(-1))
self.projMenu.addAction(self.aMoveUp)
# Project > Move Down
self.aMoveDown = QAction(self.tr("Move Item Down"), self)
self.aMoveDown.setShortcut("Ctrl+Down")
self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1))
self.projMenu.addAction(self.aMoveDown)
# Project > Undo Last Action
self.aMoveUndo = QAction(self.tr("Undo Last Move"), self)
self.aMoveUndo.setShortcut("Ctrl+Shift+Z")
self.aMoveUndo.triggered.connect(lambda: self.theParent.treeView.undoLastMove())
self.projMenu.addAction(self.aMoveUndo)
# Project > Empty Trash
self.aEmptyTrash = QAction(self.tr("Empty Trash"), self)
self.aEmptyTrash.triggered.connect(lambda: self.theParent.treeView.emptyTrash())
self.aEmptyTrash.triggered.connect(lambda: self.mainGui.projView.emptyTrash())
self.projMenu.addAction(self.aEmptyTrash)
# Project > Separator
@@ -263,7 +186,7 @@ class GuiMainMenu(QMenuBar):
self.aExitNW = QAction(self.tr("Exit"), self)
self.aExitNW.setShortcut("Ctrl+Q")
self.aExitNW.setMenuRole(QAction.QuitRole)
self.aExitNW.triggered.connect(lambda: self.theParent.closeMain())
self.aExitNW.triggered.connect(lambda: self.mainGui.closeMain())
self.projMenu.addAction(self.aExitNW)
return
@@ -274,28 +197,22 @@ class GuiMainMenu(QMenuBar):
# Document
self.docuMenu = self.addMenu(self.tr("&Document"))
# 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.docuMenu.addAction(self.aNewDoc)
# Document > Open
self.aOpenDoc = QAction(self.tr("Open Document"), self)
self.aOpenDoc.setShortcut("Ctrl+O")
self.aOpenDoc.triggered.connect(lambda: self.theParent.openSelectedItem())
self.aOpenDoc.triggered.connect(lambda: self.mainGui.openSelectedItem())
self.docuMenu.addAction(self.aOpenDoc)
# Document > Save
self.aSaveDoc = QAction(self.tr("Save Document"), self)
self.aSaveDoc.setShortcut("Ctrl+S")
self.aSaveDoc.triggered.connect(lambda: self.theParent.saveDocument())
self.aSaveDoc.triggered.connect(lambda: self.mainGui.saveDocument())
self.docuMenu.addAction(self.aSaveDoc)
# Document > Close
self.aCloseDoc = QAction(self.tr("Close Document"), self)
self.aCloseDoc.setShortcut("Ctrl+W")
self.aCloseDoc.triggered.connect(lambda: self.theParent.closeDocEditor())
self.aCloseDoc.triggered.connect(lambda: self.mainGui.closeDocEditor())
self.docuMenu.addAction(self.aCloseDoc)
# Document > Separator
@@ -304,13 +221,13 @@ class GuiMainMenu(QMenuBar):
# Document > Preview
self.aViewDoc = QAction(self.tr("View Document"), self)
self.aViewDoc.setShortcut("Ctrl+R")
self.aViewDoc.triggered.connect(lambda: self.theParent.viewDocument(None))
self.aViewDoc.triggered.connect(lambda: self.mainGui.viewDocument(None))
self.docuMenu.addAction(self.aViewDoc)
# Document > Close Preview
self.aCloseView = QAction(self.tr("Close Document View"), self)
self.aCloseView.setShortcut("Ctrl+Shift+R")
self.aCloseView.triggered.connect(lambda: self.theParent.closeDocViewer())
self.aCloseView.triggered.connect(lambda: self.mainGui.closeDocViewer())
self.docuMenu.addAction(self.aCloseView)
# Document > Separator
@@ -318,23 +235,23 @@ class GuiMainMenu(QMenuBar):
# Document > Show File Details
self.aFileDetails = QAction(self.tr("Show File Details"), self)
self.aFileDetails.triggered.connect(lambda: self.theParent.docEditor.revealLocation())
self.aFileDetails.triggered.connect(lambda: self.mainGui.docEditor.revealLocation())
self.docuMenu.addAction(self.aFileDetails)
# Document > Import From File
self.aImportFile = QAction(self.tr("Import Text from File"), self)
self.aImportFile.setShortcut("Ctrl+Shift+I")
self.aImportFile.triggered.connect(lambda: self.theParent.importDocument())
self.aImportFile.triggered.connect(lambda: self.mainGui.importDocument())
self.docuMenu.addAction(self.aImportFile)
# Document > Merge Documents
self.aMergeDocs = QAction(self.tr("Merge Folder to Document"), self)
self.aMergeDocs.triggered.connect(lambda: self.theParent.mergeDocuments())
self.aMergeDocs.triggered.connect(lambda: self.mainGui.mergeDocuments())
self.docuMenu.addAction(self.aMergeDocs)
# Document > Split Document
self.aSplitDoc = QAction(self.tr("Split Document to Folder"), self)
self.aSplitDoc.triggered.connect(lambda: self.theParent.splitDocument())
self.aSplitDoc.triggered.connect(lambda: self.mainGui.splitDocument())
self.docuMenu.addAction(self.aSplitDoc)
return
@@ -407,7 +324,7 @@ class GuiMainMenu(QMenuBar):
self.aFocusTree.setShortcut("Ctrl+Alt+1")
else:
self.aFocusTree.setShortcut("Alt+1")
self.aFocusTree.triggered.connect(lambda: self.theParent.switchFocus(nwWidget.TREE))
self.aFocusTree.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.TREE))
self.viewMenu.addAction(self.aFocusTree)
# View > Document Pane 1
@@ -416,7 +333,7 @@ class GuiMainMenu(QMenuBar):
self.aFocusEditor.setShortcut("Ctrl+Alt+2")
else:
self.aFocusEditor.setShortcut("Alt+2")
self.aFocusEditor.triggered.connect(lambda: self.theParent.switchFocus(nwWidget.EDITOR))
self.aFocusEditor.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.EDITOR))
self.viewMenu.addAction(self.aFocusEditor)
# View > Document Pane 2
@@ -425,7 +342,7 @@ class GuiMainMenu(QMenuBar):
self.aFocusView.setShortcut("Ctrl+Alt+3")
else:
self.aFocusView.setShortcut("Alt+3")
self.aFocusView.triggered.connect(lambda: self.theParent.switchFocus(nwWidget.VIEWER))
self.aFocusView.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.VIEWER))
self.viewMenu.addAction(self.aFocusView)
# View > Outline
@@ -434,7 +351,7 @@ class GuiMainMenu(QMenuBar):
self.aFocusOutline.setShortcut("Ctrl+Alt+4")
else:
self.aFocusOutline.setShortcut("Alt+4")
self.aFocusOutline.triggered.connect(lambda: self.theParent.switchFocus(nwWidget.OUTLINE))
self.aFocusOutline.triggered.connect(lambda: self.mainGui.switchFocus(nwWidget.OUTLINE))
self.viewMenu.addAction(self.aFocusOutline)
# View > Separator
@@ -443,13 +360,13 @@ class GuiMainMenu(QMenuBar):
# View > Go Backward
self.aViewPrev = QAction(self.tr("Navigate Backward"), self)
self.aViewPrev.setShortcut("Alt+Left")
self.aViewPrev.triggered.connect(lambda: self.theParent.docViewer.navBackward())
self.aViewPrev.triggered.connect(lambda: self.mainGui.docViewer.navBackward())
self.viewMenu.addAction(self.aViewPrev)
# View > Go Forward
self.aViewNext = QAction(self.tr("Navigate Forward"), self)
self.aViewNext.setShortcut("Alt+Right")
self.aViewNext.triggered.connect(lambda: self.theParent.docViewer.navForward())
self.aViewNext.triggered.connect(lambda: self.mainGui.docViewer.navForward())
self.viewMenu.addAction(self.aViewNext)
# View > Separator
@@ -459,14 +376,14 @@ class GuiMainMenu(QMenuBar):
self.aFocusMode = QAction(self.tr("Focus Mode"), self)
self.aFocusMode.setShortcut("F8")
self.aFocusMode.setCheckable(True)
self.aFocusMode.setChecked(self.theParent.isFocusMode)
self.aFocusMode.triggered.connect(lambda: self.theParent.toggleFocusMode())
self.aFocusMode.setChecked(self.mainGui.isFocusMode)
self.aFocusMode.triggered.connect(lambda: self.mainGui.toggleFocusMode())
self.viewMenu.addAction(self.aFocusMode)
# View > Toggle Full Screen
self.aFullScreen = QAction(self.tr("Full Screen Mode"), self)
self.aFullScreen.setShortcut("F11")
self.aFullScreen.triggered.connect(lambda: self.theParent.toggleFullScreenMode())
self.aFullScreen.triggered.connect(lambda: self.mainGui.toggleFullScreenMode())
self.viewMenu.addAction(self.aFullScreen)
return
@@ -669,6 +586,11 @@ class GuiMainMenu(QMenuBar):
self.aInsVSpaceM.triggered.connect(lambda: self._docInsert(nwDocInsert.VSPACE_M))
self.mInsBreaks.addAction(self.aInsVSpaceM)
# Insert > Placeholder Text
self.aLipsumText = QAction(self.tr("Placeholder Text"), self)
self.aLipsumText.triggered.connect(lambda: self.mainGui.showLoremIpsumDialog())
self.insertMenu.addAction(self.aLipsumText)
return
def _buildFormatMenu(self):
@@ -830,7 +752,7 @@ class GuiMainMenu(QMenuBar):
# Search > Find
self.aFind = QAction(self.tr("Find"), self)
self.aFind.setShortcut("Ctrl+F")
self.aFind.triggered.connect(lambda: self.theParent.docEditor.beginSearch())
self.aFind.triggered.connect(lambda: self.mainGui.docEditor.beginSearch())
self.srcMenu.addAction(self.aFind)
# Search > Replace
@@ -839,7 +761,7 @@ class GuiMainMenu(QMenuBar):
self.aReplace.setShortcut("Ctrl+=")
else:
self.aReplace.setShortcut("Ctrl+H")
self.aReplace.triggered.connect(lambda: self.theParent.docEditor.beginReplace())
self.aReplace.triggered.connect(lambda: self.mainGui.docEditor.beginReplace())
self.srcMenu.addAction(self.aReplace)
# Search > Find Next
@@ -848,7 +770,7 @@ class GuiMainMenu(QMenuBar):
self.aFindNext.setShortcuts(["Ctrl+G", "F3"])
else:
self.aFindNext.setShortcuts(["F3", "Ctrl+G"])
self.aFindNext.triggered.connect(lambda: self.theParent.docEditor.findNext())
self.aFindNext.triggered.connect(lambda: self.mainGui.docEditor.findNext())
self.srcMenu.addAction(self.aFindNext)
# Search > Find Prev
@@ -857,13 +779,13 @@ class GuiMainMenu(QMenuBar):
self.aFindPrev.setShortcuts(["Ctrl+Shift+G", "Shift+F3"])
else:
self.aFindPrev.setShortcuts(["Shift+F3", "Ctrl+Shift+G"])
self.aFindPrev.triggered.connect(lambda: self.theParent.docEditor.findNext(goBack=True))
self.aFindPrev.triggered.connect(lambda: self.mainGui.docEditor.findNext(goBack=True))
self.srcMenu.addAction(self.aFindPrev)
# Search > Replace Next
self.aReplaceNext = QAction(self.tr("Replace Next"), self)
self.aReplaceNext.setShortcut("Ctrl+Shift+1")
self.aReplaceNext.triggered.connect(lambda: self.theParent.docEditor.replaceNext())
self.aReplaceNext.triggered.connect(lambda: self.mainGui.docEditor.replaceNext())
self.srcMenu.addAction(self.aReplaceNext)
return
@@ -885,12 +807,12 @@ class GuiMainMenu(QMenuBar):
# Tools > Re-Run Spell Check
self.aReRunSpell = QAction(self.tr("Re-Run Spell Check"), self)
self.aReRunSpell.setShortcut("F7")
self.aReRunSpell.triggered.connect(lambda: self.theParent.docEditor.spellCheckDocument())
self.aReRunSpell.triggered.connect(lambda: self.mainGui.docEditor.spellCheckDocument())
self.toolsMenu.addAction(self.aReRunSpell)
# Tools > Project Word List
self.aEditWordList = QAction(self.tr("Project Word List"), self)
self.aEditWordList.triggered.connect(lambda: self.theParent.showProjectWordListDialog())
self.aEditWordList.triggered.connect(lambda: self.mainGui.showProjectWordListDialog())
self.toolsMenu.addAction(self.aEditWordList)
# Tools > Separator
@@ -899,22 +821,9 @@ class GuiMainMenu(QMenuBar):
# Tools > Rebuild Indices
self.aRebuildIndex = QAction(self.tr("Rebuild Index"), self)
self.aRebuildIndex.setShortcut("F9")
self.aRebuildIndex.triggered.connect(lambda: self.theParent.rebuildIndex())
self.aRebuildIndex.triggered.connect(lambda: self.mainGui.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()
@@ -926,20 +835,20 @@ class GuiMainMenu(QMenuBar):
# Tools > Export Project
self.aBuildProject = QAction(self.tr("Build Novel Project"), self)
self.aBuildProject.setShortcut("F5")
self.aBuildProject.triggered.connect(lambda: self.theParent.showBuildProjectDialog())
self.aBuildProject.triggered.connect(lambda: self.mainGui.showBuildProjectDialog())
self.toolsMenu.addAction(self.aBuildProject)
# Tools > Writing Stats
self.aWritingStats = QAction(self.tr("Writing Statistics"), self)
self.aWritingStats.setShortcut("F6")
self.aWritingStats.triggered.connect(lambda: self.theParent.showWritingStatsDialog())
self.aWritingStats.triggered.connect(lambda: self.mainGui.showWritingStatsDialog())
self.toolsMenu.addAction(self.aWritingStats)
# Tools > Settings
self.aPreferences = QAction(self.tr("Preferences"), self)
self.aPreferences.setShortcut("Ctrl+,")
self.aPreferences.setMenuRole(QAction.PreferencesRole)
self.aPreferences.triggered.connect(lambda: self.theParent.showPreferencesDialog())
self.aPreferences.triggered.connect(lambda: self.mainGui.showPreferencesDialog())
self.toolsMenu.addAction(self.aPreferences)
return
@@ -953,13 +862,13 @@ class GuiMainMenu(QMenuBar):
# Help > About
self.aAboutNW = QAction(self.tr("About novelWriter"), self)
self.aAboutNW.setMenuRole(QAction.AboutRole)
self.aAboutNW.triggered.connect(lambda: self.theParent.showAboutNWDialog())
self.aAboutNW.triggered.connect(lambda: self.mainGui.showAboutNWDialog())
self.helpMenu.addAction(self.aAboutNW)
# Help > About Qt5
self.aAboutQt = QAction(self.tr("About Qt5"), self)
self.aAboutQt.setMenuRole(QAction.AboutQtRole)
self.aAboutQt.triggered.connect(lambda: self.theParent.showAboutQtDialog())
self.aAboutQt.triggered.connect(lambda: self.mainGui.showAboutQtDialog())
self.helpMenu.addAction(self.aAboutQt)
# Help > Separator
@@ -1006,7 +915,7 @@ class GuiMainMenu(QMenuBar):
# Document > Check for Updates
self.aUpdates = QAction(self.tr("Check for New Release"), self)
self.aUpdates.triggered.connect(lambda: self.theParent.showUpdatesDialog())
self.aUpdates.triggered.connect(lambda: self.mainGui.showUpdatesDialog())
self.helpMenu.addAction(self.aUpdates)
return
+525 -130
View File
@@ -4,7 +4,9 @@ novelWriter GUI Novel Tree
GUI classe for the main window novel tree
File History:
Created: 2020-12-20 [1.1a0]
Created: 2020-12-20 [1.1a0] GuiNovelTree
Created: 2022-06-12 [1.7b1] GuiNovelView
Created: 2022-06-12 [1.7b1] GuiNovelToolBar
This file is a part of novelWriter
Copyright 20182020, Veronica Berglyd Olsen
@@ -26,82 +28,390 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import novelwriter
from enum import Enum
from time import time
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView
from PyQt5.QtGui import QPalette
from PyQt5.QtCore import Qt, QSize, pyqtSlot, pyqtSignal
from PyQt5.QtWidgets import (
QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView, QLabel,
QMenu, QSizePolicy, QToolButton, QToolTip, QTreeWidget, QTreeWidgetItem,
QVBoxLayout, QWidget
)
from novelwriter.enum import nwDocMode, nwItemClass, nwOutline
from novelwriter.common import checkInt
from novelwriter.constants import nwKeyWords
from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst
logger = logging.getLogger(__name__)
class NovelTreeColumn(Enum):
HIDDEN = 0
POV = 1
FOCUS = 2
PLOT = 3
# END Enum NovelTreeColumn
class GuiNovelView(QWidget):
# Signals for user interaction with the novel tree
selectedItemChanged = pyqtSignal(str)
openDocumentRequest = pyqtSignal(str, Enum, int, str)
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
self.mainGui = mainGui
self.theProject = mainGui.theProject
# Build GUI
self.novelBar = GuiNovelToolBar(self)
self.novelTree = GuiNovelTree(self)
# Assemble
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(self.novelBar, 0)
self.outerBox.addWidget(self.novelTree, 1)
self.outerBox.setContentsMargins(0, 0, 0, 0)
self.outerBox.setSpacing(0)
self.setLayout(self.outerBox)
# Function Mappings
self.updateWordCounts = self.novelTree.updateWordCounts
self.getSelectedHandle = self.novelTree.getSelectedHandle
self.setActiveHandle = self.novelTree.setActiveHandle
return
##
# Methods
##
def initSettings(self):
"""Initialise GUI elements that depend on specific settings.
"""
self.novelTree.initSettings()
return
def refreshTree(self):
"""Refresh the current tree.
"""
self.novelTree.refreshTree(rootHandle=self.theProject.lastNovel)
return
def clearProject(self):
"""Clear project-related GUI content.
"""
self.novelTree.clearContent()
self.novelBar.clearContent()
return
def openProjectTasks(self):
"""Run open project tasks.
"""
lastNovel = self.theProject.lastNovel
if lastNovel not in self.theProject.tree:
lastNovel = self.theProject.tree.findRoot(nwItemClass.NOVEL)
logger.debug("Setting novel tree to root item '%s'", lastNovel)
lastCol = self.theProject.options.getEnum(
"GuiNovelView", "lastCol", NovelTreeColumn, NovelTreeColumn.HIDDEN
)
self.clearProject()
self.novelBar.buildNovelRootMenu()
self.novelBar.setLastColType(lastCol, doRefresh=False)
self.novelBar.setCurrentRoot(lastNovel)
return
def closeProjectTasks(self):
"""Run closing project tasks.
"""
lastColType = self.novelTree.lastColType
self.theProject.options.setValue("GuiNovelView", "lastCol", lastColType)
return
def setTreeFocus(self):
"""Set the focus to the tree widget.
"""
self.novelTree.setFocus()
return
def treeHasFocus(self):
"""Check if the novel tree has focus.
"""
return self.novelTree.hasFocus()
##
# Public Slots
##
@pyqtSlot(str)
def updateRootItem(self, tHandle):
"""If any root item changes, rebuild the novel root menu.
"""
self.novelBar.buildNovelRootMenu()
return
# END Class GuiNovelView
class GuiNovelToolBar(QWidget):
def __init__(self, novelView):
QTreeWidget.__init__(self, novelView)
logger.debug("Initialising GuiNovelToolBar ...")
self.mainConf = novelwriter.CONFIG
self.novelView = novelView
self.theProject = novelView.mainGui.theProject
self.mainTheme = novelView.mainGui.mainTheme
iPx = self.mainTheme.baseIconSize
mPx = self.mainConf.pxInt(2)
self.setContentsMargins(0, 0, 0, 0)
self.setAutoFillBackground(True)
qPalette = self.palette()
qPalette.setBrush(QPalette.Window, qPalette.base())
self.setPalette(qPalette)
fadeCol = qPalette.text().color()
buttonStyle = (
"QToolButton {{padding: {0}px; border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
).format(mPx, fadeCol.red(), fadeCol.green(), fadeCol.blue())
# Widget Label
self.viewLabel = QLabel("<b>%s</b>" % self.tr("Novel Outline"))
self.viewLabel.setContentsMargins(0, 0, 0, 0)
self.viewLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# Refresh Button
self.tbRefresh = QToolButton(self)
self.tbRefresh.setToolTip(self.tr("Refresh"))
self.tbRefresh.setIcon(self.mainTheme.getIcon("refresh"))
self.tbRefresh.setIconSize(QSize(iPx, iPx))
self.tbRefresh.setStyleSheet(buttonStyle)
self.tbRefresh.clicked.connect(self._refreshNovelTree)
# Novel Root Menu
self.mRoot = QMenu()
self.gRoot = QActionGroup(self.mRoot)
self.aRoot = {}
self.tbRoot = QToolButton(self)
self.tbRoot.setToolTip(self.tr("Novel Root"))
self.tbRoot.setIcon(self.mainTheme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]))
self.tbRoot.setIconSize(QSize(iPx, iPx))
self.tbRoot.setStyleSheet(buttonStyle)
self.tbRoot.setMenu(self.mRoot)
self.tbRoot.setPopupMode(QToolButton.InstantPopup)
# More Options Menu
self.mMore = QMenu()
self.mLastCol = self.mMore.addMenu(self.tr("Last Column"))
self.gLastCol = QActionGroup(self.mMore)
self.aLastCol = {}
self._addLastColAction(NovelTreeColumn.HIDDEN, self.tr("Hidden"))
self._addLastColAction(NovelTreeColumn.POV, self.tr("Point of View Character"))
self._addLastColAction(NovelTreeColumn.FOCUS, self.tr("Focus Character"))
self._addLastColAction(NovelTreeColumn.PLOT, self.tr("Novel Plot"))
self.tbMore = QToolButton(self)
self.tbMore.setToolTip(self.tr("More Options"))
self.tbMore.setIcon(self.mainTheme.getIcon("menu"))
self.tbMore.setIconSize(QSize(iPx, iPx))
self.tbMore.setStyleSheet(buttonStyle)
self.tbMore.setMenu(self.mMore)
self.tbMore.setPopupMode(QToolButton.InstantPopup)
# Assemble
self.outerBox = QHBoxLayout()
self.outerBox.addWidget(self.viewLabel)
self.outerBox.addWidget(self.tbRefresh)
self.outerBox.addWidget(self.tbRoot)
self.outerBox.addWidget(self.tbMore)
self.outerBox.setContentsMargins(mPx, mPx, 0, mPx)
self.outerBox.setSpacing(0)
self.setLayout(self.outerBox)
logger.debug("GuiNovelToolBar initialisation complete")
return
##
# Methods
##
def clearContent(self):
"""Run clearing project tasks.
"""
self.mRoot.clear()
self.aRoot = {}
return
def buildNovelRootMenu(self):
"""Build the novel root menu.
"""
self.mRoot.clear()
self.aRoot = {}
for n, (tHandle, nwItem) in enumerate(self.theProject.tree.iterRoots(nwItemClass.NOVEL)):
aRoot = self.mRoot.addAction(nwItem.itemName)
aRoot.setData(tHandle)
aRoot.setCheckable(True)
aRoot.triggered.connect(lambda n, tHandle=tHandle: self.setCurrentRoot(tHandle))
self.gRoot.addAction(aRoot)
self.aRoot[tHandle] = aRoot
return
def setCurrentRoot(self, rootHandle):
"""Set the current active root handle.
"""
if rootHandle in self.aRoot:
self.aRoot[rootHandle].setChecked(True)
self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True)
return
def setLastColType(self, colType, doRefresh=True):
"""Set the last column type.
"""
self.aLastCol[colType].setChecked(True)
self.novelView.novelTree.setLastColType(colType, doRefresh=doRefresh)
return
##
# Private Slots
##
@pyqtSlot()
def _refreshNovelTree(self):
"""Rebuild the current tree.
"""
rootHandle = self.theProject.lastNovel
self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True)
return
##
# Internal Functions
##
def _addLastColAction(self, colType, actionLabel):
"""Add a column selection entry to the last column menu.
"""
aLast = self.mLastCol.addAction(actionLabel)
aLast.setCheckable(True)
aLast.setActionGroup(self.gLastCol)
aLast.triggered.connect(lambda: self.setLastColType(colType))
self.aLastCol[colType] = aLast
return
# END Class GuiNovelToolBar
class GuiNovelTree(QTreeWidget):
C_TITLE = 0
C_WORDS = 1
C_POV = 2
C_EXTRA = 2
C_MORE = 3
def __init__(self, theParent):
QTreeWidget.__init__(self, theParent)
D_HANDLE = Qt.UserRole
D_TITLE = Qt.UserRole + 1
D_KEY = Qt.UserRole + 2
def __init__(self, novelView):
QTreeWidget.__init__(self, novelView)
logger.debug("Initialising GuiNovelTree ...")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theParent.theProject
self.theIndex = theParent.theIndex
self.novelView = novelView
self.mainGui = novelView.mainGui
self.mainTheme = novelView.mainGui.mainTheme
self.theProject = novelView.mainGui.theProject
# Internal Variables
self._treeMap = {}
self._lastBuild = 0
self._lastCol = NovelTreeColumn.POV
self._actHandle = None
# Cached Strings
self._povLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
self._focLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY])
self._pltLabel = trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY])
# Build GUI
iPx = self.theTheme.baseIconSize
# =========
iPx = self.mainTheme.baseIconSize
cMg = self.mainConf.pxInt(6)
self.setIconSize(QSize(iPx, iPx))
self.setIndentation(iPx)
self.setColumnCount(3)
self.setHeaderLabels([
self.tr("Novel Outline"),
self.tr("Words"),
self.tr("POV")
])
self.itemDoubleClicked.connect(self._treeDoubleClick)
self.itemSelectionChanged.connect(self._itemSelected)
self.setFrameStyle(QFrame.NoFrame)
self.setUniformRowHeights(True)
self.setAllColumnsShowFocus(True)
self.setHeaderHidden(True)
self.setIndentation(0)
self.setColumnCount(4)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setSelectionMode(QAbstractItemView.SingleSelection)
self.setExpandsOnDoubleClick(False)
self.setDragEnabled(False)
treeHeadItem = self.headerItem()
treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
treeHeadItem.setToolTip(self.C_TITLE, self.tr("Section title"))
treeHeadItem.setToolTip(self.C_WORDS, self.tr("Word count"))
treeHeadItem.setToolTip(self.C_POV, self.tr("Point-of-view character"))
# Lock the column sizes
treeHeader = self.header()
treeHeader.setStretchLastSection(True)
treeHeader.setMinimumSectionSize(iPx + 6)
treeHeader.setStretchLastSection(False)
treeHeader.setMinimumSectionSize(iPx + cMg)
treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.Stretch)
treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents)
treeHeader.setSectionResizeMode(self.C_EXTRA, QHeaderView.ResizeToContents)
treeHeader.setSectionResizeMode(self.C_MORE, QHeaderView.ResizeToContents)
# Get user's column width preferences for NAME and COUNT
treeColWidth = self.mainConf.getNovelColWidths()
if len(treeColWidth) <= 3:
for colN, colW in enumerate(treeColWidth):
self.setColumnWidth(colN, colW)
# Pre-Generate Tree Formatting
fH1 = self.font()
fH1.setBold(True)
fH1.setUnderline(True)
# The last column should just auto-scale
self.resizeColumnToContents(self.C_POV)
fH2 = self.font()
fH2.setBold(True)
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
self._pIndent = [
self.mainTheme.loadDecoration("deco_doc_h0", pxH=iPx),
self.mainTheme.loadDecoration("deco_doc_h1", pxH=iPx),
self.mainTheme.loadDecoration("deco_doc_h2", pxH=iPx),
self.mainTheme.loadDecoration("deco_doc_h3", pxH=iPx),
self.mainTheme.loadDecoration("deco_doc_h4", pxH=iPx),
]
self._pMore = self.mainTheme.loadDecoration("deco_doc_more", pxH=iPx)
# Connect signals
self.clicked.connect(self._treeItemClicked)
self.itemDoubleClicked.connect(self._treeDoubleClick)
self.itemSelectionChanged.connect(self._treeSelectionChange)
# Set custom settings
self.initTree()
self.initSettings()
logger.debug("GuiNovelTree initialisation complete")
return
def initTree(self):
def initSettings(self):
"""Set or update tree widget settings.
"""
# Scroll bars
@@ -117,11 +427,19 @@ class GuiNovelTree(QTreeWidget):
return
##
# Properties
##
@property
def lastColType(self):
return self._lastCol
##
# Class Methods
##
def clearTree(self):
def clearContent(self):
"""Clear the GUI content and the related maps.
"""
self.clear()
@@ -129,12 +447,15 @@ class GuiNovelTree(QTreeWidget):
self._lastBuild = 0
return
def refreshTree(self, overRide=False):
def refreshTree(self, rootHandle=None, overRide=False):
"""Called whenever the Novel tab is activated.
"""
logger.verbose("Requesting refresh of the novel tree")
treeChanged = self.theParent.treeView.changedSince(self._lastBuild)
indexChanged = self.theIndex.novelChangedSince(self._lastBuild)
if rootHandle is None:
rootHandle = self.theProject.tree.findRoot(nwItemClass.NOVEL)
treeChanged = self.mainGui.projView.changedSince(self._lastBuild)
indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
if not (treeChanged or indexChanged or overRide):
logger.verbose("No changes have been made to the novel index")
return
@@ -142,10 +463,10 @@ class GuiNovelTree(QTreeWidget):
selItem = self.selectedItems()
titleKey = None
if selItem:
titleKey = selItem[0].data(self.C_TITLE, Qt.UserRole)[2]
titleKey = selItem[0].data(self.C_TITLE, self.D_KEY)
self.theParent.treeView.flushTreeOrder()
self._populateTree()
self._populateTree(rootHandle)
self.theProject.setLastNovelViewed(rootHandle)
if titleKey is not None and titleKey in self._treeMap:
self._treeMap[titleKey].setSelected(True)
@@ -155,21 +476,12 @@ 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}")
return
def getColumnSizes(self):
"""Return the column widths for the tree columns.
"""
retVals = [
self.columnWidth(0),
self.columnWidth(1),
]
return retVals
def getSelectedHandle(self):
"""Get the currently selected handle. If multiple items are
selected, return the first.
@@ -178,11 +490,46 @@ class GuiNovelTree(QTreeWidget):
tHandle = None
tLine = 0
if selItem:
tHandle = selItem[0].data(self.C_TITLE, Qt.UserRole)[0]
tLine = checkInt(selItem[0].data(self.C_TITLE, Qt.UserRole)[1], 1) - 1
tHandle = selItem[0].data(self.C_TITLE, self.D_HANDLE)
sTitle = selItem[0].data(self.C_TITLE, self.D_TITLE)
tLine = checkInt(sTitle[1:], 1) - 1
return tHandle, tLine
def setLastColType(self, colType, doRefresh=True):
"""Change the content type of the last column and rebuild.
"""
if self._lastCol != colType:
logger.debug("Changing last column to %s", colType.name)
self._lastCol = colType
self.setColumnHidden(self.C_EXTRA, colType == NovelTreeColumn.HIDDEN)
if doRefresh:
self.refreshTree(rootHandle=self.theProject.lastNovel, overRide=True)
return
def setActiveHandle(self, tHandle):
"""Highlight the rows associated with a given handle.
"""
tStart = time()
self._actHandle = tHandle
for i in range(self.topLevelItemCount()):
tItem = self.topLevelItem(i)
if tItem.data(self.C_TITLE, self.D_HANDLE) == tHandle:
tItem.setBackground(self.C_TITLE, self.palette().alternateBase())
tItem.setBackground(self.C_WORDS, self.palette().alternateBase())
tItem.setBackground(self.C_EXTRA, self.palette().alternateBase())
tItem.setBackground(self.C_MORE, self.palette().alternateBase())
else:
tItem.setBackground(self.C_TITLE, self.palette().base())
tItem.setBackground(self.C_WORDS, self.palette().base())
tItem.setBackground(self.C_EXTRA, self.palette().base())
tItem.setBackground(self.C_MORE, self.palette().base())
logger.verbose("Highlighted Novel Tree in %.3f ms", (time() - tStart)*1000)
return
##
# Events
##
@@ -208,113 +555,161 @@ class GuiNovelTree(QTreeWidget):
if tHandle is None:
return
self.theParent.viewDocument(tHandle)
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "")
return
def focusOutEvent(self, theEvent):
"""Clear the selection when the tree no longer has focus.
"""
QTreeWidget.focusOutEvent(self, theEvent)
self.clearSelection()
return
##
# Slots
# Private Slots
##
def _treeDoubleClick(self, tItem, tCol):
@pyqtSlot("QModelIndex")
def _treeItemClicked(self, mIndex):
"""The user clicked on an item in the tree.
"""
if mIndex.column() == self.C_MORE:
tHandle = mIndex.siblingAtColumn(self.C_TITLE).data(self.D_HANDLE)
sTitle = mIndex.siblingAtColumn(self.C_TITLE).data(self.D_TITLE)
tipPos = self.mapToGlobal(self.visualRect(mIndex).topRight())
self._popMetaBox(tipPos, tHandle, sTitle)
return
@pyqtSlot()
def _treeSelectionChange(self):
"""Extract the handle and line number of the currently selected
title, and send it to the tree meta panel.
"""
tHandle, _ = self.getSelectedHandle()
if tHandle is not None:
self.novelView.selectedItemChanged.emit(tHandle)
return
@pyqtSlot("QTreeWidgetItem*", int)
def _treeDoubleClick(self, tItem, colNo):
"""Extract the handle and line number of the title double-
clicked, and send it to the main gui class for opening in the
document editor.
"""
tHandle, tLine = self.getSelectedHandle()
self.theParent.openDocument(tHandle, tLine=tLine-1, doScroll=True)
return
def _itemSelected(self):
"""Extract the handle and line number of the currently selected
title, and send it to the tree meta panel.
"""
selItems = self.selectedItems()
if selItems:
tHandle = selItems[0].data(self.C_TITLE, Qt.UserRole)[0]
self.theParent.treeMeta.updateViewBox(tHandle)
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, tLine, "")
return
##
# Internal Functions
##
def _populateTree(self):
def _populateTree(self, rootHandle):
"""Build the tree based on the project index.
"""
self.clearTree()
self.clearContent()
tStart = time()
logger.verbose("Building novel tree for root item '%s'", rootHandle)
currTitle = None
currChapter = None
currScene = None
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
for tKey, tHandle, sTitle, novIdx in novStruct:
for tKey, tHandle, sTitle, novIdx in self.theIndex.novelStructure(skipExcluded=True):
iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0)
if iLevel == 0:
continue
tItem = self._createTreeItem(tHandle, sTitle, tKey, novIdx)
self._treeMap[tKey] = tItem
newItem = QTreeWidgetItem()
newItem.setData(self.C_TITLE, Qt.DecorationRole, self._pIndent[iLevel])
newItem.setText(self.C_TITLE, novIdx.title)
newItem.setData(self.C_TITLE, self.D_HANDLE, tHandle)
newItem.setData(self.C_TITLE, self.D_TITLE, sTitle)
newItem.setData(self.C_TITLE, self.D_KEY, tKey)
newItem.setFont(self.C_TITLE, self._hFonts[iLevel])
newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}")
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
newItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore)
tLevel = novIdx["level"]
if tLevel == "H1":
self.addTopLevelItem(tItem)
currTitle = tItem
currChapter = None
currScene = None
# Custom column
lastText, toolTip = self._getLastColumnText(tHandle, sTitle)
newItem.setText(self.C_EXTRA, lastText)
if lastText:
newItem.setToolTip(self.C_EXTRA, toolTip)
elif tLevel == "H2":
if currTitle is None:
self.addTopLevelItem(tItem)
else:
currTitle.addChild(tItem)
currChapter = tItem
currScene = None
self._treeMap[tKey] = newItem
self.addTopLevelItem(newItem)
elif tLevel == "H3":
if currChapter is None:
if currTitle is None:
self.addTopLevelItem(tItem)
else:
currTitle.addChild(tItem)
else:
currChapter.addChild(tItem)
currScene = tItem
elif tLevel == "H4":
if currScene is None:
if currChapter is None:
if currTitle is None:
self.addTopLevelItem(tItem)
else:
currTitle.addChild(tItem)
else:
currChapter.addChild(tItem)
else:
currScene.addChild(tItem)
tItem.setExpanded(True)
self.setActiveHandle(self._actHandle)
logger.verbose("Novel Tree built in %.3f ms", (time() - tStart)*1000)
self._lastBuild = time()
return
def _createTreeItem(self, tHandle, sTitle, titleKey, novIdx):
"""Populate a tree item with all the column values.
def _getLastColumnText(self, tHandle, sTitle):
"""Generate the text for the last column based on user settings.
"""
newItem = QTreeWidgetItem()
hIcon = "doc_%s" % novIdx["level"].lower()
theData = (tHandle, sTitle[1:].lstrip("0"), titleKey)
if self._lastCol == NovelTreeColumn.HIDDEN:
return "", ""
wC = int(novIdx["wCount"])
theRefs = self.theProject.index.getReferences(tHandle, sTitle)
if self._lastCol == NovelTreeColumn.POV:
newText = ", ".join(theRefs[nwKeyWords.POV_KEY])
return newText, f"{self._povLabel}: {newText}"
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)
elif self._lastCol == NovelTreeColumn.FOCUS:
newText = ", ".join(theRefs[nwKeyWords.FOCUS_KEY])
return newText, f"{self._focLabel}: {newText}"
theRefs = self.theIndex.getReferences(tHandle, sTitle)
newItem.setText(self.C_POV, ", ".join(theRefs[nwKeyWords.POV_KEY]))
elif self._lastCol == NovelTreeColumn.PLOT:
newText = ", ".join(theRefs[nwKeyWords.PLOT_KEY])
return newText, f"{self._pltLabel}: {newText}"
return newItem
return "", ""
def _popMetaBox(self, qPos, tHandle, sTitle):
"""Show the novel meta data box.
"""
logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle)
pIndex = self.theProject.index
novIdx = pIndex.getNovelData(tHandle, sTitle)
refTags = pIndex.getReferences(tHandle, sTitle)
synopText = novIdx.synopsis
if synopText:
synopLabel = trConst(nwLabels.OUTLINE_COLS[nwOutline.SYNOP])
synopText = f"<p><b>{synopLabel}</b>: {synopText}</p>"
refLines = []
refLines = self._appendMetaTag(refTags, nwKeyWords.POV_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.FOCUS_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.CHAR_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.PLOT_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.TIME_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.WORLD_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.OBJECT_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.ENTITY_KEY, refLines)
refLines = self._appendMetaTag(refTags, nwKeyWords.CUSTOM_KEY, refLines)
refText = ""
if refLines:
refList = "<br>".join(refLines)
refText = f"<p>{refList}</p>"
ttText = refText + synopText or self.tr("No meta data")
if ttText:
QToolTip.showText(qPos, ttText)
return
@staticmethod
def _appendMetaTag(refs, key, lines):
"""Generate a reference list for a given reference key.
"""
tags = ", ".join(refs.get(key, []))
if tags:
lines.append(f"<b>{trConst(nwLabels.KEY_NAME[key])}</b>: {tags}")
return lines
# END Class GuiNovelTree
+734 -155
View File
File diff suppressed because it is too large Load Diff
-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
File diff suppressed because it is too large Load Diff
+13 -13
View File
@@ -41,22 +41,22 @@ logger = logging.getLogger(__name__)
class GuiMainStatus(QStatusBar):
def __init__(self, theParent):
QStatusBar.__init__(self, theParent)
def __init__(self, mainGui):
QStatusBar.__init__(self, mainGui)
logger.debug("Initialising GuiMainStatus ...")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.refTime = None
self.userIdle = False
colNone = QColor(*self.theTheme.statNone)
colTrue = QColor(*self.theTheme.statUnsaved)
colFalse = QColor(*self.theTheme.statSaved)
colNone = QColor(*self.mainTheme.statNone)
colTrue = QColor(*self.mainTheme.statUnsaved)
colFalse = QColor(*self.mainTheme.statSaved)
iPx = self.theTheme.baseIconSize
iPx = self.mainTheme.baseIconSize
# Permanent Widgets
# =================
@@ -66,7 +66,7 @@ class GuiMainStatus(QStatusBar):
# The Spell Checker Language
self.langIcon = QLabel("")
self.langText = QLabel(self.tr("None"))
self.langIcon.setPixmap(self.theTheme.getPixmap("status_lang", (iPx, iPx)))
self.langIcon.setPixmap(self.mainTheme.getPixmap("status_lang", (iPx, iPx)))
self.langIcon.setContentsMargins(0, 0, 0, 0)
self.langText.setContentsMargins(0, 0, xM, 0)
self.addPermanentWidget(self.langIcon)
@@ -91,7 +91,7 @@ class GuiMainStatus(QStatusBar):
# The Project and Session Stats
self.statsIcon = QLabel()
self.statsText = QLabel("")
self.statsIcon.setPixmap(self.theTheme.getPixmap("status_stats", (iPx, iPx)))
self.statsIcon.setPixmap(self.mainTheme.getPixmap("status_stats", (iPx, iPx)))
self.statsIcon.setContentsMargins(0, 0, 0, 0)
self.statsText.setContentsMargins(0, 0, xM, 0)
self.addPermanentWidget(self.statsIcon)
@@ -99,14 +99,14 @@ class GuiMainStatus(QStatusBar):
# The Session Clock
# Set the mimimum width so the label doesn't rescale every second
self.timePixmap = self.theTheme.getPixmap("status_time", (iPx, iPx))
self.idlePixmap = self.theTheme.getPixmap("status_idle", (iPx, iPx))
self.timePixmap = self.mainTheme.getPixmap("status_time", (iPx, iPx))
self.idlePixmap = self.mainTheme.getPixmap("status_idle", (iPx, iPx))
self.timeIcon = QLabel()
self.timeText = QLabel("")
self.timeIcon.setPixmap(self.timePixmap)
self.timeText.setToolTip(self.tr("Session Time"))
self.timeText.setMinimumWidth(self.theTheme.getTextWidth("00:00:00:"))
self.timeText.setMinimumWidth(self.mainTheme.getTextWidth("00:00:00:"))
self.timeIcon.setContentsMargins(0, 0, 0, 0)
self.timeText.setContentsMargins(0, 0, 0, 0)
self.addPermanentWidget(self.timeIcon)
+32 -31
View File
@@ -54,7 +54,7 @@ class GuiTheme:
def __init__(self):
self.mainConf = novelwriter.CONFIG
self.theIcons = GuiIcons(self)
self.iconCache = GuiIcons(self)
# Loaded Theme Settings
# =====================
@@ -127,13 +127,13 @@ class GuiTheme:
self.updateFont()
self.updateTheme()
self.theIcons.updateTheme()
self.iconCache.updateTheme()
# Icon Functions
self.getIcon = self.theIcons.getIcon
self.getPixmap = self.theIcons.getPixmap
self.getItemIcon = self.theIcons.getItemIcon
self.loadDecoration = self.theIcons.loadDecoration
self.getIcon = self.iconCache.getIcon
self.getPixmap = self.iconCache.getPixmap
self.getItemIcon = self.iconCache.getItemIcon
self.loadDecoration = self.iconCache.loadDecoration
# Extract Other Info
self.guiDPI = qApp.primaryScreen().logicalDotsPerInchX()
@@ -456,34 +456,35 @@ 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",
"bullet-on", "bullet-off",
# Decorations
"deco_doc_h0", "deco_doc_h1", "deco_doc_h2", "deco_doc_h3", "deco_doc_h4", "deco_doc_more",
}
DECO_MAP = {
IMAGE_MAP = {
"wiz-back": "wizard-back.jpg",
}
def __init__(self, theTheme):
def __init__(self, mainTheme):
self.mainConf = novelwriter.CONFIG
self.theTheme = theTheme
self.mainTheme = mainTheme
# Storage
self._qIcons = {}
@@ -575,19 +576,22 @@ class GuiIcons:
# Access Functions
##
def loadDecoration(self, decoKey, pxW, pxH):
def loadDecoration(self, decoKey, pxW=None, pxH=None):
"""Load graphical decoration element based on the decoration
map. This function always returns a QSwgWidget.
map or the icon map. This function always returns a QPixmap.
"""
if decoKey not in self.DECO_MAP:
if decoKey in self._themeMap:
imgPath = self._themeMap[decoKey]
elif decoKey in self.IMAGE_MAP:
imgPath = os.path.join(
self.mainConf.assetPath, "images", self.IMAGE_MAP[decoKey]
)
else:
logger.error("Decoration with name '%s' does not exist", decoKey)
return QPixmap()
imgPath = os.path.join(
self.mainConf.assetPath, "images", self.DECO_MAP[decoKey]
)
if not os.path.isfile(imgPath):
logger.error("Decoration file '%s' not in assets folder", self.DECO_MAP[decoKey])
logger.error("Asset '%s' not found", self.IMAGE_MAP[decoKey])
return QPixmap()
theDeco = QPixmap(imgPath)
@@ -639,9 +643,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, mainGui):
QToolBar.__init__(self, mainGui)
logger.debug("Initialising GuiViewsBar ...")
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
# Style
iPx = self.mainConf.pxInt(22)
mPx = self.mainConf.pxInt(60)
lblFont = self.mainTheme.guiFont
lblFont.setPointSizeF(0.65*self.mainTheme.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.mainTheme.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.mainTheme.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.mainTheme.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.mainTheme.getIcon("view_build"))
self.aBuild.triggered.connect(lambda: self.mainGui.showBuildProjectDialog())
self.aDetails = QAction(self.tr("Details"))
self.aDetails.setFont(lblFont)
self.aDetails.setToolTip(self.tr("Show project details"))
self.aDetails.setIcon(self.mainTheme.getIcon("proj_details"))
self.aDetails.triggered.connect(lambda: self.mainGui.showProjectDetailsDialog())
self.aStats = QAction(self.tr("Stats"))
self.aStats.setFont(lblFont)
self.aStats.setToolTip(self.tr("Show project statistics"))
self.aStats.setIcon(self.mainTheme.getIcon("proj_stats"))
self.aStats.triggered.connect(lambda: self.mainGui.showWritingStatsDialog())
# Settings Menu
self.mSettings = QMenu()
self.aPrjSettings = QAction(self.tr("Project Settings"))
self.aPrjSettings.triggered.connect(lambda: self.mainGui.showProjectSettingsDialog())
self.mSettings.addAction(self.aPrjSettings)
self.aPreferences = QAction(self.tr("Preferences"))
self.aPreferences.triggered.connect(lambda: self.mainGui.showPreferencesDialog())
self.mSettings.addAction(self.aPreferences)
self.tbSettings = QToolButton(self)
self.tbSettings.setFont(lblFont)
self.tbSettings.setText(self.tr("Settings"))
self.tbSettings.setIcon(self.mainTheme.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
+243 -288
View File
@@ -27,33 +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.QtGui import QIcon, QPixmap, QColor, QKeySequence, QCursor
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, GuiNovelView, GuiOutlineView, GuiProjectView, 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, GuiProjectWizard, GuiWritingStats
from novelwriter.core import NWProject, NWIndex
from novelwriter.tools import (
GuiBuildNovel, GuiLipsum, GuiProjectWizard, GuiWritingStats
)
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__)
@@ -83,9 +84,8 @@ class GuiMain(QMainWindow):
# ============
# Core Classes and Settings
self.theTheme = GuiTheme()
self.mainTheme = GuiTheme()
self.theProject = NWProject(self)
self.theIndex = NWIndex(self.theProject)
self.hasProject = False
self.isFocusMode = False
self.idleRefTime = time()
@@ -101,126 +101,75 @@ 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)
self.treeView = GuiProjectTree(self)
self.novelView = GuiNovelTree(self)
self.docEditor = GuiDocEditor(self)
self.viewMeta = GuiDocViewDetails(self)
self.docViewer = GuiDocViewer(self)
self.treeMeta = GuiItemDetails(self)
self.projView = GuiOutline(self)
self.projMeta = GuiOutlineDetails(self)
self.mainMenu = GuiMainMenu(self)
self.statusBar = GuiMainStatus(self)
self.projView = GuiProjectView(self)
self.novelView = GuiNovelView(self)
self.docEditor = GuiDocEditor(self)
self.viewMeta = GuiDocViewDetails(self)
self.docViewer = GuiDocViewer(self)
self.itemDetails = GuiItemDetails(self)
self.outlineView = GuiOutlineView(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.treeView.itemSelectionChanged.connect(self._treeSingleClick)
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.treeView.novelItemChanged.connect(self._treeNovelItemChanged)
self.treeView.wordCountsChanged.connect(self._updateStatusWordCount)
# Minor GUI Elements
self.statusIcons = []
self.importIcons = []
# 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)
tabFont = self.projTabs.tabBar().font()
tabFont.setPointSizeF(0.9*fPt)
self.projTabs.tabBar().setFont(tabFont)
# 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.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.projView)
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.treeMeta)
self.treeBox.addWidget(self.projStack)
self.treeBox.addWidget(self.itemDetails)
self.treePane.setLayout(self.treeBox)
# Splitter : Document Viewer / Document Meta
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.outlineView)
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.outlineView)
self.idxProjView = self.projStack.indexOf(self.projView)
self.idxNovelView = self.projStack.indexOf(self.novelView)
# Splitter Behaviour
self.splitMain.setCollapsible(self.idxTree, False)
@@ -239,8 +188,36 @@ 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)
# Connect Signals
# ===============
self.viewsBar.viewChangeRequested.connect(self._changeView)
self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
self.projView.openDocumentRequest.connect(self._openDocument)
self.projView.wordCountsChanged.connect(self._updateStatusWordCount)
self.projView.treeItemChanged.connect(self.docEditor.updateDocInfo)
self.projView.treeItemChanged.connect(self.docViewer.updateDocInfo)
self.projView.treeItemChanged.connect(self.itemDetails.updateViewBox)
self.projView.rootFolderChanged.connect(self.outlineView.updateRootItem)
self.projView.rootFolderChanged.connect(self.novelView.updateRootItem)
self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
self.novelView.openDocumentRequest.connect(self._openDocument)
self.docEditor.spellDictionaryChanged.connect(self.statusBar.setLanguage)
self.docEditor.docEditedStatusChanged.connect(self.statusBar.doUpdateDocumentStatus)
self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts)
self.docEditor.docCountsChanged.connect(self.projView.updateCounts)
self.docEditor.loadDocumentTagRequest.connect(self._followTag)
self.docViewer.loadDocumentTagRequest.connect(self._followTag)
self.outlineView.loadDocumentTagRequest.connect(self._followTag)
# Finalise Initialisation
# =======================
@@ -317,15 +294,15 @@ class GuiMain(QMainWindow):
"""Wrapper function to clear all sub-elements of the main GUI.
"""
# Project Area
self.treeView.clearTree()
self.novelView.clearTree()
self.treeMeta.clearDetails()
self.projView.clearProject()
self.novelView.clearProject()
self.itemDetails.clearDetails()
# Work Area
self.docEditor.clearEditor()
self.docEditor.setDictionaries()
self.closeDocViewer()
self.projMeta.clearDetails()
self.outlineView.clearProject()
# General
self.statusBar.clearStatus()
@@ -387,6 +364,8 @@ class GuiMain(QMainWindow):
self.rebuildTrees()
self.saveProject()
self.docEditor.setDictionaries()
self.novelView.openProjectTasks()
self.outlineView.openProjectTasks()
self.rebuildIndex(beQuiet=True)
self.statusBar.setRefTime(self.theProject.projOpened)
self.statusBar.setProjectStatus(nwState.GOOD)
@@ -442,16 +421,17 @@ class GuiMain(QMainWindow):
if saveOK:
self.closeDocument()
self.docViewer.clearNavHistory()
self.projView.closeOutline()
self.outlineView.closeProjectTasks()
self.novelView.closeProjectTasks()
self.theProject.closeProject(self.idleTime)
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
@@ -467,7 +447,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):
@@ -525,15 +505,16 @@ 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.novelView.openProjectTasks()
self.outlineView.openProjectTasks()
self._updateStatusWordCount()
# Restore previously open documents, if any
@@ -544,10 +525,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,9 +547,9 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return False
self.treeView.saveTreeOrder()
self.projView.saveProjectTree()
if self.theProject.saveProject(autoSave=autoSave):
self.theIndex.saveIndex()
self.theProject.index.saveIndex()
return True
@@ -576,7 +557,7 @@ class GuiMain(QMainWindow):
# Document Actions
##
def closeDocument(self):
def closeDocument(self, beforeOpen=False):
"""Close the document and clear the editor and title field.
"""
if not self.hasProject:
@@ -591,6 +572,8 @@ class GuiMain(QMainWindow):
if self.docEditor.docChanged():
self.saveDocument()
self.docEditor.clearEditor()
if not beforeOpen:
self.novelView.setActiveHandle(None)
return True
@@ -601,17 +584,18 @@ 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.closeDocument(beforeOpen=True)
self._changeView(nwView.EDITOR)
if self.docEditor.loadText(tHandle, tLine):
if changeFocus:
self.docEditor.setFocus()
self.theProject.setLastEdited(tHandle)
self.treeView.setSelectedHandle(tHandle, doScroll=doScroll)
self.projView.setSelectedHandle(tHandle, doScroll=doScroll)
self.novelView.setActiveHandle(tHandle)
else:
return False
@@ -625,12 +609,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
@@ -678,7 +661,7 @@ class GuiMain(QMainWindow):
self.saveDocument()
else:
logger.verbose("Trying selected document")
tHandle = self.treeView.getSelectedHandle()
tHandle = self.projView.getSelectedHandle()
if tHandle is None:
logger.verbose("Trying last viewed document")
@@ -689,7 +672,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):
@@ -815,12 +798,12 @@ class GuiMain(QMainWindow):
tHandle = None
tLine = None
if self.treeView.hasFocus():
tHandle = self.treeView.getSelectedHandle()
elif self.novelView.hasFocus():
if self.projView.treeHasFocus():
tHandle = self.projView.getSelectedHandle()
elif self.novelView.treeHasFocus():
tHandle, tLine = self.novelView.getSelectedHandle()
elif self.projView.hasFocus():
tHandle, tLine = self.projView.getSelectedHandle()
elif self.outlineView.treeHasFocus():
tHandle, tLine = self.outlineView.getSelectedHandle()
else:
logger.warning("No item selected")
return False
@@ -830,7 +813,7 @@ class GuiMain(QMainWindow):
return True
def editItem(self, tHandle=None):
def editItemLabel(self, tHandle=None):
"""Open the edit item dialog.
"""
if not self.hasProject:
@@ -841,42 +824,23 @@ class GuiMain(QMainWindow):
if self.docEditor.anyFocus() or self.isFocusMode:
tHandle = self.docEditor.docHandle()
else:
tHandle = self.treeView.getSelectedHandle()
tHandle = self.projView.getSelectedHandle()
if tHandle:
return self.projView.renameTreeItem(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.
"""
self._makeStatusIcons()
self._makeImportIcons()
self.treeView.buildTree()
self.novelView.refreshTree()
self.projView.populateTree()
# self.novelView.refreshTree()
return
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
@@ -892,27 +856,18 @@ class GuiMain(QMainWindow):
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
tStart = time()
self.treeView.saveTreeOrder()
self.theIndex.clearIndex()
self.projView.saveProjectTree()
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)
self.treeView.setTreeItemValues(tItem.itemHandle)
logger.verbose("Indexing '%s'", tItem.itemName)
if self.theProject.index.reIndexHandle(tItem.itemHandle):
# Update Word Counts
self.projView.propagateCount(tItem.itemHandle, tItem.wordCount, countChildren=True)
self.projView.setTreeItemValues(tItem.itemHandle)
tEnd = time()
self.setStatus(
@@ -929,19 +884,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
##
@@ -982,14 +924,13 @@ class GuiMain(QMainWindow):
if dlgConf.result() == QDialog.Accepted:
logger.debug("Applying new preferences")
self.initMain()
self.theTheme.updateTheme()
self.mainTheme.updateTheme()
self.saveDocument()
self.docEditor.initEditor()
self.docViewer.initViewer()
self.treeView.initTree()
self.novelView.initTree()
self.projView.initOutline()
self.projMeta.initDetails()
self.projView.initSettings()
self.novelView.initSettings()
self.outlineView.initSettings()
self._updateStatusWordCount()
return
@@ -1006,7 +947,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.itemDetails.refreshDetails()
self._updateWindowTitle(self.theProject.projName)
return True
@@ -1018,8 +961,6 @@ class GuiMain(QMainWindow):
logger.error("No project open")
return False
self.treeView.flushTreeOrder()
dlgDetails = getGuiItem("GuiProjectDetails")
if dlgDetails is None:
dlgDetails = GuiProjectDetails(self)
@@ -1050,6 +991,24 @@ class GuiMain(QMainWindow):
return True
def showLoremIpsumDialog(self):
"""Open the insert lorem ipsum text dialog.
"""
if not self.hasProject:
logger.error("No project open")
return False
dlgLipsum = getGuiItem("GuiLipsum")
if dlgLipsum is None:
dlgLipsum = GuiLipsum(self)
dlgLipsum.setModal(False)
dlgLipsum.show()
dlgLipsum.raise_()
qApp.processEvents()
return True
def showProjectWordListDialog(self):
"""Open the project word list dialog.
"""
@@ -1206,13 +1165,11 @@ 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.outlineView.splitSizes())
if self.viewMeta.isVisible():
self.mainConf.setViewPanePos(self.splitView.sizes())
self.mainConf.setShowRefPanel(self.viewMeta.isVisible())
self.mainConf.setTreeColWidths(self.treeView.getColumnSizes())
self.mainConf.setNovelColWidths(self.novelView.getColumnSizes())
if not self.mainConf.isFullScreen:
self.mainConf.setWinSize(self.width(), self.height())
@@ -1230,20 +1187,20 @@ class GuiMain(QMainWindow):
"""Switch focus between main GUI views.
"""
if paneNo == nwWidget.TREE:
tabIdx = self.projTabs.currentIndex()
if tabIdx == self.idxTreeView:
self.treeView.setFocus()
tabIdx = self.projStack.currentIndex()
if tabIdx == self.idxProjView:
self.projView.setFocus()
elif tabIdx == self.idxNovelView:
self.novelView.setFocus()
self.novelView.setTreeFocus()
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.outlineView.setTreeFocus()
return
def closeDocEditor(self):
@@ -1275,7 +1232,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")
@@ -1284,7 +1240,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)
@@ -1412,7 +1368,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:
@@ -1442,29 +1398,6 @@ class GuiMain(QMainWindow):
self.saveDocument()
return
def _makeStatusIcons(self):
"""Generate all the item status icons based on project settings.
"""
self.statusIcons = {}
iPx = self.mainConf.pxInt(32)
for sLabel, sCol, _ in self.theProject.statusItems:
theIcon = QPixmap(iPx, iPx)
theIcon.fill(QColor(*sCol))
self.statusIcons[sLabel] = QIcon(theIcon)
return
def _makeImportIcons(self):
"""Generate all the item importance icons based on project
settings.
"""
self.importIcons = {}
iPx = self.mainConf.pxInt(32)
for sLabel, sCol, _ in self.theProject.importItems:
theIcon = QPixmap(iPx, iPx)
theIcon.fill(QColor(*sCol))
self.importIcons[sLabel] = QIcon(theIcon)
return
def _assembleProjectWizardData(self, newProj):
"""Extract the user choices from the New Project Wizard and
store them in a dictionary.
@@ -1478,9 +1411,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 = []
@@ -1490,19 +1423,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
##
@@ -1518,9 +1462,53 @@ 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(str, Enum, int, str)
def _openDocument(self, tHandle, tMode, tLine, tAnchor):
"""Handle an open document request from one of the tree views.
"""
if tHandle is not None:
if tMode == nwDocMode.EDIT:
self.openDocument(tHandle, tLine=tLine, changeFocus=False)
elif tMode == nwDocMode.VIEW:
self.viewDocument(tHandle=tHandle, tAnchor=(tAnchor or None))
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.projView)
elif view == nwView.NOVEL:
self.mainStack.setCurrentWidget(self.splitMain)
self.projStack.setCurrentWidget(self.novelView)
elif view == nwView.OUTLINE:
self.mainStack.setCurrentWidget(self.outlineView)
return
@pyqtSlot()
def _timeTick(self):
"""Triggered on every tick of the main timer.
@@ -1563,39 +1551,6 @@ class GuiMain(QMainWindow):
return
@pyqtSlot()
def _treeSingleClick(self):
"""Single click on a project tree item just updates the details
panel below the tree.
"""
tHandle = self.treeView.getSelectedHandle()
if tHandle is not None:
self.treeMeta.updateViewBox(tHandle)
return
@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.
"""
tHandle = self.treeView.getSelectedHandle()
if tHandle is not None:
self.openDocument(tHandle, changeFocus=False, doScroll=False)
return
@pyqtSlot()
def _treeNovelItemChanged(self):
"""Triggered when there is a change to a novel item in the
project tree.
"""
if self.mainTabs.currentIndex() == self.idxTabProj:
logger.verbose("Novel tree changed while Outline tab active")
if self.hasProject:
self.treeView.flushTreeOrder()
self.projView.refreshTree(novelChanged=True)
return
@pyqtSlot()
def _keyPressReturn(self):
"""Forward the return/enter keypress to the function that opens
@@ -1617,35 +1572,35 @@ class GuiMain(QMainWindow):
return
@pyqtSlot(int)
def _mainTabChanged(self, tabIndex):
def _mainStackChanged(self, stIndex):
"""Activated when the main window tab is changed.
"""
if tabIndex == self.idxTabEdit:
logger.verbose("Editor tab activated")
elif tabIndex == self.idxTabProj:
logger.verbose("Project outline tab activated")
if stIndex == self.idxEditorView:
logger.verbose("Editor View activated")
elif stIndex == self.idxOutlineView:
logger.verbose("Outline View activated")
if self.hasProject:
self.projView.refreshTree()
self.outlineView.refreshTree()
return
@pyqtSlot(int)
def _projTabsChanged(self, tabIndex):
def _projStackChanged(self, stIndex):
"""Activated when the project view tab is changed.
"""
sHandle = None
if tabIndex == self.idxTreeView:
logger.verbose("Project tree tab activated")
sHandle = self.treeView.getSelectedHandle()
if stIndex == self.idxProjView:
logger.verbose("Project Tree View activated")
sHandle = self.projView.getSelectedHandle()
elif tabIndex == self.idxNovelView:
logger.verbose("Novel tree tab activated")
elif stIndex == self.idxNovelView:
logger.verbose("Novel Tree View activated")
if self.hasProject:
self.novelView.refreshTree()
sHandle, _ = self.novelView.getSelectedHandle()
self.treeMeta.updateViewBox(sHandle)
self.itemDetails.updateViewBox(sHandle)
return
+2
View File
@@ -20,11 +20,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from novelwriter.tools.build import GuiBuildNovel
from novelwriter.tools.lipsum import GuiLipsum
from novelwriter.tools.projwizard import GuiProjectWizard
from novelwriter.tools.writingstats import GuiWritingStats
__all__ = [
"GuiBuildNovel",
"GuiLipsum",
"GuiProjectWizard",
"GuiWritingStats",
]
+70 -77
View File
@@ -65,17 +65,16 @@ class GuiBuildNovel(QDialog):
FMT_JSON_H = 8 # HTML5 wrapped in JSON
FMT_JSON_M = 9 # nW Markdown wrapped in JSON
def __init__(self, theParent):
QDialog.__init__(self, theParent)
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
logger.debug("Initialising GuiBuildNovel ...")
self.setObjectName("GuiBuildNovel")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theParent.theProject
self.optState = theParent.theProject.optState
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject
self.htmlText = [] # List of html documents
self.htmlStyle = [] # List of html styles
@@ -86,14 +85,15 @@ 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)
hS = self.theTheme.fontPixelSize
hS = self.mainTheme.fontPixelSize
wS = 2*hS
# Title Formats
@@ -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,29 +235,29 @@ 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("...")))
self.fontButton.setMaximumWidth(int(2.5*self.mainTheme.getTextWidth("...")))
self.fontButton.clicked.connect(self._selectFont)
self.textSize = QSpinBox(self)
self.textSize.setFixedWidth(6*self.theTheme.textNWidth)
self.textSize.setFixedWidth(6*self.mainTheme.textNWidth)
self.textSize.setMinimum(6)
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)
self.lineHeight.setFixedWidth(6*self.theTheme.textNWidth)
self.lineHeight.setFixedWidth(6*self.mainTheme.textNWidth)
self.lineHeight.setMinimum(0.8)
self.lineHeight.setMaximum(3.0)
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()
@@ -677,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))
@@ -711,13 +711,12 @@ class GuiBuildNovel(QDialog):
bldObj.initDocument()
# Make sure the project and document is up to date
self.theParent.treeView.flushTreeOrder()
self.theParent.saveDocument()
self.mainGui.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
@@ -761,7 +760,7 @@ class GuiBuildNovel(QDialog):
logger.debug("Built project in %.3f ms", 1000*(tEnd - tStart))
if bldObj.errData:
self.theParent.makeAlert([
self.mainGui.makeAlert([
self.tr("There were problems when building the project:")
] + bldObj.errData, nwAlert.ERROR)
@@ -782,16 +781,14 @@ 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 = not theItem.isFileType()
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
isNote = theItem.isNoteLayout()
isNovel = not isNone and not isNote
if isNone:
@@ -801,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):
@@ -1010,11 +1003,11 @@ class GuiBuildNovel(QDialog):
# ==============
if wSuccess:
self.theParent.makeAlert([
self.mainGui.makeAlert([
self.tr("{0} file successfully written to:").format(textFmt), savePath
], nwAlert.INFO)
else:
self.theParent.makeAlert(self.tr(
self.mainGui.makeAlert(self.tr(
"Failed to write {0} file. {1}"
).format(textFmt, errMsg), nwAlert.ERROR)
@@ -1161,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
@@ -1200,18 +1193,18 @@ class GuiBuildNovel(QDialog):
class GuiBuildNovelDocView(QTextBrowser):
def __init__(self, theParent, theProject):
QTextBrowser.__init__(self, theParent)
def __init__(self, mainGui, theProject):
QTextBrowser.__init__(self, mainGui)
logger.debug("Initialising GuiBuildNovelDocView ...")
self.mainConf = novelwriter.CONFIG
self.theProject = theProject
self.theParent = theParent
self.theTheme = theParent.theTheme
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.buildTime = 0
self.setMinimumWidth(40*self.theParent.theTheme.textNWidth)
self.setMinimumWidth(40*self.mainGui.mainTheme.textNWidth)
self.setOpenExternalLinks(False)
self.document().setDocumentMargin(self.mainConf.getTextMargin())
@@ -1245,9 +1238,9 @@ class GuiBuildNovelDocView(QTextBrowser):
lblPalette.setColor(QPalette.Foreground, lblPalette.toolTipText().color())
lblFont = self.font()
lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize)
lblFont.setPointSizeF(0.9*self.mainTheme.fontPointSize)
fPx = int(1.1*self.theTheme.fontPixelSize)
fPx = int(1.1*self.mainTheme.fontPixelSize)
self.theTitle = QLabel("", self)
self.theTitle.setIndent(0)
+142
View File
@@ -0,0 +1,142 @@
"""
novelWriter Lorem Ipsum Tool
==============================
Simple tool for inserting placeholder text in a document
File History:
Created: 2022-04-02 [1.7a0]
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 os
import random
import logging
import novelwriter
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QGridLayout, QHBoxLayout, QVBoxLayout, QLabel, QDialogButtonBox,
QSpinBox
)
from novelwriter.gui.custom import QSwitch
from novelwriter.common import readTextFile
logger = logging.getLogger(__name__)
class GuiLipsum(QDialog):
def __init__(self, mainGui):
QDialog.__init__(self, mainGui)
logger.debug("Initialising GuiLipsum ...")
self.setObjectName("GuiLipsum")
self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.setWindowTitle(self.tr("Insert Placeholder Text"))
self.innerBox = QHBoxLayout()
self.innerBox.setSpacing(self.mainConf.pxInt(16))
# Icon
nPx = self.mainConf.pxInt(64)
vSp = self.mainConf.pxInt(4)
self.docIcon = QLabel()
self.docIcon.setPixmap(self.mainTheme.getPixmap("proj_document", (nPx, nPx)))
self.leftBox = QVBoxLayout()
self.leftBox.setSpacing(vSp)
self.leftBox.addWidget(self.docIcon)
self.leftBox.addStretch(1)
self.innerBox.addLayout(self.leftBox)
# Form
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Insert Lorem Ipsum Text")))
self.paraLabel = QLabel(self.tr("Number of paragraphs"))
self.paraCount = QSpinBox()
self.paraCount.setMinimum(1)
self.paraCount.setMaximum(100)
self.paraCount.setValue(5)
self.randLabel = QLabel(self.tr("Randomise order"))
self.randSwitch = QSwitch()
self.formBox = QGridLayout()
self.formBox.addWidget(self.headLabel, 0, 0, 1, 2, Qt.AlignLeft)
self.formBox.addWidget(self.paraLabel, 1, 0, 1, 1, Qt.AlignLeft)
self.formBox.addWidget(self.paraCount, 1, 1, 1, 1, Qt.AlignRight)
self.formBox.addWidget(self.randLabel, 2, 0, 1, 1, Qt.AlignLeft)
self.formBox.addWidget(self.randSwitch, 2, 1, 1, 1, Qt.AlignRight)
self.formBox.setVerticalSpacing(vSp)
self.formBox.setRowStretch(3, 1)
self.innerBox.addLayout(self.formBox)
# Buttons
self.buttonBox = QDialogButtonBox()
self.buttonBox.rejected.connect(self._doClose)
self.btnClose = self.buttonBox.addButton(QDialogButtonBox.Close)
self.btnClose.setAutoDefault(False)
self.btnSave = self.buttonBox.addButton(self.tr("Insert"), QDialogButtonBox.ActionRole)
self.btnSave.clicked.connect(self._doInsert)
self.btnSave.setAutoDefault(False)
# Assemble
self.outerBox = QVBoxLayout()
self.outerBox.addLayout(self.innerBox)
self.outerBox.addWidget(self.buttonBox)
self.outerBox.setSpacing(self.mainConf.pxInt(16))
self.setLayout(self.outerBox)
logger.debug("GuiLipsum initialisation complete")
return
##
# Slots
##
def _doInsert(self):
"""Load the text and insert it in the open document.
"""
lipsumFile = os.path.join(self.mainConf.assetPath, "text", "lipsum.txt")
lipsumText = readTextFile(lipsumFile).splitlines()
if self.randSwitch.isChecked():
random.shuffle(lipsumText)
pCount = self.paraCount.value()
inText = "\n\n".join(lipsumText[0:pCount]) + "\n\n"
self.mainGui.docEditor.insertText(inText)
return
def _doClose(self):
"""Close the dialog window without doing anything.
"""
self.close()
return
# END Class GuiLipsum
+128 -99
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__)
@@ -50,17 +48,17 @@ PAGE_FINAL = 4
class GuiProjectWizard(QWizard):
def __init__(self, theParent):
QWizard.__init__(self, theParent)
def __init__(self, mainGui):
QWizard.__init__(self, mainGui)
logger.debug("Initialising GuiProjectWizard ...")
self.setObjectName("GuiProjectWizard")
self.mainConf = novelwriter.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.mainGui = mainGui
self.mainTheme = mainGui.mainTheme
self.sideImage = self.theTheme.loadDecoration(
self.sideImage = self.mainTheme.loadDecoration(
"wiz-back", None, self.mainConf.pxInt(370)
)
self.setWizardStyle(QWizard.ModernStyle)
@@ -94,14 +92,14 @@ class ProjWizardIntroPage(QWizardPage):
self.mainConf = novelwriter.CONFIG
self.theWizard = theWizard
self.theTheme = theWizard.theTheme
self.mainTheme = theWizard.mainTheme
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)
@@ -109,7 +107,7 @@ class ProjWizardIntroPage(QWizardPage):
"Peter Mitterhofer", "CC BY-SA 4.0"
))
lblFont = self.imgCredit.font()
lblFont.setPointSizeF(0.6*self.theTheme.fontPointSize)
lblFont.setPointSizeF(0.6*self.mainTheme.fontPointSize)
self.imgCredit.setFont(lblFont)
xW = self.mainConf.pxInt(300)
@@ -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)
@@ -164,7 +162,7 @@ class ProjWizardFolderPage(QWizardPage):
self.mainConf = novelwriter.CONFIG
self.theWizard = theWizard
self.theTheme = theWizard.theTheme
self.mainTheme = theWizard.mainTheme
self.setTitle(self.tr("Select Project Folder"))
self.theText = QLabel(self.tr(
@@ -182,9 +180,12 @@ class ProjWizardFolderPage(QWizardPage):
self.projPath.setPlaceholderText(self.tr("Required"))
self.browseButton = QPushButton("...")
self.browseButton.setMaximumWidth(int(2.5*self.theTheme.getTextWidth("...")))
self.browseButton.setMaximumWidth(int(2.5*self.mainTheme.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

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