Merge branch 'main' into python_39

This commit is contained in:
Veronica K. B. Olsen
2020-10-24 19:26:44 +02:00
149 changed files with 2525 additions and 1159 deletions
+2 -2
View File
@@ -25,5 +25,5 @@ jobs:
flake8 tests --count --select=E9,F63,F7,F82 --show-source --statistics flake8 tests --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Coding Style Violations - name: Coding Style Violations
run: | run: |
flake8 nw --count --max-line-length=99 --ignore E203,E221,E226,E241,E251,E261,E266,E302,E305 --show-source --statistics flake8 nw --count --max-line-length=99 --ignore E203,E221,E226,E228,E241,E251,E261,E266,E302,E305 --show-source --statistics
flake8 tests --count --max-line-length=99 --ignore E203,E221,E226,E241,E251,E261,E266,E302,E305 --show-source --statistics flake8 tests --count --max-line-length=99 --ignore E203,E221,E226,E228,E241,E251,E261,E266,E302,E305 --show-source --statistics
+2
View File
@@ -5,6 +5,8 @@
/deploy/ /deploy/
*.spec *.spec
*.egg-info *.egg-info
setup.iss
novelwriter.desktop
# Documentation # Documentation
/docs/build/ /docs/build/
+79
View File
@@ -1,5 +1,84 @@
# novelWriter ChangeLog # novelWriter ChangeLog
## Version 1.0 Beta 5 [2020-10-18]
**Important Notes**
* The minimal supported Python version is now 3.6. While novelWriter has worked fine in the post with versions as low as 3.4, neither 3.4 nor 3.5 is tested. They have also both reached end of life. There are a couple of good reasons to drop support for older versions. PR #470.
1. Python 3.6 introduces ordered dictionaries as the standard.
2. The format string decorator (`f""`) was added in 3.6, and is much less clunky in many parts of the code than the full `"".format()` syntax.
3. Especially 3.4 has limited support for `*var` expansion of iterables. These are used several places in the code.
**Bugfixes**
* Fixed a bug in the Build Novel Project tool where novelWriter would crash when trying to build the preview when running a version of the Qt library lower than 5.14. Issue #471, PR #472.
**User Interface**
* An option has been added in Preferences to hide horizontal or vertical scroll bars on the main GUI. These optons will hide scroll bars on the Project Tree, Document Editor, Document Viewer, Outline Tab and on the controls of the Build Novel Project tool. Scroll bars take up space, and as long as the project doesn't contain very long documents, scrolling with the mouse wheel is enough. The feature is of course entirely optional. PRs #468 and #469.
* It is no possible to enable scrolling past the end of the document with a new option in Preferences. Previously, the editor would just allow scrolling to the bottom of the document. The new option adds a margin to the bottom of the document itself that allows for scrolling past this point. This avoids having to type text at the bottom of the editor window. PRs #468 and #469.
* A new feature called "Typewriter Scrolling" has been added. It basically means that the editor window will try to keep the cursor at a given vertical position and instead scroll the document when the cursor moves to a new line, either by arrow keys or while typing. The position can also be defined in Preferences. The scroll bar uses an animation effect to perform the scrolling to avoid abrupt jumps in the editor window. PRs #468 and #474.
* The line counter in the Document Editor footer now shows the location in the document in terms of percentage. This is convenient for very large documents. PR #474.
* A "Follow Tag" option has been added to the Document Editor context menu. This option appears when right-clicking a tag value on a meta data line. PR #474.
* When applying a format from the format menu to a selection of multiple paragraphs (or lines), only the first paragraph (or line) receives the formatting. The editor doesn't allow markdown formatting to span multiple lines. Issue #451, PR #475.
* The syntax highlighter no longer uses the same colour to highlight strikethrough text as for emphasised text. The colour is intended to stand out, which makes little sense for such text. Instead, the highlighter uses the same colour as for comments. PR #476.
**Other Changes**
* Since support for Python < 3.6 has been dropped, it is now possible to use `f""` formatted strings in many more places in the source code where this is convenient. This has been implemented many places, but the code is still a mix of all three styles of formatting text. PR #478.
* Extensive changes have been made to the build and distribute tools. The `install.py` file has been dropped, and the features in it merged into a new file named `make.py`. The make file can now also build a setup installer for Windows. The `setup.py` file has been rewritten to a more standardised source layout, and all the setup configuration moved to the `setup.cfg` file. PRs #479 and #480.
## Version 1.0 Beta 4 [2020-10-11]
**Bugfixes**
* When the Trash folder didn't exist because nothing had been deleted yet, the lookup function for the Trash folder's handle returned `None`. That meant that any item with a parent handle `None` would be treated as a Trash folder in many parts of the code before the Trash folder was first used. This caused a few decision branches to make non-critical mistakes. In particular the project tree context menu. This issue has now been fixed with a new check function that takes this into account. PRs #452 and #453.
* If an older project was opened, one with a different project file layout than the more recent versions, a dialog asked whether the user wants the project updated or not. However, the function that moves files to their new location would actually start working before the dialog asked for permission. The permission would only be applied to the project XML file. Now, the check is still run before the dialog, but the action of moving files around are postponed to after the permission has been given and the project XML file parsed. PR #453.
* If there were multiple headings in a file, and the last paragraph did not end in a line break, the word counter for the individual sections would miss the last paragraph of the last section due to an indexing error. This has now been fixed. PR #453.
* The last cursor position of a document in the editor would only be saved if the document had been altered. It is now also saved in the cases where the user makes no changes. PR #460.
* When using an aspell dictionary for spell checking, words containing a hyphen would be highlighted as misspelled. This is not the case for hunspell dictionaries. The hyphen is now taken into account when splitting sentences into words for spell check highlighting. PR #462.
* Some of the file dialogs would fail with a non-critical error when the cancel button was clicked. The cancel is now captured consistently in all instances where such a dialog is used, and the calling function exited properly. PR #463.
**User Interface**
* Some minor changes to the text formatting on the Recent Projects dialog. PR #452.
* The Build Novel Project tool has been improved. The settings side panel is now scrollable, and the document and settings panel now have a movable splitter between them. This gives more flexibility to the sizes of the various parts. PR #459.
* A new option to replace tabs with spaces has been added to the Build Novel Project tool. Previously, they were always replaces for HTML output. However, converting them to the HTML code for a tab is actually convenient for later import into for instance Libre Office, which then converts them back to regular tabs. Issue #458, PR #459.
* Non-breaking spaces have been removed from the HTML conversion of keywords and tags. Issue #458, PR #459.
* An upper limit of how large a document the Build Novel Project tool can view has been set. It is 10 megabytes of generated HTML. The tool will still build larger documents, but they aren't displayed. This also limits which options are available in the "Save As" list for such large documents. Only native novelWriter exports are supported in such cases. The limit is an order of magnitude larger than a typical long novel. PR #460.
* The language indicator in the status bar now has a tooltip stating what tool and spell check dictionary provider is being used. PR #462.
* All representations of integers, mostly word counts, are now presented in the same way. They should all use a thousand separator representation defined by the local language settings. PR #464.
* Many parts of the GUI have had a spin/wait cursor added for processes that may take a while and will block the GUI in the meantime. PRs #460, #463 and #464.
* A line counter has been added to the footer of the document editor next to the word counter. It makes it easier to compare the position in the document when also accessing it in an external editor. PR #466.
**Improvements for macOS**
* The native macOS menu bar now pulls the correct menu entries into the first menu column. PR #463.
* The application name in the main menu would state Python instead of novelWriter. As long as the `pyobjc` package is installed, the label will now correctly state novelWriter. PR #463.
* Install and run instructions for macOS have been added to the main README. PR #463.
**Editor Performance**
* The syntax highlighter now remembers what type of line every line in the document is. This means that certain types of lines can be re-highlighted without having to process the entire document again. This is particularly useful for refreshing the highlighting of keywords and tags after the index has been rebuilt. PR #460.
* On a few occasions, the entire document in the editor would be reloaded in order to update the layout and formatting. This is not only slow for big documents, it also resets the undo stack. Instead, the entire document is "marked as dirty" to force the Qt library to update the layout, which is much faster. PR #460.
* For very large documents (in the megabyte range), the repositioning of the cursor when the document was opened would sometimes interfere with the rendering of the document itself. This could potentially cause the editor to hang for up to a couple of minutes. Instead, the repositioning of the cursor is now postponed until the document layout size has reached past the character where the cursor is to be moved. This mode is only used for documents larger than 50 kilobytes. PR #460.
* The document editor will no longer accept single documents larger than 5 megabytes. This restriction has also been applied to the Build Novel Project tool. For reference, a typical long novel is less than 1 megabyte in size. PR #460.
**Other Changes**
* The command line switches `--quiet` and `--logfile=` have been removed. They were intended for testing, but have never been used. The default mode of only printing warnings and errors is quiet enough, and logging to file shouldn't be necessary for a GUI application. PR #453.
* A number of if-statements and conditions in the code that were intended to alter behaviour when running tests, mostly to stop modal dialogs from blocking the main thread, have been removed. These types of changes to the program flow when running tests have now been reduced to a minimum, and modifications instead handled with pytest monkeypatches. PR #453.
* The `QtSvg` package is no longer in use by novelWriter. The internal dependency check has been removed. PR #457.
* It is no longer possible to set the user's home folder as the root directory of a project. The home folder is the default lookup folder in many cases, so it's easy to do by mistake. PR #457.
* The background word counter has been rewritten to run on an application wide thread pool. This is a more appropriate way of running background tasks. PR #462.
**Test Suite**
* Major additions to the test suite, taking the test coverage to 91%. PR #453.
* Test coverage for Linux (Ubuntu) for Python versions 3.6, 3.7, and 3.8 are now separate jobs. In addition, Windows with Python 3.8 and macOS with Python 3.8 is also tested. All OSes are piped into test coverage, and they all have status badges. PRs #453 and #454.
## Version 1.0 Beta 3 [2020-09-20] ## Version 1.0 Beta 3 [2020-09-20]
**Bugfixes** **Bugfixes**
+1 -1
View File
@@ -1,4 +1,4 @@
include LICENSE.md include LICENSE.md
recursive-include assets * recursive-include setup *
recursive-include nw/assets * recursive-include nw/assets *
recursive-include sample *.nwx *.nwd recursive-include sample *.nwx *.nwd
+220 -183
View File
@@ -12,11 +12,11 @@
[![pypi](https://img.shields.io/pypi/v/novelwriter)](https://pypi.org/project/novelWriter) [![pypi](https://img.shields.io/pypi/v/novelwriter)](https://pypi.org/project/novelWriter)
[![python](https://img.shields.io/pypi/pyversions/novelwriter)](https://pypi.org/project/novelWriter) [![python](https://img.shields.io/pypi/pyversions/novelwriter)](https://pypi.org/project/novelWriter)
<img align="left" style="margin: 0 16px 4px 0;" src="https://raw.githubusercontent.com/vkbo/novelWriter/main/assets/icons/96x96/novelwriter.png"> <img align="left" style="margin: 0 16px 4px 0;" src="https://raw.githubusercontent.com/vkbo/novelWriter/main/setup/icons/scaled/icon-novelwriter-96.png">
novelWriter is a Markdown-like text editor designed for writing novels and larger projects of many novelWriter is a Markdown-like text editor designed for writing novels and larger projects of many
smaller plain text documents. It uses its own flavour of Markdown that supports a meta data syntax smaller plain text documents. It uses its own flavour of Markdown that supports a meta data syntax
for comments, synopsis and cross-referencing between files. It's designed to be a simple text editor for comments, synopsis, and cross-referencing between files. It's designed to be a simple text editor
that allows for easy organisation of text files and notes, built on plain text files for that allows for easy organisation of text files and notes, built on plain text files for
robustness. robustness.
@@ -29,19 +29,6 @@ The full documentation is available at [novelwriter.readthedocs.io](https://nove
The contributing guide is available in [CONTRIBUTING](CONTRIBUTING.md). The contributing guide is available in [CONTRIBUTING](CONTRIBUTING.md).
### Note on the Default Branch
The default branch on this repository switched to `main` on 6. August 2020. If you are running
novelWriter from a git clone, you need to clone the repository again.
Alternatively, you can run the following to get back on the new default branch:
```bash
git remote update
git checkout -t origin/main
```
### Development Status ### Development Status
The application is still under initial development, but all core features have now been added. The The application is still under initial development, but all core features have now been added. The
@@ -49,15 +36,230 @@ core functionality has been in place for a while, and novelWriter is being used
by the author and collaborators. by the author and collaborators.
No new major features will be added at this time, until the application is stable. Until then, No new major features will be added at this time, until the application is stable. Until then,
novelWriter is in a _beta_ state. Please report any issues you may encounter in the repository issue novelWriter is in a _beta_ state. Please report any issues you encounter via the repository's issue
tracker. tracker.
You should be able to use novelWriter for real projects, but as with all software, please make You should be able to use novelWriter for real projects, but as with all software, please make
regular backups of your data. There is a built in backup feature that can pack the entire project regular backups of your data. There is a built in backup feature that can pack the entire project
into a zip file on close. Please check the documentation for further details. into a zip file each time the main window or the project is closed. Please check the documentation
for further details.
## License ## Implementation
The application is written in Python 3 using Qt5 via PyQt5. It is developed on Linux, but it should
in principle work fine on other operating systems as well as long as dependencies are met. The unit
tests are run on the latest versions of Ubuntu Linux, Windows Server and macOS.
# Installing and Running
novelWriter is available on [pypi.org](https://pypi.org/project/novelWriter/), and can be installed with:
```bash
pip install novelwriter
```
To upgrade an existing installation, use:
```bash
pip install --upgrade novelwriter
```
Dependencies are installed automatically, but can generally be installed with:
```bash
pip install -r requirements.txt
```
Below are some brief instructions on how to get started on different operating systems.
## Linux
Either download the source, or install with pip.
If you run from source, install the dependencies via pip, or directly from the OS repo.
There are very few dependencies, and they should be available in the standard repo.
The Python packages needed are `pyqt5`, `lxml` and `pyenchant`.
### Installing from Source
You can also install novelWriter from source with:
```bash
python3 setup.py sample
sudo python3 setup.py install
sudo python3 setup.py launcher
```
The last line will install the application icons and set up a launcher for novelWriter.
The method uses hardcoded paths, so it may or may not work for your Linux distro.
If you have any issues, please submit a ticket so the script can be tuned.
The script may prompt you to choose which executable to configure if it finds more than one.
### Running from Source
If you want to run directly from the source, the application can be started with:
```bash
./novelWriter.py
```
You can also create a launcher for running directly from source with:
```bash
python3 setup.py xdg-install
```
This will install the launcher and icons for the current user.
To install them system-wide, run the above command with `sudo` or as root.
For more install options, see [Build and Install novelWriter](setup/README.md).
## macOS
These instructions assume you're using brew, and have Python and pip set up.
If not, see the [brew docs](https://docs.brew.sh/Homebrew-and-Python) for help.
Main requirements are installed via the requirements file.
You also need to install the `pyobjc` package on macOS, so you must run:
```bash
pip3 install --user -r requirements.txt
pip3 install --user pyobjc
```
For spell checking you may also need to install the enchant package.
It comes with a lot of default dictionaries.
```bash
brew install enchant
```
## Windows
On Windows, you may first need to install Python.
See the [python.org](https://www.python.org/) website for download packages.
It is recommended that you install the latest version of Python 3.8.
To install dependencies, run:
```bash
pip install --user -r requirements.txt
```
**Note:** On Windows, make sure Python3 is in your PATH if you want to launch novelWriter from
command line. You can also right click the `novelWriter.py` file, create a shortcut, then right
click again, select "Properties" and change the target to your python executable and
`novelWriter.py`.
It should look something like this:
```
C:\...\AppData\Local\Programs\Python\Python38\python.exe novelWriter.py
```
You can also run the `make.py` script to generate a single executable, or an installer.
See [Build and Install novelWriter](setup/README.md) for more details.
## Package Versions
Exporting to Markdown requires PyQt/Qt 5.14. There are no known minimum for `lxml`, but the code
was originally written with 4.2. The optional spell check library must be at least 3.0.0 to work
with Windows 64 bit systems. On Linux, 2.0.0 also works fine.
If no external spell checking tool is installed, novelWriter will use a basic spell checker based on
standard Python package `difflib`. Currently, only English dictionaries are available for this spell
checker, but more can be added to the `nw/assets/dict` folder. See the [README](nw/assets/dict/README.md)
file in that folder for how to generate more dictionaries. Note that the difflib-based option is
both slow and limited.
## Debugging
If you need to debug novelWriter, you must run it from command line.
It takes a few parameters, which can be listed with the switch `--help`.
The `--info`, `--debug` or `--verbose` flags are particularly useful for increasing logging output
for debugging.
# Key Features
Some features of novelWriter are listed below. Consult the documentation for more information.
### Markdown Flavour
novelWriter is _not_ a full-feature Markdown editor. It allows for a minimal set of formatting
needed for writing text documents for novels. These are currently limited to:
* Headings level 1 to 4 using the `#` syntax only.
* Emphasised and strong text. These are rendered as italicised and bold.
* Strikethrough text.
* Hard line breaks using two or more spaces at the end of a line.
That is it. Features not supported in the editor are also not exported when using the export tool.
In addition, novelWriter adds the following, which is otherwise not supported by Markdown:
* A line starting with `%` is treated as a comment and not rendered on exports unless requested.
Comments do not count towards the word count. If the first word of the comment is `synopsis:`, the
comment is indexed and treated as the synopsis for the section of text under the same header.
These synopsis comments can be used to build an outline and exported to external documents.
* A set of meta data keyword/values starting with the character `@`. This is used for tagging
and inter-linking documents, and can be used to generate a project outline.
* Non-breaking spaces are supported as long as your system is using at least Qt 5.9. For earlier
version, non-breaking spaces are converted to normal spaces when saving the document. This is done
by the Qt library.
* Thin spaces are also supported, as well as non-breaking thin spaces, with the same library version
restriction as above.
* Tabs can be used in the text, and should be properly aligned. The width of a tab in pixels can be
changed in Preferences. Note that for the HTML format, most browsers will treat a tab as a space,
so it may not show up like expected. If you import the HTML file to Libre Office, for instance,
they should appear as expected.
The core export format of novelWriter is HTML5. You can also export the entire project as a single
novelWriter Markdown-flavour document. In addition, other exports to Open Document, PDF, and plain
text is offered through the Qt library, although with limitations to formatting.
The HTML format is well suited for file conversion tools and import into other text editors.
### Colour Themes
The editor has syntax highlighting for the features it supports, and includes a set of different
syntax highlighting themes. The GUI also has an optional dark theme in addition to the default
system theme.
New themes can easily be added to the `nw/assets/themes` folder. Have a look in the existing folders
for examples of how to define the colours.
### Easy Organising of Project Files
The structure of the project is shown on the left hand side of the main GUI. Project files are
organised into root folders, indicating what class of file they are. The most important root folder
is the Novel folder, which contains all of the files that makes up the finished novel. Each root
folder can have subfolders. Folders have no impact on the final project structure, they are purely
tools for organising the files in whatever way the user needs.
The editor supports four levels of headings, which determines what level the following text belongs
to. Headings of level one signify a book or partition title. Headings of level two signify the start
of a new chapter. Headings of level three signify the start of a new scene. Headings of level four
can be used internally in each scene to separate sections.
Each novel file can be assigned a layout format, which shows up as a flag next to the item in the
project tree. These are mostly to help the user track what they contain, but they also have some
impact on the format of the exported document. See the documentation for further details.
#### Project Notes
Supporting note files can be added for the story plot, characters, locations, story timeline, etc.
These have their separate root folders. These are optional files.
### Visualisation of Story Elements
The different notes can be assigned tags, which other files can refer back to using the `@` meta
keywords. This information can be used to display an outline of the story, showing where each scene
connects to the plot, and which characters, etc. occur in them. In addition, the tags themselves are
clickable in the document view pane, and control-clickable in the editor. They make it possible to
quickly navigate between the documents while editing.
## Licenses
This is Open Source software, and novelWriter is licensed under GPLv3. See the This is Open Source software, and novelWriter is licensed under GPLv3. See the
[GNU General Public License website](https://www.gnu.org/licenses/gpl-3.0.en.html) for more details, [GNU General Public License website](https://www.gnu.org/licenses/gpl-3.0.en.html) for more details,
@@ -79,171 +281,6 @@ Bundled assets have the following licenses:
main repo is available at [sdras/night-owl-vscode-theme](https://github.com/sdras/night-owl-vscode-theme). main repo is available at [sdras/night-owl-vscode-theme](https://github.com/sdras/night-owl-vscode-theme).
## Markdown Flavour
novelWriter is _not_ a full-feature Markdown editor. It allows for a minimal set of formatting
needed for writing text documents for novels. These are currently limited to:
* Headings level 1 to 4 using the `#` syntax only.
* Emphasised and strong text. These are rendered as italicised and bold.
* Strikethrough text.
* Hard line breaks using two or more spaces at the end of a line.
That is it. Features not supported in the editor are also not exported when using the export tool.
In addition, novelWriter adds the following, which is otherwise not supported by Markdown:
* A line starting with `%` is treated as a comment and not rendered on exports unless requested.
Comments do not count towards the word count. If the first word of the comment is `synopsis:`, the
comment is indexed and treated as the synopsis for the following section of text. These synopsis
comments can be used to build an outline and exported to external documents.
* A set of meta data keyword/value sets starting with the character `@`. This is used for tagging
and inter-linking documents.
* Non-breaking spaces are supported as long as your system is using at least Qt 5.9. For earlier
version, non-breaking spaces are converted to normal spaces when saving the document. This is done
by the Qt library.
* Thin spaces are also supported, as well as non-breaking thin spaces.
* Tabs can be used in the text, and should be properly aligned. The width of a tab in pixels can be
changed in Preferences. Note that tabs are exported as-is, also to HTML format. However, most
browsers will treat a tab as a space, so it may not show up like expected if you view the exported
HTML file.
The core export format of novelWriter is HTML5. You can also export the entire project as a single
novelWriter Markdown-flavour document. In addition, other exports to Open Document, PDF, and plain
text is offered through the Qt library, although with limitations to formatting.
## Implementation
The application is written in Python3 using Qt5 via PyQt5. It is developed on Linux, but it should
in principle work fine on other operating systems as well, as long as dependencies are met. It is
regularly tested on Windows 10.
The application can be started from the source folder with one of the commands, depending on your
Python configuration:
```bash
./novelWriter.py
python novelWriter.py
python3 novelWriter.py
```
It also takes a few parameters for debugging and such, which can be listed with the switch `--help`.
In the root assets folder there are icons and scripts and a template for setting up a launcher on
Gnome desktops. You may need to modify those scripts slightly, but as they are, they work on Debian
and Ubuntu. For other operating systems, please consult your operating system documentation for how
to make those. Feel free to submit more if you are able to make them.
## Package Dependencies
It is recommended that novelWriter runs with Qt 5.10 or later, and Python 3.6 or later. Running with
Qt as low as 5.2.1 and Python 3.4.3 has been tested, and worked in the past, but there are no
guarantees that this will keep working as these are not a part of the test builds.
For the apt package manager on Debian/Ubuntu systems, the following Python3 packages are needed:
* `python3-pyqt5` for the GUI
* `python3-lxml` for writing project files
These are optional, but recommended:
* `python3-enchant` for better spell checking
Alternatively, the packages can be installed with `pip` by running
```bash
pip install -r requirements.txt
```
in the application folder.
You can also do them one at a time, skipping the ones you don't need:
```bash
pip install pyqt5
pip install lxml
pip install pyenchant
```
PyQt/Qt should be at least 5.3, but ideally 5.10 or higher for nearly all features to work.
Exporting to Markdown requires PyQt/Qt 5.14. There are no known minimum for `lxml`, but the code
was originally written with 4.2. The optional spell check library must be at least 3.0.0 to work
with Windows 64 bit systems. On Linux, 2.0.0 also works fine.
If no external spell checking tool is installed, novelWriter will use a basic spell checker based on
standard Python package `difflib`. Currently, only English dictionaries are available for this spell
checker, but more can be added to the `nw/assets/dict` folder. See the [README](nw/assets/dict/README.md)
file in that folder for how to generate more dictionaries. Note that the difflib-based option is
both slow and limited.
Note: On Windows, make sure Python3 is in your PATH if you want to launch novelWriter from command
line. You can also right click the `novelWriter.py` file, create a shortcut, then right click again,
select "Properties" and change the target to your python executable and `novelWriter.py`.
It should look something like this:
```
C:\...\AppData\Local\Programs\Python\Python38\python.exe novelWriter.py
```
## Key Features
Some features of novelWriter are listed below. Consult the documentation for more information.
### Colour Themes
The editor has syntax highlighting for the features it supports, and includes a set of different
syntax highlighting themes. The GUI also has an optional dark theme in addition to the default
system theme.
New themes can easily be added to the `nw/assets/themes` folder. Have a look in the existing folders
for examples of how to define the colours.
### Auto-Saving and Document Stats
Open documents and the project file itself is saved regularly on a timer. The status of this is
indicated by two indicators on the right hand side of the status bar. Latest project word count is
shown next to these indicators in the status bar. The counts are updated regularly, but not
as-you-type.
The word count for documents is presented in a footer in the document editor itself. Both project
and document word counters will also show how many words you've added in the current writing
session.
### Easy Organising of Project Files
The structure of the project is shown on the left hand side of the main GUI. Project files are
organised into root folders, indicating what class of file they are. The most important root folder
is the Novel folder, which contains all of the files that makes up the finished novel. Each root
folder can have subfolders. Folders have no impact on the project structure, they are purely tools
for organising the files in whatever way the user needs.
The editor supports four levels of headings, which determines what level the following text belongs
to. Headings of level one signify a book or partition title. Headings of level two signify the start
of a new chapter. Headings of level three signify the start of a new scene. Headings of level four
can be used internally in each scene to separate sections.
Each novel file can be assigned a layout format, which shows up as a flag next to the item in the
project tree. These are mostly to help the user see what they contain, but they also have some
impact on the format of the exported document. See the documentation for further details.
#### Project Notes
Supporting note files can be added for the story plot, characters, locations, story timeline, etc.
These have their separate root folders. These are optional files.
### Visualisation of Story Elements
The different notes can be assigned tags, which other files can refer back to using special meta
keywords. This information can be used to display an outline of the story, showing where each scene
connects to the plot, and which characters, etc. occur in them. In addition, the tags themselves are
clickable in the document view pane, and control-clickable in the editor. They make it possible to
quickly navigate between the documents while editing.
## Screenshot ## Screenshot
**novelWriter with default system theme:** **novelWriter with default system theme:**
-49
View File
@@ -1,49 +0,0 @@
#!/bin/bash
cd ..
EXEC=$(pwd)/novelWriter.py
EXEC=$(echo $EXEC | sed 's_/_\\/_g')
sed "s/%%exec%%/$EXEC/g" assets/novelwriter.desktop > /usr/share/applications/novelwriter.desktop
if [ ! -d /usr/share/icons/hicolor/24x24/apps ]; then
mkdir -pv /usr/share/icons/hicolor/24x24/apps
fi
if [ ! -d /usr/share/icons/hicolor/48x48/apps ]; then
mkdir -pv /usr/share/icons/hicolor/48x48/apps
fi
if [ ! -d /usr/share/icons/hicolor/96x96/apps ]; then
mkdir -pv /usr/share/icons/hicolor/96x96/apps
fi
if [ ! -d /usr/share/icons/hicolor/256x256/apps ]; then
mkdir -pv /usr/share/icons/hicolor/256x256/apps
fi
if [ ! -d /usr/share/icons/hicolor/512x512/apps ]; then
mkdir -pv /usr/share/icons/hicolor/512x512/apps
fi
if [ ! -d /usr/share/icons/hicolor/scalable/apps ]; then
mkdir -pv /usr/share/icons/hicolor/scalable/apps
fi
if [ ! -d /usr/share/icons/hicolor/scalable/mimetypes ]; then
mkdir -pv /usr/share/icons/hicolor/scalable/mimetypes
fi
cp -v assets/icons/24x24/novelwriter.png /usr/share/icons/hicolor/24x24/apps/
cp -v assets/icons/48x48/novelwriter.png /usr/share/icons/hicolor/48x48/apps/
cp -v assets/icons/96x96/novelwriter.png /usr/share/icons/hicolor/96x96/apps/
cp -v assets/icons/256x256/novelwriter.png /usr/share/icons/hicolor/256x256/apps/
cp -v assets/icons/512x512/novelwriter.png /usr/share/icons/hicolor/512x512/apps/
cp -v assets/icons/novelwriter.svg /usr/share/icons/hicolor/scalable/apps/
cp -v assets/icons/x-novelwriter-project.svg /usr/share/icons/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg
cp -v assets/mime/x-novelwriter-project.xml /usr/share/mime/packages/
update-mime-database /usr/share/mime/
update-icon-caches /usr/share/icons/hicolor/24x24/apps/
update-icon-caches /usr/share/icons/hicolor/48x48/apps/
update-icon-caches /usr/share/icons/hicolor/96x96/apps/
update-icon-caches /usr/share/icons/hicolor/256x256/apps/
update-icon-caches /usr/share/icons/hicolor/512x512/apps/
update-icon-caches /usr/share/icons/hicolor/1024x1024/apps/
update-icon-caches /usr/share/icons/hicolor/scalable/apps/
update-icon-caches /usr/share/icons/hicolor/scalable/mimetypes/
-42
View File
@@ -1,42 +0,0 @@
#!/bin/bash
cd ..
EXEC=$(pwd)/novelWriter.py
EXEC=$(echo $EXEC | sed 's_/_\\/_g')
sed "s/%%exec%%/$EXEC/g" assets/novelwriter.desktop > /usr/share/applications/novelwriter.desktop
if [ ! -d /usr/share/icons/hicolor/24x24/apps ]; then
mkdir -pv /usr/share/icons/hicolor/24x24/apps
fi
if [ ! -d /usr/share/icons/hicolor/48x48/apps ]; then
mkdir -pv /usr/share/icons/hicolor/48x48/apps
fi
if [ ! -d /usr/share/icons/hicolor/96x96/apps ]; then
mkdir -pv /usr/share/icons/hicolor/96x96/apps
fi
if [ ! -d /usr/share/icons/hicolor/256x256/apps ]; then
mkdir -pv /usr/share/icons/hicolor/256x256/apps
fi
if [ ! -d /usr/share/icons/hicolor/512x512/apps ]; then
mkdir -pv /usr/share/icons/hicolor/512x512/apps
fi
if [ ! -d /usr/share/icons/hicolor/scalable/apps ]; then
mkdir -pv /usr/share/icons/hicolor/scalable/apps
fi
if [ ! -d /usr/share/icons/hicolor/scalable/mimetypes ]; then
mkdir -pv /usr/share/icons/hicolor/scalable/mimetypes
fi
cp -v assets/icons/24x24/novelwriter.png /usr/share/icons/hicolor/24x24/apps/
cp -v assets/icons/48x48/novelwriter.png /usr/share/icons/hicolor/48x48/apps/
cp -v assets/icons/96x96/novelwriter.png /usr/share/icons/hicolor/96x96/apps/
cp -v assets/icons/256x256/novelwriter.png /usr/share/icons/hicolor/256x256/apps/
cp -v assets/icons/512x512/novelwriter.png /usr/share/icons/hicolor/512x512/apps/
cp -v assets/icons/novelwriter.svg /usr/share/icons/hicolor/scalable/apps/
cp -v assets/icons/x-novelwriter-project.svg /usr/share/icons/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg
cp -v assets/mime/x-novelwriter-project.xml /usr/share/mime/packages/
update-mime-database /usr/share/mime/
update-icon-caches /usr/share/icons/*
+1 -2
View File
@@ -12,7 +12,6 @@
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
# #
# import os
# import sys # import sys
# sys.path.insert(0, os.path.abspath(".")) # sys.path.insert(0, os.path.abspath("."))
import os import os
@@ -28,7 +27,7 @@ author = "Veronica Berglyd Olsen"
# The short X.Y version # The short X.Y version
version = "1.0" version = "1.0"
# The full version, including alpha/beta/rc tags # The full version, including alpha/beta/rc tags
release = "1.0-beta3" release = "1.0-beta5"
# -- General configuration --------------------------------------------------- # -- General configuration ---------------------------------------------------
+2 -2
View File
@@ -46,7 +46,7 @@ than the document editor itself are hidden away.
The colour scheme of the user interface defaults to that of the host operating system. In addition, The colour scheme of the user interface defaults to that of the host operating system. In addition,
a dark theme is provided, and can be enabled in :guilabel:`Preferences` from the :guilabel:`Tools` a dark theme is provided, and can be enabled in :guilabel:`Preferences` from the :guilabel:`Tools`
menu. A number of syntax highlighting themes are also available in :guilabel:`Preferences`. A set of menu. A number of syntax highlighting themes are also available in :guilabel:`Preferences`. A set of
icon themes in colour and greyscale are also offered. The icons are based on the Typicon_ icon set icon themes in colour and greyscale are also offered. The icons are based on the Typicons_ icon set
designed by Stephen Hutchings. designed by Stephen Hutchings.
The main window is split in two, or optionally three, panels. The left-most contains the project The main window is split in two, or optionally three, panels. The left-most contains the project
@@ -58,7 +58,7 @@ entire novel structure can be displayed, with all the tags and references listed
you structure your novel project files, this outline can be quite different than your project tree. you structure your novel project files, this outline can be quite different than your project tree.
Your project tree lists files, your Outline tree lists the structure of the novel itself. Your project tree lists files, your Outline tree lists the structure of the novel itself.
.. _Typicon: https://github.com/stephenhutchings/typicons.font .. _Typicons: https://github.com/stephenhutchings/typicons.font
.. _a_intro_project: .. _a_intro_project:
+1 -1
View File
@@ -140,7 +140,7 @@ encountered. To list all options, run:
python novelWriter.py --help python novelWriter.py --help
There are also a couple of install scripts in the assets folder which will assist in setting up a There are also a couple of install scripts in the setup folder which will assist in setting up a
launch icon and the novelWriter project file mimetype for Gnome desktops on Linux. Currently, launch icon and the novelWriter project file mimetype for Gnome desktops on Linux. Currently,
there's one script for Debian and one for Ubuntu. there's one script for Debian and one for Ubuntu.
-84
View File
@@ -1,84 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import getopt
import subprocess
# Defaults
buildWindowed = True
# Parse Options
shortOpt = "hd"
longOpt = [
"help",
"debug",
]
helpMsg = (
"\n"
"novelWriter Install Script\n"
"\n"
"Usage:\n"
" -h, --help Print this message.\n"
" -d, --debug Build novelWriter for debugging. To debug novelwriter after build,\n"
" run it from command line with the debug options. Please check the\n"
" novelWriter --help output for details.\n"
)
try:
inOpts, inArgs = getopt.getopt(sys.argv[1:], shortOpt, longOpt)
except getopt.GetoptError:
print(helpMsg)
sys.exit(2)
for inOpt, inArg in inOpts:
if inOpt in ("-h", "--help"):
print(helpMsg)
sys.exit()
elif inOpt in ("-d", "--debug"):
buildWindowed = False
# Run pip
packList = ["pyinstaller"]
with open("requirements.txt", mode="r") as reqFile:
for reqPack in reqFile:
if len(reqPack.strip()) > 0:
packList.append(reqPack)
for packName in packList:
print("Installing package dependency: %s" % packName)
try:
subprocess.call([sys.executable, "-m", "pip", "install", packName])
except Exception as e:
print("Failed with error:")
print(str(e))
# Run pyinstaller
if sys.platform.startswith("win32"):
dotDot = ";"
else:
dotDot = ":"
instOpt = [
"--name=novelWriter",
"--clean",
"--onefile",
"--add-data=%s%s%s" % (os.path.join("nw", "assets"), dotDot, "assets"),
"--icon=%s" % os.path.join("nw", "assets", "icons", "novelwriter.ico"),
]
if buildWindowed:
instOpt.append("--windowed")
instOpt.append("novelWriter.py")
import PyInstaller.__main__ # noqa: F401
PyInstaller.__main__.run(instOpt)
print("")
print("##################")
print(" Build Finished")
print("##################")
print("")
print("If everything went well, the novelWriter executable should be in the folder named 'dist'")
print("")
Executable
+333
View File
@@ -0,0 +1,333 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
This make script is intended for building distributable packages of
novelWriter. These are either:
* A single file executable named dist/novelWriter(.exe). This is a
quite slow option, and the file is fairly big.
* A single directory named dist/novelWriter with a novelWriter(.exe),
and all dependecies included.
* The latter can be combined with a build stage of a setup.exe file if
on Windows. This requires Inno Setup to be installed and in path.
In addition, providing the pip otion will cause the script to try to
install all dependencies needed for runing the build, and for running
novelWriter itself.
"""
import os
import sys
import shutil
import subprocess
OS_NONE = 0
OS_LINUX = 1
OS_WIN = 2
OS_DARWIN = 3
# =============================================================================================== #
# Package Installer
# =============================================================================================== #
def installPackages(hostOS):
"""Install package dependencies both for this script and for running
novelWriter itself.
"""
print("")
print("Installing Dependencies")
print("#######################")
print("")
installQueue = ["pip", "pyinstaller", "-r requirements.txt"]
if hostOS == OS_DARWIN:
installQueue.append("pyobjc")
pyCmd = [sys.executable, "-m"]
pipCmd = ["pip", "install", "--user", "--upgrade"]
for stepCmd in installQueue:
pkgCmd = stepCmd.split(" ")
try:
subprocess.call(pyCmd + pipCmd + pkgCmd)
except Exception as e:
print("Failed with error:")
print(str(e))
sys.exit(1)
return
# =============================================================================================== #
# Run PyInstaller on Package
# =============================================================================================== #
def freezePackage(buildWindowed, oneFile, makeSetup, hostOS):
"""Run PyInstaller to freeze the packages. This assumes all
dependencies are already in place.
"""
import PyInstaller.__main__ # noqa: E402
print("")
print("Running PyInstaller")
print("###################")
print("")
if hostOS == OS_WIN:
dotDot = ";"
else:
dotDot = ":"
sys.modules["FixTk"] = None
instOpt = [
"--name=novelWriter",
"--clean",
"--add-data=%s%s%s" % (os.path.join("nw", "assets"), dotDot, "assets"),
"--icon=%s" % os.path.join("nw", "assets", "icons", "novelwriter.ico"),
"--exclude-module=PyQt5.QtQml",
"--exclude-module=PyQt5.QtBluetooth",
"--exclude-module=PyQt5.QtDBus",
"--exclude-module=PyQt5.QtMultimedia",
"--exclude-module=PyQt5.QtMultimediaWidgets",
"--exclude-module=PyQt5.QtNetwork",
"--exclude-module=PyQt5.QtNetworkAuth",
"--exclude-module=PyQt5.QtNfc",
"--exclude-module=PyQt5.QtQuick",
"--exclude-module=PyQt5.QtQuickWidgets",
"--exclude-module=PyQt5.QtRemoteObjects",
"--exclude-module=PyQt5.QtSensors",
"--exclude-module=PyQt5.QtSerialPort",
"--exclude-module=PyQt5.QtSql",
"--exclude-module=FixTk",
"--exclude-module=tcl",
"--exclude-module=tk",
"--exclude-module=_tkinter",
"--exclude-module=tkinter",
"--exclude-module=Tkinter",
]
if buildWindowed:
instOpt.append("--windowed")
if oneFile and not makeSetup:
instOpt.append("--onefile")
else:
instOpt.append("--onedir")
instOpt.append("novelWriter.py")
# Make sample.zip first
try:
subprocess.call([sys.executable, "setup.py", "sample"])
except Exception as e:
print("Failed with error:")
print(str(e))
sys.exit(1)
PyInstaller.__main__.run(instOpt)
if not oneFile:
# These files are not needed, and take up a fair bit of space.
delFiles = []
if hostOS == OS_WIN:
delFiles = [
"Qt5DBus.dll",
"Qt5Network.dll",
"Qt5Qml.dll",
"Qt5QmlModels.dll",
"Qt5Quick.dll",
"Qt5Quick3D.dll",
"Qt5Quick3DAssetImport.dll",
"Qt5Quick3DRender.dll",
"Qt5Quick3DRuntimeRender.dll",
"Qt5Quick3DUtils.dll",
"Qt5Sql.dll"
]
elif hostOS == OS_LINUX:
delFiles = [
"libQt5DBus.so.5",
"libQt5Network.so.5",
"libQt5Qml.so.5",
"libQt5QmlModels.so.5",
"libQt5Quick.so.5",
"libQt5Quick3D.so.5",
"libQt5Quick3DAssetImport.so.5",
"libQt5Quick3DRender.so.5",
"libQt5Quick3DRuntimeRender.so.5",
"libQt5Quick3DUtils.so.5",
"libQt5Sql.so.5"
]
distDir = os.path.join(os.getcwd(), "dist", "novelWriter")
for delFile in delFiles:
delPath = os.path.join(distDir, delFile)
if os.path.isfile(delPath):
print("Deleting file: %s" % delPath)
os.unlink(delPath)
print("")
print("Build Finished")
print("")
print("The novelWriter executable should be in the folder named 'dist'")
print("")
return
# =============================================================================================== #
# Inno Setup Builder
# =============================================================================================== #
def innoSetup():
"""Run the Inno Setup tool to build a setup.exe file for Windows.
"""
print("")
print("Running Inno Setup")
print("##################")
print("")
# Read the iss template
issData = ""
with open(os.path.join("setup", "win_setup.iss"), mode="r") as inFile:
issData = inFile.read()
import nw # noqa: E402
issData = issData.replace(r"%%version%%", nw.__version__)
issData = issData.replace(r"%%dir%%", os.getcwd())
with open("setup.iss", mode="w+") as outFile:
outFile.write(issData)
try:
subprocess.call(["iscc", "setup.iss"])
except Exception as e:
print("Inno Setup failed with error:")
print(str(e))
sys.exit(1)
return
# =============================================================================================== #
# Clean Build and Dist Folders
# =============================================================================================== #
def cleanInstall():
"""Recursively delete the 'build' and 'dist' folders.
"""
print("")
print("Cleaning up build environment ...")
buildDir = os.path.join(os.getcwd(), "build")
if os.path.isdir(buildDir):
try:
shutil.rmtree(buildDir)
print("Deleted folder 'build'")
except Exception as e:
print("Error: Cannot delete 'build' folder.")
print(str(e))
sys.exit(1)
else:
print("Folder 'build' not found")
distDir = os.path.join(os.getcwd(), "dist")
if os.path.isdir(distDir):
try:
shutil.rmtree(distDir)
print("Deleted folder 'dist'")
except Exception as e:
print("Error: Cannot delete 'dist' folder.")
print(str(e))
sys.exit(1)
else:
print("Folder 'dist' not found")
print("")
return
# =============================================================================================== #
# Process Build Steps
# =============================================================================================== #
if __name__ == "__main__":
"""Parse command line options and run the commands.
"""
# Detect OS
if sys.platform.startswith("linux"):
hostOS = OS_LINUX
elif sys.platform.startswith("darwin"):
hostOS = OS_DARWIN
elif sys.platform.startswith("win32"):
hostOS = OS_WIN
elif sys.platform.startswith("cygwin"):
hostOS = OS_WIN
else:
hostOS = OS_NONE
# Flags and Variables
buildWindowed = True
oneFile = False
makeSetup = False
doFreeze = False
helpMsg = (
"\n"
"novelWriter Make Tool\n"
"=====================\n"
"This tool provides build commands for distibuting novelWriter as a\n"
"package. The available options are as follows:\n"
"\n"
"help Print the help message.\n"
"freeze Freeze the package and produces a folder of all\n"
" dependencies using pyinstaller.\n"
"onefile Build a standalone executable with all dependencies\n"
" bundled. Implies 'freeze', cannot be used with 'setup'.\n"
"pip Run pip to install all package dependencies for\n"
" novelWriter and this build tool.\n"
"setup Build a setup.exe installer for Windows. This option\n"
" automaticall disables the 'onefile' option.\n"
"clean This will attempt to delete the 'build' and 'dist'\n"
" folders in the current folder.\n"
)
if "help" in sys.argv or len(sys.argv) <= 1:
print(helpMsg)
sys.exit(0)
if not os.path.isfile(os.path.join(os.getcwd(), "novelWriter.py")):
print("Error: This script must be run in the root folder of novelWriter.")
sys.exit(1)
if not os.path.isdir(os.path.join(os.getcwd(), "nw")):
print("Error: This script must be run in the root folder of novelWriter.")
sys.exit(1)
if "clean" in sys.argv:
sys.argv.remove("clean")
cleanInstall()
if "pip" in sys.argv:
sys.argv.remove("pip")
installPackages(hostOS)
if "freeze" in sys.argv:
sys.argv.remove("freeze")
doFreeze = True
if "onefile" in sys.argv:
sys.argv.remove("onefile")
doFreeze = True
oneFile = True
if "setup" in sys.argv:
sys.argv.remove("setup")
if hostOS == OS_WIN:
oneFile = False
makeSetup = True
else:
print("Error: Argument 'setup' for Inno Setup is Windows only.")
sys.exit(1)
if doFreeze:
freezePackage(buildWindowed, oneFile, makeSetup, hostOS)
if makeSetup:
innoSetup()
# END Main
+37 -5
View File
@@ -35,13 +35,35 @@ from PyQt5.QtWidgets import QApplication, QErrorMessage
from nw.error import exceptionHandler from nw.error import exceptionHandler
from nw.config import Config from nw.config import Config
#
# Version Scheme
# ================
# Generally follows PEP 440
# Hex Version:
# - Digit 1,2 : Major Version (01, 02, 03)
# = Digit 3,4 : Minor Version (01, 09, 10, 99)
# - Digit 5,6 : Patch Version (01, 09, 10, 99)
# = Digit 7 : Release Type (a: aplha, b: beta, c: candidate, f: final)
# - Digit 8 : Release Number (0-9)
#
# Example : Full Short Description
# -------------------------------------------------------------------------
# 0x010200a0 : 1.2-alpha0 1.2a0 Can be used for the dev branch
# 0x010200a1 : 1.2-alpha1 1.2a1 First alpha release
# 0x010200b1 : 1.2-beta1 1.2b1 First beta release
# 0x010200c1 : 1.2-rc1 1.2rc1 First release candidate
# 0x010200f0 : 1.2 1.2 Final release
# 0x010200f1 : 1.2-post1 1.2.post1 Post release, but not a code patch!
# 0x010201f0 : 1.2.1 1.2.1 Patch release
#
__package__ = "nw" __package__ = "nw"
__author__ = "Veronica Berglyd Olsen" __author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 20182020, Veronica Berglyd Olsen" __copyright__ = "Copyright 20182020, Veronica Berglyd Olsen"
__license__ = "GPLv3" __license__ = "GPLv3"
__version__ = "1.0b3" __version__ = "1.0b5"
__hexversion__ = "0x010000b3" __hexversion__ = "0x010000b5"
__date__ = "2020-09-20" __date__ = "2020-10-18"
__maintainer__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net" __email__ = "code@vkbo.net"
__status__ = "Beta" __status__ = "Beta"
@@ -200,9 +222,9 @@ def main(sysArgs=None):
# Check Packages and Versions # Check Packages and Versions
errorData = [] errorData = []
errorCode = 0 errorCode = 0
if sys.hexversion < 0x030403f0: if sys.hexversion < 0x030600f0:
errorData.append( errorData.append(
"At least Python 3.4.3 is required, but 3.6 is highly recommended." "At least Python 3.6.0 is required, found %s." % CONFIG.verPyString
) )
errorCode |= 4 errorCode |= 4
if CONFIG.verQtValue < 50200: if CONFIG.verQtValue < 50200:
@@ -241,6 +263,16 @@ def main(sysArgs=None):
# Finish initialising config # Finish initialising config
CONFIG.initConfig(confPath, dataPath) CONFIG.initConfig(confPath, dataPath)
if CONFIG.osDarwin:
try:
from Foundation import NSBundle
bundle = NSBundle.mainBundle()
info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
info["CFBundleName"] = "novelWriter"
except ImportError as e:
logger.error("Failed to set application name")
logger.error(str(e))
# Import GUI (after dependency checks), and launch # Import GUI (after dependency checks), and launch
from nw.guimain import GuiMain from nw.guimain import GuiMain
if testMode: if testMode:
@@ -0,0 +1,78 @@
<?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"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg5291"
sodipodi:docname="status_lines-dark.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1344"
id="namedview6681"
showgrid="false"
inkscape:zoom="38.125"
inkscape:cx="12.065574"
inkscape:cy="12"
inkscape:window-x="2560"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg5291" />
<metadata
id="metadata5297">
<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 />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs5295" />
<g
id="g5303"
transform="matrix(1.1578947,0,0,1.1578947,-1.6052627,-1.8947364)"
style="fill:#aeaeae;fill-opacity:1">
<path
d="m 19,17 h -7 c -1.103,0 -2,0.897 -2,2 0,1.103 0.897,2 2,2 h 7 c 1.103,0 2,-0.897 2,-2 0,-1.103 -0.897,-2 -2,-2 z m 0,-7 h -7 c -1.103,0 -2,0.897 -2,2 0,1.103 0.897,2 2,2 h 7 c 1.103,0 2,-0.897 2,-2 0,-1.103 -0.897,-2 -2,-2 z m 0,-7 h -7 c -1.103,0 -2,0.897 -2,2 0,1.103 0.897,2 2,2 h 7 C 20.103,7 21,6.103 21,5 21,3.897 20.103,3 19,3 Z"
id="path5283"
style="fill:#aeaeae;fill-opacity:1" />
<circle
cx="5"
cy="19"
r="2.5"
id="circle5285"
style="fill:#aeaeae;fill-opacity:1" />
<circle
cx="5"
cy="12"
r="2.5"
id="circle5287"
style="fill:#aeaeae;fill-opacity:1" />
<circle
cx="5"
cy="5"
r="2.5"
id="circle5289"
style="fill:#aeaeae;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+78
View File
@@ -0,0 +1,78 @@
<?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"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg5291"
sodipodi:docname="status_lines.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1344"
id="namedview7270"
showgrid="false"
inkscape:zoom="38.125"
inkscape:cx="12.065574"
inkscape:cy="12"
inkscape:window-x="2560"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg5291" />
<metadata
id="metadata5297">
<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 />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs5295" />
<g
id="g5303"
transform="matrix(1.1578947,0,0,1.1578947,-1.6052627,-1.8947364)"
style="fill:#000000;fill-opacity:0.72">
<path
d="m 19,17 h -7 c -1.103,0 -2,0.897 -2,2 0,1.103 0.897,2 2,2 h 7 c 1.103,0 2,-0.897 2,-2 0,-1.103 -0.897,-2 -2,-2 z m 0,-7 h -7 c -1.103,0 -2,0.897 -2,2 0,1.103 0.897,2 2,2 h 7 c 1.103,0 2,-0.897 2,-2 0,-1.103 -0.897,-2 -2,-2 z m 0,-7 h -7 c -1.103,0 -2,0.897 -2,2 0,1.103 0.897,2 2,2 h 7 C 20.103,7 21,6.103 21,5 21,3.897 20.103,3 19,3 Z"
id="path5283"
style="fill:#000000;fill-opacity:0.72" />
<circle
cx="5"
cy="19"
r="2.5"
id="circle5285"
style="fill:#000000;fill-opacity:0.72" />
<circle
cx="5"
cy="12"
r="2.5"
id="circle5287"
style="fill:#000000;fill-opacity:0.72" />
<circle
cx="5"
cy="5"
r="2.5"
id="circle5289"
style="fill:#000000;fill-opacity:0.72" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+19 -8
View File
@@ -152,13 +152,13 @@ def formatInt(theInt):
theVal /= 1000.0 theVal /= 1000.0
if theVal < 1000.0: if theVal < 1000.0:
if theVal < 10.0: if theVal < 10.0:
return "%4.2f%s%s" % (theVal, nwUnicode.U_THNSP, pF) return f"{theVal:4.2f}{nwUnicode.U_THNSP}{pF}"
elif theVal < 100.0: elif theVal < 100.0:
return "%4.1f%s%s" % (theVal, nwUnicode.U_THNSP, pF) return f"{theVal:4.1f}{nwUnicode.U_THNSP}{pF}"
else: else:
return "%3.0f%s%s" % (theVal, nwUnicode.U_THNSP, pF) return f"{theVal:3.0f}{nwUnicode.U_THNSP}{pF}"
return "%d" % theInt return str(theInt)
def formatTimeStamp(theTime, fileSafe=False): def formatTimeStamp(theTime, fileSafe=False):
"""Take a number (on the format returned by time.time()) and convert """Take a number (on the format returned by time.time()) and convert
@@ -169,6 +169,17 @@ def formatTimeStamp(theTime, fileSafe=False):
else: else:
return datetime.fromtimestamp(theTime).strftime(nwConst.tStampFmt) return datetime.fromtimestamp(theTime).strftime(nwConst.tStampFmt)
def formatTime(tS):
"""Format a time in seconds in HH:MM:SS format or d-HH:MM:SS format
if a full day or longer.
"""
if isinstance(tS, int):
if tS >= 86400:
return f"{tS//86400:d}-{tS%86400//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}"
else:
return f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}"
return "ERROR"
def splitVersionNumber(vString): def splitVersionNumber(vString):
""" Splits a version string on the form aa.bb.cc into major, minor """ Splits a version string on the form aa.bb.cc into major, minor
and patch, and computes an integer value aabbcc. and patch, and computes an integer value aabbcc.
@@ -203,12 +214,12 @@ def transferCase(theSource, theTarget):
if len(theTarget) < 1 or len(theSource) < 1: if len(theTarget) < 1 or len(theSource) < 1:
return theResult return theResult
if theSource[0] == theSource[0].upper(): if theSource.istitle():
theResult = theTarget[0].upper() + theTarget[1:] theResult = theTarget.title()
if theSource == theSource.upper(): if theSource.isupper():
theResult = theTarget.upper() theResult = theTarget.upper()
elif theSource == theSource.lower(): elif theSource.islower():
theResult = theTarget.lower() theResult = theTarget.lower()
return theResult return theResult
+80 -40
View File
@@ -3,7 +3,7 @@
novelWriter Config Class novelWriter Config Class
============================ ============================
This class reads and store the main preferences of the application Class reading and holding the preferences of the application
File History: File History:
Created: 2018-09-22 [0.0.1] Created: 2018-09-22 [0.0.1]
@@ -102,41 +102,57 @@ class Config:
self.outlnPanePos = [500, 150] self.outlnPanePos = [500, 150]
self.isFullScreen = False self.isFullScreen = False
## Features
self.hideVScroll = False # Hide vertical scroll bars on main widgets
self.hideHScroll = False # Hide horizontal scroll bars on main widgets
## Project ## Project
self.autoSaveProj = 60 self.autoSaveProj = 60 # Interval for auto-saving project in seconds
self.autoSaveDoc = 30 self.autoSaveDoc = 30 # Interval for auto-saving document in seconds
## Text Editor ## Text Editor
self.textFont = None self.textFont = None # Editor font
self.textSize = 12 self.textSize = 12 # Editor font size
self.textFixedW = True self.textFixedW = True # Keep editor text fixed width
self.textWidth = 600 self.textWidth = 600 # Editor text width
self.textMargin = 40 self.textMargin = 40 # Editor/viewer text margin
self.tabWidth = 40 self.tabWidth = 40 # Editor tabulator width
self.focusWidth = 800
self.hideFocusFooter = False
self.doJustify = False
self.autoSelect = True
self.doReplace = True
self.doReplaceSQuote = True
self.doReplaceDQuote = True
self.doReplaceDash = True
self.doReplaceDots = True
self.wordCountTimer = 5.0
self.showTabsNSpaces = False
self.showLineEndings = False
self.bigDocLimit = 800
self.showFullPath = True
self.highlightQuotes = True
self.highlightEmph = True
self.focusWidth = 800 # Focus Mode text width
self.hideFocusFooter = False # Hide document footer in Focus Mode
self.showFullPath = True # Show full document path in editor header
self.autoSelect = True # Auto-select word when applying format with no selection
self.doJustify = False # Justify text
self.showTabsNSpaces = False # Show tabs and spaces in edior
self.showLineEndings = False # Show line endings in editor
self.doReplace = True # Enable auto-replace as you type
self.doReplaceSQuote = True # Smart single quotes
self.doReplaceDQuote = True # Smart double quotes
self.doReplaceDash = True # Replace multiple hyphens with dashes
self.doReplaceDots = True # Replace three dots with ellipsis
self.scrollPastEnd = True # Allow scrolling past end of document
self.autoScroll = False # Typewriter-like scrolling
self.autoScrollPos = 30 # Start point for typewriter-like scrolling
self.wordCountTimer = 5.0 # Interval for word count update in seconds
self.bigDocLimit = 800 # Size threshold for heavy editor features in kilobytes
self.highlightQuotes = True # Highlight text in quotes
self.highlightEmph = True # Add colour to text emphasis
## User-Selected Symbols
self.fmtApostrophe = nwUnicode.U_RSQUO self.fmtApostrophe = nwUnicode.U_RSQUO
self.fmtSingleQuotes = [nwUnicode.U_LSQUO, nwUnicode.U_RSQUO] self.fmtSingleQuotes = [nwUnicode.U_LSQUO, nwUnicode.U_RSQUO]
self.fmtDoubleQuotes = [nwUnicode.U_LDQUO, nwUnicode.U_RDQUO] self.fmtDoubleQuotes = [nwUnicode.U_LDQUO, nwUnicode.U_RDQUO]
## Spell Checking
self.spellTool = None self.spellTool = None
self.spellLanguage = None self.spellLanguage = None
## Search Bar Switches
self.searchCase = False self.searchCase = False
self.searchWord = False self.searchWord = False
self.searchRegEx = False self.searchRegEx = False
@@ -391,6 +407,12 @@ class Config:
self.isFullScreen = self._parseLine( self.isFullScreen = self._parseLine(
cnfParse, cnfSec, "fullscreen", self.CNF_BOOL, self.isFullScreen cnfParse, cnfSec, "fullscreen", self.CNF_BOOL, self.isFullScreen
) )
self.hideVScroll = self._parseLine(
cnfParse, cnfSec, "hidevscroll", self.CNF_BOOL, self.hideVScroll
)
self.hideHScroll = self._parseLine(
cnfParse, cnfSec, "hidehscroll", self.CNF_BOOL, self.hideHScroll
)
## Project ## Project
cnfSec = "Project" cnfSec = "Project"
@@ -448,6 +470,15 @@ class Config:
self.doReplaceDots = self._parseLine( self.doReplaceDots = self._parseLine(
cnfParse, cnfSec, "repdots", self.CNF_BOOL, self.doReplaceDots cnfParse, cnfSec, "repdots", self.CNF_BOOL, self.doReplaceDots
) )
self.scrollPastEnd = self._parseLine(
cnfParse, cnfSec, "scrollpastend", self.CNF_BOOL, self.scrollPastEnd
)
self.autoScroll = self._parseLine(
cnfParse, cnfSec, "autoscroll", self.CNF_BOOL, self.autoScroll
)
self.autoScrollPos = self._parseLine(
cnfParse, cnfSec, "autoscrollpos", self.CNF_INT, self.autoScrollPos
)
self.fmtSingleQuotes = self._parseLine( self.fmtSingleQuotes = self._parseLine(
cnfParse, cnfSec, "fmtsinglequote", self.CNF_LIST, self.fmtSingleQuotes cnfParse, cnfSec, "fmtsinglequote", self.CNF_LIST, self.fmtSingleQuotes
) )
@@ -565,6 +596,8 @@ class Config:
cnfParse.set(cnfSec, "viewpane", self._packList(self.viewPanePos)) cnfParse.set(cnfSec, "viewpane", self._packList(self.viewPanePos))
cnfParse.set(cnfSec, "outlinepane", self._packList(self.outlnPanePos)) cnfParse.set(cnfSec, "outlinepane", self._packList(self.outlnPanePos))
cnfParse.set(cnfSec, "fullscreen", str(self.isFullScreen)) cnfParse.set(cnfSec, "fullscreen", str(self.isFullScreen))
cnfParse.set(cnfSec, "hidevscroll", str(self.hideVScroll))
cnfParse.set(cnfSec, "hidehscroll", str(self.hideHScroll))
## Project ## Project
cnfSec = "Project" cnfSec = "Project"
@@ -590,6 +623,9 @@ class Config:
cnfParse.set(cnfSec, "repdquotes", str(self.doReplaceDQuote)) cnfParse.set(cnfSec, "repdquotes", str(self.doReplaceDQuote))
cnfParse.set(cnfSec, "repdash", str(self.doReplaceDash)) cnfParse.set(cnfSec, "repdash", str(self.doReplaceDash))
cnfParse.set(cnfSec, "repdots", str(self.doReplaceDots)) cnfParse.set(cnfSec, "repdots", str(self.doReplaceDots))
cnfParse.set(cnfSec, "scrollpastend", str(self.scrollPastEnd))
cnfParse.set(cnfSec, "autoscroll", str(self.autoScroll))
cnfParse.set(cnfSec, "autoscrollpos", str(self.autoScrollPos))
cnfParse.set(cnfSec, "fmtsinglequote", self._packList(self.fmtSingleQuotes)) cnfParse.set(cnfSec, "fmtsinglequote", self._packList(self.fmtSingleQuotes))
cnfParse.set(cnfSec, "fmtdoublequote", self._packList(self.fmtDoubleQuotes)) cnfParse.set(cnfSec, "fmtdoublequote", self._packList(self.fmtDoubleQuotes))
cnfParse.set(cnfSec, "spelltool", str(self.spellTool)) cnfParse.set(cnfSec, "spelltool", str(self.spellTool))
@@ -654,8 +690,7 @@ class Config:
if os.path.isfile(cacheFile): if os.path.isfile(cacheFile):
try: try:
with open(cacheFile, mode="r", encoding="utf8") as inFile: with open(cacheFile, mode="r", encoding="utf8") as inFile:
theJson = inFile.read() theData = json.load(inFile)
theData = json.loads(theJson)
for projPath in theData.keys(): for projPath in theData.keys():
theEntry = theData[projPath] theEntry = theData[projPath]
@@ -693,7 +728,7 @@ class Config:
try: try:
with open(cacheTemp, mode="w+", encoding="utf8") as outFile: with open(cacheTemp, mode="w+", encoding="utf8") as outFile:
outFile.write(json.dumps(self.recentProj, indent=2)) json.dump(self.recentProj, outFile, indent=2)
except Exception as e: except Exception as e:
self.hasError = True self.hasError = True
self.errData.append("Could not save recent project cache") self.errData.append("Could not save recent project cache")
@@ -876,23 +911,28 @@ class Config:
def _packList(self, inData): def _packList(self, inData):
"""Pack a list of items into a comma separated string. """Pack a list of items into a comma separated string.
""" """
return ", ".join(str(inVal) for inVal in inData) return ", ".join([str(inVal) for inVal in inData])
def _parseLine(self, cnfParse, cnfSec, cnfName, cnfType, cnfDefault): def _parseLine(self, cnfParse, cnfSec, cnfName, cnfType, cnfDefault):
"""Parse a line and return the correct datatype. """Parse a line and return the correct datatype.
""" """
if cnfParse.has_section(cnfSec): if cnfParse.has_section(cnfSec):
if cnfParse.has_option(cnfSec, cnfName): if cnfParse.has_option(cnfSec, cnfName):
if cnfType == self.CNF_STR: try:
return cnfParse.get(cnfSec, cnfName) if cnfType == self.CNF_STR:
elif cnfType == self.CNF_INT: return cnfParse.get(cnfSec, cnfName)
return cnfParse.getint(cnfSec, cnfName) elif cnfType == self.CNF_INT:
elif cnfType == self.CNF_BOOL: return cnfParse.getint(cnfSec, cnfName)
return cnfParse.getboolean(cnfSec, cnfName) elif cnfType == self.CNF_BOOL:
elif cnfType == self.CNF_LIST: return cnfParse.getboolean(cnfSec, cnfName)
return self._unpackList( elif cnfType == self.CNF_LIST:
cnfParse.get(cnfSec, cnfName), len(cnfDefault), cnfDefault return self._unpackList(
) cnfParse.get(cnfSec, cnfName), len(cnfDefault), cnfDefault
)
except ValueError as e:
logger.error("Failed to load value from config file.")
logger.error(str(e))
return cnfDefault return cnfDefault
def _checkNone(self, checkVal): def _checkNone(self, checkVal):
@@ -927,4 +967,4 @@ class Config:
return return
# End Class Config # END Class Config
+2 -2
View File
@@ -41,8 +41,8 @@ class nwConst():
class nwRegEx(): class nwRegEx():
FMT_I = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)" FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_B = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)" FMT_EB = r"(?<![\w\\])([\*]{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_ST = r"(?<![\w\\])([~]{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)" FMT_ST = r"(?<![\w\\])([~]{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
# END Class nwRegEx # END Class nwRegEx
+3 -3
View File
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter Language Codes """novelWriter ISO Codes
novelWriter Language Codes novelWriter ISO Codes
============================== =========================
Handles translating language codes to language names Handles translating language codes to language names
File History: File History:
+63 -55
View File
@@ -3,7 +3,7 @@
novelWriter Project Document novelWriter Project Document
================================ ================================
Class holding a document Class holding a single novelWriter document
File History: File History:
Created: 2018-09-29 [0.0.1] Created: 2018-09-29 [0.0.1]
@@ -30,7 +30,7 @@ import os
from nw.constants import nwAlert from nw.constants import nwAlert
from nw.common import isHandle from nw.common import isHandle
from nw.constants import nwItemLayout, nwItemClass, nwConst from nw.constants import nwItemLayout, nwItemClass
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -45,7 +45,7 @@ class NWDoc():
self._theItem = None # The currently open item self._theItem = None # The currently open item
self._docHandle = None # The handle of the currently open item self._docHandle = None # The handle of the currently open item
self._fileLoc = None # The file location of the currently open item self._fileLoc = None # The file location of the currently open item
self._docMeta = "" # The meta string of the currently open item self._docMeta = {} # The meta data of the currently open item
# Internal Mapping # Internal Mapping
self.makeAlert = self.theParent.makeAlert self.makeAlert = self.theParent.makeAlert
@@ -62,12 +62,14 @@ class NWDoc():
self._theItem = None self._theItem = None
self._docHandle = None self._docHandle = None
self._fileLoc = None self._fileLoc = None
self._docMeta = "" self._docMeta = {}
return return
def openDocument(self, tHandle, showStatus=True, isOrphan=False): def openDocument(self, tHandle, showStatus=True, isOrphan=False):
"""Open a document from handle, capturing potential file system """Open a document from handle, capturing potential file system
errors and parse meta data. errors and parse meta data. If the document doesn't exist on
disk, return an empty string. If something went wrong, return
None.
""" """
if not isHandle(tHandle): if not isHandle(tHandle):
return None return None
@@ -92,16 +94,21 @@ class NWDoc():
self._fileLoc = docPath self._fileLoc = docPath
theText = "" theText = ""
self._docMeta = "" self._docMeta = {}
if os.path.isfile(docPath): if os.path.isfile(docPath):
try: try:
with open(docPath, mode="r", encoding="utf8") as inFile: with open(docPath, mode="r", encoding="utf8") as inFile:
fstLine = inFile.readline()
if fstLine.startswith("%%~ "): # Check the first <= 10 lines for metadata
# This is the meta line for i in range(10):
self._docMeta = fstLine[4:].strip() inLine = inFile.readline()
else: if inLine.startswith(r"%%~"):
theText = fstLine self._parseMeta(inLine)
else:
theText = inLine
break
# Load the rest of the file
theText += inFile.read() theText += inFile.read()
except Exception as e: except Exception as e:
@@ -117,16 +124,14 @@ class NWDoc():
logger.debug("The requested document does not exist.") logger.debug("The requested document does not exist.")
return "" return ""
logger.verbose("DocMeta: '%s'" % self._docMeta)
if showStatus and not isOrphan: if showStatus and not isOrphan:
self.theParent.statusBar.setStatus("Opened Document: %s" % self._theItem.itemName) self.theParent.setStatus("Opened Document: %s" % self._theItem.itemName)
return theText return theText
def saveDocument(self, docText): def saveDocument(self, docText):
"""Save the document via temp file in case of save failure, and """Save the document. The file is saved via a temp file in case
in any case keep a backup of the file. of save failure. Returns True if successful, False if not.
""" """
if self._docHandle is None: if self._docHandle is None:
return False return False
@@ -139,17 +144,14 @@ class NWDoc():
docPath = os.path.join(self.theProject.projContent, docFile) docPath = os.path.join(self.theProject.projContent, docFile)
docTemp = os.path.join(self.theProject.projContent, docFile+"~") docTemp = os.path.join(self.theProject.projContent, docFile+"~")
# DocMeta line
if self._theItem is None: if self._theItem is None:
docMeta = "" docMeta = ""
else: else:
itemPath = self.theProject.projTree.getItemPath(self._docHandle)
docMeta = ( docMeta = (
"%%~ {handlepath:s}:{itemclass:s}:{itemlayout:s}:{itemname:s}\n" f"%%~name: {self._theItem.itemName:s}\n"
).format( f"%%~path: {self._theItem.itemParent:s}/{self._theItem.itemHandle:s}\n"
handlepath = ":".join(itemPath), f"%%~kind: {self._theItem.itemClass.name:s}/{self._theItem.itemLayout.name:s}\n"
itemclass = self._theItem.itemClass.name,
itemlayout = self._theItem.itemLayout.name,
itemname = self._theItem.itemName,
) )
try: try:
@@ -166,12 +168,12 @@ class NWDoc():
os.unlink(docPath) os.unlink(docPath)
os.rename(docTemp, docPath) os.rename(docTemp, docPath)
self.theParent.statusBar.setStatus("Saved Document: %s" % self._theItem.itemName) self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName)
return True return True
def deleteDocument(self, tHandle): def deleteDocument(self, tHandle):
"""Permanently delete a document source file and its backups """Permanently delete a document source file and related files
from the project data folder. from the project data folder.
""" """
if not isHandle(tHandle): if not isHandle(tHandle):
@@ -212,39 +214,45 @@ class NWDoc():
"""Parses the document meta tag and returns the path and name as """Parses the document meta tag and returns the path and name as
a list and a string. a list and a string.
""" """
if len(self._docMeta) < 14: theName = self._docMeta.get("name", "")
# Not enough information theParent = self._docMeta.get("parent", None)
return "", [], None, None theClass = self._docMeta.get("class", None)
theLayout = self._docMeta.get("layout", None)
theMeta = self._docMeta return theName, theParent, theClass, theLayout
# Scan for handles ##
thePath = [] # Internal Functions
for n in range(nwConst.maxDepth + 5): ##
if len(theMeta) < 14:
break
if theMeta[13] == ":":
theHandle = theMeta[:13]
if isHandle(theHandle):
thePath.append(theHandle)
theMeta = theMeta[14:]
else:
break
else:
break
theClass = nwItemClass.NO_CLASS def _parseMeta(self, metaLine):
for aClass in nwItemClass: """Parse a line from the document statting with the characters
if theMeta.startswith(aClass.name): %%~ that may contain meta data.
theClass = aClass """
theMeta = theMeta[len(aClass.name)+1:] if metaLine.startswith("%%~name:"):
self._docMeta["name"] = metaLine[8:].strip()
theLayout = nwItemLayout.NO_LAYOUT elif metaLine.startswith("%%~path:"):
for aLayout in nwItemLayout: metaVal = metaLine[8:].strip()
if theMeta.startswith(aLayout.name): metaBits = metaVal.split("/")
theLayout = aLayout if len(metaBits) == 2:
theMeta = theMeta[len(aLayout.name)+1:] if isHandle(metaBits[0]):
self._docMeta["parent"] = metaBits[0]
if isHandle(metaBits[1]):
self._docMeta["handle"] = metaBits[1]
return theMeta, thePath, theClass, theLayout elif metaLine.startswith("%%~kind:"):
metaVal = metaLine[8:].strip()
metaBits = metaVal.split("/")
if len(metaBits) == 2:
if metaBits[0] in nwItemClass.__members__:
self._docMeta["class"] = nwItemClass[metaBits[0]]
if metaBits[1] in nwItemLayout.__members__:
self._docMeta["layout"] = nwItemLayout[metaBits[1]]
else:
logger.debug("Ignoring meta data: '%s'" % metaLine)
return
# END Class NWDoc # END Class NWDoc
+9 -14
View File
@@ -3,7 +3,7 @@
novelWriter Project Index novelWriter Project Index
============================= =============================
Class holding the index of tags Class holding the project index of tags, headers and references
File History: File History:
Created: 2019-05-27 [0.1.4] Created: 2019-05-27 [0.1.4]
@@ -159,8 +159,7 @@ class NWIndex():
logger.debug("Loading index file") logger.debug("Loading index file")
try: try:
with open(indexFile, mode="r", encoding="utf8") as inFile: with open(indexFile, mode="r", encoding="utf8") as inFile:
theJson = inFile.read() theData = json.load(inFile)
theData = json.loads(theJson)
except Exception as e: except Exception as e:
logger.error("Failed to load index file") logger.error("Failed to load index file")
logger.error(str(e)) logger.error(str(e))
@@ -190,23 +189,18 @@ class NWIndex():
"""Save the current index as a json file in the project meta """Save the current index as a json file in the project meta
data folder. data folder.
""" """
indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
logger.debug("Saving index file") logger.debug("Saving index file")
if self.mainConf.debugInfo: indexFile = os.path.join(self.theProject.projMeta, nwFiles.INDEX_FILE)
nIndent = 2
else:
nIndent = None
try: try:
with open(indexFile, mode="w+", encoding="utf8") as outFile: with open(indexFile, mode="w+", encoding="utf8") as outFile:
outFile.write(json.dumps({ json.dump({
"tagIndex" : self.tagIndex, "tagIndex" : self.tagIndex,
"refIndex" : self.refIndex, "refIndex" : self.refIndex,
"novelIndex" : self.novelIndex, "novelIndex" : self.novelIndex,
"noteIndex" : self.noteIndex, "noteIndex" : self.noteIndex,
"textCounts" : self.textCounts, "textCounts" : self.textCounts,
}, indent=nIndent)) }, outFile, indent=2)
except Exception as e: except Exception as e:
logger.error("Failed to save index file") logger.error("Failed to save index file")
logger.error(str(e)) logger.error(str(e))
@@ -218,6 +212,7 @@ class NWIndex():
"""Check that the entries in the index are valid and contain the """Check that the entries in the index are valid and contain the
elements it should. elements it should.
""" """
logger.debug("Checking index")
self.indexBroken = False self.indexBroken = False
try: try:
@@ -279,7 +274,7 @@ class NWIndex():
if theItem.itemLayout == nwItemLayout.NO_LAYOUT: if theItem.itemLayout == nwItemLayout.NO_LAYOUT:
logger.info("Not indexing no-layout item %s" % tHandle) logger.info("Not indexing no-layout item %s" % tHandle)
return False return False
if theItem.parHandle is None: if theItem.itemParent is None:
logger.info("Not indexing orphaned item %s" % tHandle) logger.info("Not indexing orphaned item %s" % tHandle)
return False return False
@@ -288,7 +283,7 @@ class NWIndex():
self.textCounts[tHandle] = [cC, wC, pC] self.textCounts[tHandle] = [cC, wC, pC]
# If the file is archived or trashed, we don't index the file itself # If the file is archived or trashed, we don't index the file itself
if self.theProject.projTree.isTrashRoot(theItem.parHandle): if self.theProject.projTree.isTrashRoot(theItem.itemParent):
logger.info("Not indexing trash item %s" % tHandle) logger.info("Not indexing trash item %s" % tHandle)
return False return False
if theRoot.itemClass == nwItemClass.ARCHIVE: if theRoot.itemClass == nwItemClass.ARCHIVE:
@@ -583,7 +578,7 @@ class NWIndex():
def getCounts(self, tHandle, sTitle=None): def getCounts(self, tHandle, sTitle=None):
"""Returns the counts for a file, or a section of a file """Returns the counts for a file, or a section of a file
starting at title nTitle. starting at title sTitle if it is provided.
""" """
cC = 0 cC = 0
wC = 0 wC = 0
+41 -28
View File
@@ -29,7 +29,7 @@ import logging
from lxml import etree from lxml import etree
from nw.common import checkInt from nw.common import checkInt, isHandle
from nw.constants import nwItemType, nwItemClass, nwItemLayout from nw.constants import nwItemType, nwItemClass, nwItemLayout
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -42,7 +42,7 @@ class NWItem():
self.itemName = "" self.itemName = ""
self.itemHandle = None self.itemHandle = None
self.parHandle = None self.itemParent = None
self.itemOrder = None self.itemOrder = None
self.itemType = nwItemType.NO_TYPE self.itemType = nwItemType.NO_TYPE
self.itemClass = nwItemClass.NO_CLASS self.itemClass = nwItemClass.NO_CLASS
@@ -70,7 +70,7 @@ class NWItem():
xPack = etree.SubElement(xParent, "item", attrib={ xPack = etree.SubElement(xParent, "item", attrib={
"handle" : str(self.itemHandle), "handle" : str(self.itemHandle),
"order" : str(self.itemOrder), "order" : str(self.itemOrder),
"parent" : str(self.parHandle), "parent" : str(self.itemParent),
}) })
self._subPack(xPack, "name", text=str(self.itemName)) self._subPack(xPack, "name", text=str(self.itemName))
self._subPack(xPack, "type", text=str(self.itemType.name)) self._subPack(xPack, "type", text=str(self.itemType.name))
@@ -85,6 +85,7 @@ class NWItem():
self._subPack(xPack, "cursorPos", text=str(self.cursorPos), none=False) self._subPack(xPack, "cursorPos", text=str(self.cursorPos), none=False)
else: else:
self._subPack(xPack, "expanded", text=str(self.isExpanded)) self._subPack(xPack, "expanded", text=str(self.isExpanded))
return return
def unpackXML(self, xItem): def unpackXML(self, xItem):
@@ -101,29 +102,39 @@ class NWItem():
return False return False
if "parent" in xItem.attrib: if "parent" in xItem.attrib:
self.parHandle = xItem.attrib["parent"] self.itemParent = xItem.attrib["parent"]
setMap = { retStatus = True
"name" : self.setName,
"order" : self.setOrder,
"type" : self.setType,
"class" : self.setClass,
"layout" : self.setLayout,
"status" : self.setStatus,
"expanded" : self.setExpanded,
"exported" : self.setExported,
"charCount" : self.setCharCount,
"wordCount" : self.setWordCount,
"paraCount" : self.setParaCount,
"cursorPos" : self.setCursorPos,
}
for xValue in xItem: for xValue in xItem:
if xValue.tag in setMap: if xValue.tag == "name":
setMap[xValue.tag](xValue.text) self.setName(xValue.text)
elif xValue.tag == "order":
self.setOrder(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":
self.setStatus(xValue.text)
elif xValue.tag == "expanded":
self.setExpanded(xValue.text)
elif xValue.tag == "exported":
self.setExported(xValue.text)
elif xValue.tag == "charCount":
self.setCharCount(xValue.text)
elif xValue.tag == "wordCount":
self.setWordCount(xValue.text)
elif xValue.tag == "paraCount":
self.setParaCount(xValue.text)
elif xValue.tag == "cursorPos":
self.setCursorPos(xValue.text)
else: else:
logger.error("Unknown tag '%s'" % xValue.tag) logger.error("Unknown tag '%s'" % xValue.tag)
retStatus = False
return True return retStatus
@staticmethod @staticmethod
def _subPack(xParent, name, attrib=None, text=None, none=True): def _subPack(xParent, name, attrib=None, text=None, none=True):
@@ -131,9 +142,11 @@ class NWItem():
""" """
if not none and (text is None or text == "None"): if not none and (text is None or text == "None"):
return None return None
xSub = etree.SubElement(xParent, name, attrib=attrib) xAttr = {} if attrib is None else attrib
xSub = etree.SubElement(xParent, name, attrib=xAttr)
if text is not None: if text is not None:
xSub.text = text xSub.text = text
return return
## ##
@@ -150,7 +163,7 @@ class NWItem():
"""Set the item handle, and ensure it is valid. """Set the item handle, and ensure it is valid.
""" """
if isinstance(theHandle, str): if isinstance(theHandle, str):
if len(theHandle) == 13: if isHandle(theHandle):
self.itemHandle = theHandle self.itemHandle = theHandle
else: else:
self.itemHandle = None self.itemHandle = None
@@ -162,14 +175,14 @@ class NWItem():
"""Set the parent handle, and ensure that it is valid. """Set the parent handle, and ensure that it is valid.
""" """
if theParent is None: if theParent is None:
self.parHandle = None self.itemParent = None
elif isinstance(theParent, str): elif isinstance(theParent, str):
if len(theParent) == 13: if isHandle(theParent):
self.parHandle = theParent self.itemParent = theParent
else: else:
self.parHandle = None self.itemParent = None
else: else:
self.parHandle = None self.itemParent = None
return return
def setOrder(self, theOrder): def setOrder(self, theOrder):
+2 -3
View File
@@ -109,8 +109,7 @@ class OptionState():
logger.debug("Loading GUI options file") logger.debug("Loading GUI options file")
try: try:
with open(stateFile, mode="r", encoding="utf8") as inFile: with open(stateFile, mode="r", encoding="utf8") as inFile:
theJson = inFile.read() theState = json.load(inFile)
theState = json.loads(theJson)
except Exception as e: except Exception as e:
logger.error("Failed to load GUI options file") logger.error("Failed to load GUI options file")
logger.error(str(e)) logger.error(str(e))
@@ -137,7 +136,7 @@ class OptionState():
try: try:
with open(stateFile, mode="w+", encoding="utf8") as outFile: with open(stateFile, mode="w+", encoding="utf8") as outFile:
outFile.write(json.dumps(self.theState, indent=2)) json.dump(self.theState, outFile, indent=2)
except Exception as e: except Exception as e:
logger.error("Failed to save GUI options file") logger.error("Failed to save GUI options file")
logger.error(str(e)) logger.error(str(e))
+6 -6
View File
@@ -782,7 +782,7 @@ class NWProject():
"""Create a zip file of the entire project. """Create a zip file of the entire project.
""" """
logger.info("Backing up project") logger.info("Backing up project")
self.theParent.statusBar.setStatus("Backing up project ...") self.theParent.setStatus("Backing up project ...")
if self.mainConf.backupPath is None or self.mainConf.backupPath == "": if self.mainConf.backupPath is None or self.mainConf.backupPath == "":
self.theParent.makeAlert(( self.theParent.makeAlert((
@@ -847,7 +847,7 @@ class NWProject():
) )
return False return False
self.theParent.statusBar.setStatus("Project backed up to '%s.zip'" % baseName) self.theParent.setStatus("Project backed up to '%s.zip'" % baseName)
return True return True
@@ -1024,7 +1024,7 @@ class NWProject():
return True return True
def setTreeOrder(self, newOrder): def setTreeOrder(self, newOrder):
"""A list representing the liner/flattened order of project """A list representing the linear/flattened order of project
items in the GUI project tree. The user can rearrange the order items in the GUI project tree. The user can rearrange the order
by drag-and-drop. Forwarded to the NWTree class. by drag-and-drop. Forwarded to the NWTree class.
""" """
@@ -1139,16 +1139,16 @@ class NWProject():
# Technically a bug since treeOrder is built from the # Technically a bug since treeOrder is built from the
# same data as projTree # same data as projTree
continue continue
elif tItem.parHandle is None: elif tItem.itemParent is None:
# Item is a root, or already been identified as an # Item is a root, or already been identified as an
# orphaned item # orphaned item
sentItems.append(tHandle) sentItems.append(tHandle)
yield tItem yield tItem
elif tItem.parHandle in sentItems: elif tItem.itemParent in sentItems:
# Item's parent has been sent, so all is fine # Item's parent has been sent, so all is fine
sentItems.append(tHandle) sentItems.append(tHandle)
yield tItem yield tItem
elif tItem.parHandle in iterItems: elif tItem.itemParent in iterItems:
# Item's parent exists, but hasn't been sent yet, so add # Item's parent exists, but hasn't been sent yet, so add
# it again to the end # it again to the end
logger.warning("Item %s found before its parent" % tHandle) logger.warning("Item %s found before its parent" % tHandle)
+34 -9
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter Spell Check Wrapper """novelWriter Spell Check Classes
novelWriter Spell Check Wrapper novelWriter Spell Check Classes
=================================== ===================================
Wrapper class for spell checking Wrapper class for spell checking tools
File History: File History:
Created: 2019-06-11 [0.1.5] Created: 2019-06-11 [0.1.5]
@@ -87,6 +87,11 @@ class NWSpellCheck():
""" """
return [] return []
def describeDict(self):
"""Dummy function.
"""
return "", ""
@staticmethod @staticmethod
def expandLanguage(spTag): def expandLanguage(spTag):
"""Translate a language tag to something more user friendly. """Translate a language tag to something more user friendly.
@@ -187,6 +192,21 @@ class NWSpellEnchant(NWSpellCheck):
logger.error("Failed to list languages for enchant spell checking") logger.error("Failed to list languages for enchant spell checking")
return retList return retList
def describeDict(self):
"""Return the tag and provider of the currently loaded
dictionary.
"""
try:
spTag = self.theDict.tag
spName = self.theDict.provider.name
except Exception as e:
logger.error("Failed to extract information about the dictionary")
logger.error(str(e))
spTag = ""
spName = ""
return spTag, spName
# END Class NWSpellEnchant # END Class NWSpellEnchant
class NWSpellEnchantDummy: class NWSpellEnchantDummy:
@@ -221,12 +241,14 @@ class NWSpellSimple(NWSpellCheck):
def __init__(self): def __init__(self):
NWSpellCheck.__init__(self) NWSpellCheck.__init__(self)
self.theLang = ""
logger.debug("Simple spell checking activated") logger.debug("Simple spell checking activated")
return return
def setLanguage(self, theLang, projectDict=None): def setLanguage(self, theLang, projectDict=None):
"""Load a dictionary as a list from the app assets folder. """Load a dictionary as a list from the app assets folder.
""" """
self.theLang = theLang
self.WORDS = [] self.WORDS = []
dictFile = os.path.join(self.mainConf.dictPath, theLang+".dict") dictFile = os.path.join(self.mainConf.dictPath, theLang+".dict")
try: try:
@@ -269,15 +291,12 @@ class NWSpellSimple(NWSpellCheck):
if len(theWord) == 0: if len(theWord) == 0:
return [] return []
firstUp = theWord[0] == theWord[0].upper() theMatches = get_close_matches(theWord.lower(), self.WORDS, n=10, cutoff=0.75)
theWord = theWord.lower()
theMatches = get_close_matches(theWord, self.WORDS, n=10, cutoff=0.75)
theOptions = [] theOptions = []
for aWord in theMatches: for aWord in theMatches:
if len(aWord) == 0: if len(aWord) == 0:
continue continue
if firstUp: if theWord[0].isupper():
aWord = aWord[0].upper() + aWord[1:] aWord = aWord[0].upper() + aWord[1:]
aWord = aWord.replace("'", self.mainConf.fmtApostrophe) aWord = aWord.replace("'", self.mainConf.fmtApostrophe)
theOptions.append(aWord) theOptions.append(aWord)
@@ -305,9 +324,15 @@ class NWSpellSimple(NWSpellCheck):
if theBits[1] != ".dict": if theBits[1] != ".dict":
continue continue
spName = "%s [Internal]" % self.expandLanguage(theBits[0]) spName = "%s [internal]" % self.expandLanguage(theBits[0])
retList.append((theBits[0], spName)) retList.append((theBits[0], spName))
return retList return retList
def describeDict(self):
"""Return the tag and provider of the currently loaded
dictionary.
"""
return self.theLang, "internal"
# END Class NWSpellSimple # END Class NWSpellSimple
+1 -1
View File
@@ -3,7 +3,7 @@
novelWriter Project Item Status Class novelWriter Project Item Status Class
========================================= =========================================
Class holding the status elements of a project item Class holding the status/importance elements of a project item
File History: File History:
Created: 2019-05-19 [0.1.3] Created: 2019-05-19 [0.1.3]
+1
View File
@@ -41,6 +41,7 @@ class ToHtml(Tokenizer):
def __init__(self, theProject, theParent): def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent) Tokenizer.__init__(self, theProject, theParent)
self.genMode = self.M_EXPORT self.genMode = self.M_EXPORT
self.cssStyles = True self.cssStyles = True
+3 -3
View File
@@ -3,7 +3,7 @@
novelWriter Text Tokenizer novelWriter Text Tokenizer
============================== ==============================
Splits a piece of nW markdown text into its elements Splits a piece of novelWriter markdown text into its elements
File History: File History:
Created: 2019-05-05 [0.0.1] Created: 2019-05-05 [0.0.1]
@@ -297,8 +297,8 @@ class Tokenizer():
""" """
# RegExes for adding formatting tags within text lines # RegExes for adding formatting tags within text lines
rxFormats = [ rxFormats = [
(QRegularExpression(nwRegEx.FMT_I), [None, self.FMT_I_B, None, self.FMT_I_E]), (QRegularExpression(nwRegEx.FMT_EI), [None, self.FMT_I_B, None, self.FMT_I_E]),
(QRegularExpression(nwRegEx.FMT_B), [None, self.FMT_B_B, None, self.FMT_B_E]), (QRegularExpression(nwRegEx.FMT_EB), [None, self.FMT_B_B, None, self.FMT_B_E]),
(QRegularExpression(nwRegEx.FMT_ST), [None, self.FMT_D_B, None, self.FMT_D_E]), (QRegularExpression(nwRegEx.FMT_ST), [None, self.FMT_D_B, None, self.FMT_D_E]),
] ]
+16 -16
View File
@@ -1,14 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter Word Counter """novelWriter Various Tools
novelWriter Word Counter novelWriter Various Tools
============================ =============================
Simple word counter Various core tool functions
File History: File History:
Created: 2019-04-22 [0.0.1] countWords Created: 2019-04-22 [0.0.1] countWords
Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN
Merged: 2020-05-08 [0.4.5] All of the above into this file Merged: 2020-05-08 [0.4.5] All of the above into this file
Created: 2020-07-05 [0.10.0] numberToRoman
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182020, Veronica Berglyd Olsen Copyright 20182020, Veronica Berglyd Olsen
@@ -136,7 +137,6 @@ def numberToWord(numVal, theLanguage):
def _numberToWordEN(numVal): def _numberToWordEN(numVal):
"""Convert numbers to English words. """Convert numbers to English words.
""" """
numWord = ""
oneWord = "" oneWord = ""
tenWord = "" tenWord = ""
hunWord = "" hunWord = ""
@@ -145,8 +145,8 @@ def _numberToWordEN(numVal):
return "Zero" return "Zero"
oneVal = numVal % 10 oneVal = numVal % 10
tenVal = (numVal-oneVal) % 100 tenVal = (numVal - oneVal) % 100
hunVal = (numVal-tenVal-oneVal) % 1000 hunVal = (numVal - tenVal - oneVal) % 1000
theHundreds = { theHundreds = {
100: "One Hundred", 200: "Two Hundred", 300: "Three Hundred", 100: "One Hundred", 200: "Two Hundred", 300: "Three Hundred",
@@ -167,18 +167,18 @@ def _numberToWordEN(numVal):
} }
hunWord = theHundreds.get(hunVal, "") hunWord = theHundreds.get(hunVal, "")
tenWord = theTens.get(tenVal, "")
if tenVal == 10: if tenVal == 10:
oneWord = theTeens.get(oneVal, "") oneWord = theTeens.get(oneVal, "")
numWord = ("%s %s" % (hunWord, oneWord)).strip() return f"{hunWord} {oneWord}".strip()
else: else:
oneWord = theOnes.get(oneVal, "") oneWord = theOnes.get(oneVal, "")
if tenVal == 0: if tenVal == 0:
numWord = ("%s %s" % (hunWord, oneWord)).strip() return f"{hunWord} {oneWord}".strip()
else: else:
tenWord = theTens.get(tenVal, "")
if oneVal == 0: if oneVal == 0:
numWord = ("%s %s" % (hunWord, tenWord)).strip() return f"{hunWord} {tenWord}".strip()
else: else:
numWord = ("%s %s-%s" % (hunWord, tenWord, oneWord)).strip() return f"{hunWord} {tenWord}-{oneWord}".strip()
return numWord return ""
+7 -7
View File
@@ -3,7 +3,7 @@
novelWriter Project Tree Class novelWriter Project Tree Class
================================== ==================================
Class holding the data of the project tree Class holding the project's tree of project items
File History: File History:
Created: 2020-05-07 [0.4.5] Created: 2020-05-07 [0.4.5]
@@ -134,7 +134,7 @@ class NWTree():
for xItem in xContent: for xItem in xContent:
nwItem = NWItem(self.theProject) nwItem = NWItem(self.theProject)
if nwItem.unpackXML(xItem): if nwItem.unpackXML(xItem):
self.append(nwItem.itemHandle, nwItem.parHandle, nwItem) self.append(nwItem.itemHandle, nwItem.itemParent, nwItem)
nwItem.saveInitialCount() nwItem.saveInitialCount()
return True return True
@@ -177,7 +177,7 @@ class NWTree():
# Dump the JSON # Dump the JSON
with open(tocJson, mode="w+", encoding="utf8") as outFile: with open(tocJson, mode="w+", encoding="utf8") as outFile:
outFile.write(json.dumps(jsonData, indent=2)) json.dump(jsonData, outFile, indent=2)
except Exception as e: except Exception as e:
logger.error(str(e)) logger.error(str(e))
@@ -261,10 +261,10 @@ class NWTree():
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem is not None: if tItem is not None:
for i in range(nwConst.maxDepth + 1): for i in range(nwConst.maxDepth + 1):
if tItem.parHandle is None: if tItem.itemParent is None:
return tItem return tItem
else: else:
tHandle = tItem.parHandle tHandle = tItem.itemParent
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
return None return None
@@ -279,10 +279,10 @@ class NWTree():
if tItem is not None: if tItem is not None:
tTree.append(tHandle) tTree.append(tHandle)
for i in range(nwConst.maxDepth + 1): for i in range(nwConst.maxDepth + 1):
if tItem.parHandle is None: if tItem.itemParent is None:
return tTree return tTree
else: else:
tHandle = tItem.parHandle tHandle = tItem.itemParent
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem is None: if tItem is None:
return tTree return tTree
+1 -1
View File
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter Init """novelWriter Exception Handling
novelWriter Exception Handling novelWriter Exception Handling
================================== ==================================
+6 -8
View File
@@ -55,14 +55,14 @@ class GuiAbout(QDialog):
self.innerBox = QHBoxLayout() self.innerBox = QHBoxLayout()
self.innerBox.setSpacing(self.mainConf.pxInt(16)) self.innerBox.setSpacing(self.mainConf.pxInt(16))
self.setWindowTitle("About %s" % self.mainConf.appName) self.setWindowTitle("About novelWriter")
self.setMinimumWidth(self.mainConf.pxInt(650)) self.setMinimumWidth(self.mainConf.pxInt(650))
self.setMinimumHeight(self.mainConf.pxInt(600)) self.setMinimumHeight(self.mainConf.pxInt(600))
nPx = self.mainConf.pxInt(96) nPx = self.mainConf.pxInt(96)
self.nwIcon = QLabel() self.nwIcon = QLabel()
self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx))) self.nwIcon.setPixmap(self.theParent.theTheme.getPixmap("novelwriter", (nPx, nPx)))
self.lblName = QLabel("<b>%s</b>" % self.mainConf.appName) self.lblName = QLabel("<b>novelWriter</b>")
self.lblVers = QLabel("v%s" % nw.__version__) self.lblVers = QLabel("v%s" % nw.__version__)
self.lblDate = QLabel(datetime.strptime(nw.__date__, "%Y-%m-%d").strftime("%x")) self.lblDate = QLabel(datetime.strptime(nw.__date__, "%Y-%m-%d").strftime("%x"))
@@ -115,17 +115,17 @@ class GuiAbout(QDialog):
""" """
listPrefix = "&nbsp;&nbsp;&bull;&nbsp;&nbsp;" listPrefix = "&nbsp;&nbsp;&bull;&nbsp;&nbsp;"
aboutMsg = ( aboutMsg = (
"<h2>About {name:s}</h2>" "<h2>About novelWriter</h2>"
"<p>{copyright:s}.</p>" "<p>{copyright:s}.</p>"
"<p>Website: <a href='{website:s}'>{domain:s}</a></p>" "<p>Website: <a href='{website:s}'>{domain:s}</a></p>"
"<p>{name:s} is a markdown-like text editor designed for " "<p>novelWriter is a markdown-like text editor designed for "
"organising and writing novels. It is written in Python 3 with a " "organising and writing novels. It is written in Python 3 with a "
"Qt5 GUI, using PyQt5.</p>" "Qt5 GUI, using PyQt5.</p>"
"<p>{name:s} is free software: you can redistribute it and/or " "<p>novelWriter is free software: you can redistribute it and/or "
"modify it under the terms of the GNU General Public License as " "modify it under the terms of the GNU General Public License as "
"published by the Free Software Foundation, either version 3 of " "published by the Free Software Foundation, either version 3 of "
"the License, or (at your option) any later version.</p>" "the License, or (at your option) any later version.</p>"
"<p>{name:s} is distributed in the hope that it will be useful, " "<p>novelWriter is distributed in the hope that it will be useful, "
"but WITHOUT ANY WARRANTY; without even the implied warranty of " "but WITHOUT ANY WARRANTY; without even the implied warranty of "
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.</p>" "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.</p>"
"<p>See the License tab for the full text, or visit the GNU website " "<p>See the License tab for the full text, or visit the GNU website "
@@ -134,7 +134,6 @@ class GuiAbout(QDialog):
"<h3>Credits</h3>" "<h3>Credits</h3>"
"<p>{credits:s}</p>" "<p>{credits:s}</p>"
).format( ).format(
name = self.mainConf.appName,
copyright = nw.__copyright__, copyright = nw.__copyright__,
website = nw.__url__, website = nw.__url__,
domain = nw.__domain__, domain = nw.__domain__,
@@ -222,7 +221,6 @@ class GuiAbout(QDialog):
hColB = self.theParent.theTheme.colHead[2], hColB = self.theParent.theTheme.colHead[2],
) )
self.pageAbout.document().setDefaultStyleSheet(styleSheet) self.pageAbout.document().setDefaultStyleSheet(styleSheet)
# self.pageCredit.document().setDefaultStyleSheet(styleSheet)
self.pageLicense.document().setDefaultStyleSheet(styleSheet) self.pageLicense.document().setDefaultStyleSheet(styleSheet)
return return
+29 -17
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter GUI Build Novel """novelWriter GUI Build Novel Project
novelWriter GUI Build Novel novelWriter GUI Build Novel Project
=============================== =======================================
Class holding the build novel window Class holding the build novel project dialog
File History: File History:
Created: 2020-05-09 [0.5] Created: 2020-05-09 [0.5]
@@ -391,11 +391,11 @@ class GuiBuildNovel(QDialog):
self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF)) self.savePDF.triggered.connect(lambda: self._saveDocument(self.FMT_PDF))
self.saveMenu.addAction(self.savePDF) self.saveMenu.addAction(self.savePDF)
self.saveHTM = QAction("%s HTML (.htm)" % self.mainConf.appName, self) self.saveHTM = QAction("novelWriter HTML (.htm)", self)
self.saveHTM.triggered.connect(lambda: self._saveDocument(self.FMT_HTM)) self.saveHTM.triggered.connect(lambda: self._saveDocument(self.FMT_HTM))
self.saveMenu.addAction(self.saveHTM) self.saveMenu.addAction(self.saveHTM)
self.saveNWD = QAction("%s Markdown (.nwd)" % self.mainConf.appName, self) self.saveNWD = QAction("novelWriter Markdown (.nwd)", self)
self.saveNWD.triggered.connect(lambda: self._saveDocument(self.FMT_NWD)) self.saveNWD.triggered.connect(lambda: self._saveDocument(self.FMT_NWD))
self.saveMenu.addAction(self.saveNWD) self.saveMenu.addAction(self.saveNWD)
@@ -408,11 +408,11 @@ class GuiBuildNovel(QDialog):
self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT)) self.saveTXT.triggered.connect(lambda: self._saveDocument(self.FMT_TXT))
self.saveMenu.addAction(self.saveTXT) self.saveMenu.addAction(self.saveTXT)
self.saveJsonH = QAction("JSON + %s HTML (.json)" % self.mainConf.appName, self) self.saveJsonH = QAction("JSON + novelWriter HTML (.json)", self)
self.saveJsonH.triggered.connect(lambda: self._saveDocument(self.FMT_JSON_H)) self.saveJsonH.triggered.connect(lambda: self._saveDocument(self.FMT_JSON_H))
self.saveMenu.addAction(self.saveJsonH) self.saveMenu.addAction(self.saveJsonH)
self.saveJsonM = QAction("JSON + %s Markdown (.json)" % self.mainConf.appName, self) self.saveJsonM = QAction("JSON + novelWriters Markdown (.json)", self)
self.saveJsonM.triggered.connect(lambda: self._saveDocument(self.FMT_JSON_M)) self.saveJsonM.triggered.connect(lambda: self._saveDocument(self.FMT_JSON_M))
self.saveMenu.addAction(self.saveJsonM) self.saveMenu.addAction(self.saveJsonM)
@@ -450,11 +450,19 @@ class GuiBuildNovel(QDialog):
# Tool Box Scroll Area # Tool Box Scroll Area
self.toolsArea = QScrollArea() self.toolsArea = QScrollArea()
self.toolsArea.setMinimumWidth(self.mainConf.pxInt(250)) self.toolsArea.setMinimumWidth(self.mainConf.pxInt(250))
self.toolsArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.toolsArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.toolsArea.setWidgetResizable(True) self.toolsArea.setWidgetResizable(True)
self.toolsArea.setWidget(self.toolsWidget) self.toolsArea.setWidget(self.toolsWidget)
if self.mainConf.hideVScroll:
self.toolsArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
self.toolsArea.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
if self.mainConf.hideHScroll:
self.toolsArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
else:
self.toolsArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
# Tools and Buttons Layout # Tools and Buttons Layout
self.innerBox = QVBoxLayout() self.innerBox = QVBoxLayout()
self.innerBox.addWidget(self.toolsArea) self.innerBox.addWidget(self.toolsArea)
@@ -676,8 +684,8 @@ class GuiBuildNovel(QDialog):
isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT
isNone |= theItem.itemClass == nwItemClass.NO_CLASS isNone |= theItem.itemClass == nwItemClass.NO_CLASS
isNone |= theItem.itemClass == nwItemClass.TRASH isNone |= theItem.itemClass == nwItemClass.TRASH
isNone |= theItem.parHandle == self.theProject.projTree.trashRoot() isNone |= theItem.itemParent == self.theProject.projTree.trashRoot()
isNone |= theItem.parHandle is None isNone |= theItem.itemParent is None
isNote = theItem.itemLayout == nwItemLayout.NOTE isNote = theItem.itemLayout == nwItemLayout.NOTE
isNovel = not isNone and not isNote isNovel = not isNone and not isNote
@@ -762,12 +770,10 @@ class GuiBuildNovel(QDialog):
if self.mainConf.showGUI: if self.mainConf.showGUI:
dlgOpt = QFileDialog.Options() dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog dlgOpt |= QFileDialog.DontUseNativeDialog
saveTo = QFileDialog.getSaveFileName( savePath, _ = QFileDialog.getSaveFileName(
self, "Save Document As", savePath, options=dlgOpt self, "Save Document As", savePath, options=dlgOpt
) )
if saveTo[0]: if not savePath:
savePath = saveTo[0]
else:
return False return False
self.mainConf.setLastPath(savePath) self.mainConf.setLastPath(savePath)
@@ -901,8 +907,10 @@ class GuiBuildNovel(QDialog):
def _doPrintPreview(self, thePrinter): def _doPrintPreview(self, thePrinter):
"""Connect the print preview painter to the document viewer. """Connect the print preview painter to the document viewer.
""" """
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
thePrinter.setOrientation(QPrinter.Portrait) thePrinter.setOrientation(QPrinter.Portrait)
self.docView.qDocument.print(thePrinter) self.docView.qDocument.print(thePrinter)
qApp.restoreOverrideCursor()
return return
def _selectFont(self): def _selectFont(self):
@@ -915,6 +923,9 @@ class GuiBuildNovel(QDialog):
if theStatus: if theStatus:
self.textFont.setText(theFont.family()) self.textFont.setText(theFont.family())
self.textSize.setValue(theFont.pointSize()) self.textSize.setValue(theFont.pointSize())
self.raise_() # Move the dialog to front (fixes a bug on macOS)
return return
def _loadCache(self): def _loadCache(self):
@@ -1002,8 +1013,9 @@ class GuiBuildNovel(QDialog):
""" """
self.saveODT.setEnabled(theState) self.saveODT.setEnabled(theState)
self.savePDF.setEnabled(theState) self.savePDF.setEnabled(theState)
self.saveMD.setEnabled(theState)
self.saveTXT.setEnabled(theState) self.saveTXT.setEnabled(theState)
if self.mainConf.verQtValue >= 51400:
self.saveMD.setEnabled(theState)
return return
def _saveSettings(self): def _saveSettings(self):
+5 -5
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter Addition QConfigLayout """novelWriter Custom Widgets and Layouts
novelWriter Addition QConfigLayout novelWriter Custom Widgets and Layouts
====================================== ==========================================
A custom Qt grid layout for config forms similar to QFormLayout Various custom widget and layout classes
File History: File History:
Created: 2020-05-03 [0.4.5] QConfigLayout Created: 2020-05-03 [0.4.5] QConfigLayout
@@ -326,7 +326,7 @@ class QSwitch(QAbstractButton):
""" """
super().mouseReleaseEvent(event) super().mouseReleaseEvent(event)
if event.button() == Qt.LeftButton: if event.button() == Qt.LeftButton:
doAnim = QPropertyAnimation(self, b'offset', self) doAnim = QPropertyAnimation(self, b"offset", self)
doAnim.setDuration(120) doAnim.setDuration(120)
doAnim.setStartValue(self.offset) doAnim.setStartValue(self.offset)
if self.isChecked(): if self.isChecked():
+227 -75
View File
@@ -3,7 +3,7 @@
novelWriter GUI Document Editor novelWriter GUI Document Editor
=================================== ===================================
Class holding the document editor Class holding the main document editor
File History: File History:
Created: 2018-09-29 [0.0.1] GuiDocEditor Created: 2018-09-29 [0.0.1] GuiDocEditor
@@ -12,6 +12,7 @@
Created: 2020-04-25 [0.4.5] GuiDocEditHeader Created: 2020-04-25 [0.4.5] GuiDocEditHeader
Rewritten: 2020-06-15 [0.9.0] GuiDocEditSearch Rewritten: 2020-06-15 [0.9.0] GuiDocEditSearch
Created: 2020-06-27 [0.10.0] GuiDocEditFooter Created: 2020-06-27 [0.10.0] GuiDocEditFooter
Rewritten: 2020-10-07 [1.0b3] BackgroundWordCounter
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182020, Veronica Berglyd Olsen Copyright 20182020, Veronica Berglyd Olsen
@@ -36,7 +37,8 @@ import logging
from time import time from time import time
from PyQt5.QtCore import ( from PyQt5.QtCore import (
Qt, QSize, QThread, QTimer, pyqtSlot, QRegExp, QRegularExpression, QPointF Qt, QSize, QTimer, pyqtSlot, pyqtSignal, QRegExp, QRegularExpression,
QPointF, QObject, QRunnable, QPropertyAnimation
) )
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette, QTextCursor, QTextOption, QKeySequence, QFont, QColor, QPalette,
@@ -59,6 +61,11 @@ logger = logging.getLogger(__name__)
class GuiDocEditor(QTextEdit): class GuiDocEditor(QTextEdit):
MOVE_KEYS = (
Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down,
Qt.Key_PageUp, Qt.Key_PageDown
)
def __init__(self, theParent): def __init__(self, theParent):
QTextEdit.__init__(self, theParent) QTextEdit.__init__(self, theParent)
@@ -135,14 +142,15 @@ class GuiDocEditor(QTextEdit):
activated=self._followTag activated=self._followTag
) )
# Set Up Word Count Thread and Timer # Set Up Word Counter
self.wcInterval = self.mainConf.wordCountTimer self.wcInterval = self.mainConf.wordCountTimer
self.wcTimer = QTimer() self.wcTimer = QTimer()
self.wcTimer.setInterval(int(self.wcInterval*1000)) self.wcTimer.setInterval(int(self.wcInterval*1000))
self.wcTimer.timeout.connect(self._runCounter) self.wcTimer.timeout.connect(self._runCounter)
self.wCounter = BackgroundWordCounter(self) self.wCounter = BackgroundWordCounter(self)
self.wCounter.finished.connect(self._updateCounts) self.wCounter.setAutoDelete(False)
self.wCounter.signals.countsReady.connect(self._updateCounts)
self.initEditor() self.initEditor()
@@ -221,6 +229,17 @@ class GuiDocEditor(QTextEdit):
self.qDocument.setDefaultTextOption(theOpt) self.qDocument.setDefaultTextOption(theOpt)
# 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)
# Refresh the tab stops # Refresh the tab stops
if self.mainConf.verQtValue >= 51000: if self.mainConf.verQtValue >= 51000:
self.setTabStopDistance(self.mainConf.getTabWidth()) self.setTabStopDistance(self.mainConf.getTabWidth())
@@ -282,7 +301,7 @@ class GuiDocEditor(QTextEdit):
self._allowAutoReplace(True) self._allowAutoReplace(True)
afTime = time() afTime = time()
logger.debug("Document highlighted in %.3f milliseconds" % (1000*(afTime-bfTime))) logger.debug("Document highlighted in %.3f ms" % (1000*(afTime-bfTime)))
self.lastEdit = time() self.lastEdit = time()
self._runCounter() self._runCounter()
@@ -309,6 +328,9 @@ class GuiDocEditor(QTextEdit):
else: else:
self.setCursorLine(tLine) self.setCursorLine(tLine)
self.docFooter.updateLineCount()
self.lengthLast = self.qDocument.characterCount()
qApp.restoreOverrideCursor() qApp.restoreOverrideCursor()
return True return True
@@ -338,9 +360,11 @@ class GuiDocEditor(QTextEdit):
) % (docSize/1.0e6, nwConst.maxDocSize/1.0e6), nwAlert.ERROR) ) % (docSize/1.0e6, nwConst.maxDocSize/1.0e6), nwAlert.ERROR)
return False return False
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self.setPlainText(theText) self.setPlainText(theText)
self.setDocumentChanged(True) self.setDocumentChanged(True)
self.updateDocMargins() self.updateDocMargins()
qApp.restoreOverrideCursor()
return True return True
@@ -353,9 +377,14 @@ class GuiDocEditor(QTextEdit):
return False return False
docText = self.getText() docText = self.getText()
cC, wC, pC = countWords(docText)
self._updateCounts(cC, wC, pC)
theItem.setCharCount(self.charCount) theItem.setCharCount(self.charCount)
theItem.setWordCount(self.wordCount) theItem.setWordCount(self.wordCount)
theItem.setParaCount(self.paraCount) theItem.setParaCount(self.paraCount)
self.saveCursorPosition() self.saveCursorPosition()
self.nwDocument.saveDocument(docText) self.nwDocument.saveDocument(docText)
self.setDocumentChanged(False) self.setDocumentChanged(False)
@@ -370,6 +399,7 @@ class GuiDocEditor(QTextEdit):
just ensure the margins are set correctly. just ensure the margins are set correctly.
""" """
wW = self.width() wW = self.width()
wH = self.height()
cM = self.mainConf.getTextMargin() cM = self.mainConf.getTextMargin()
vBar = self.verticalScrollBar() vBar = self.verticalScrollBar()
@@ -393,7 +423,7 @@ class GuiDocEditor(QTextEdit):
tW = wW - 2*tB - sW tW = wW - 2*tB - sW
tH = self.docHeader.height() tH = self.docHeader.height()
fH = self.docFooter.height() fH = self.docFooter.height()
fY = self.height() - fH - tB - sH fY = wH - fH - tB - sH
self.docHeader.setGeometry(tB, tB, tW, tH) self.docHeader.setGeometry(tB, tB, tW, tH)
self.docFooter.setGeometry(tB, fY, tW, fH) self.docFooter.setGeometry(tB, fY, tW, fH)
@@ -405,7 +435,14 @@ class GuiDocEditor(QTextEdit):
else: else:
rH = 0 rH = 0
self.setViewportMargins(tM, max(cM, tH, rH), tM, max(cM, fH)) uM = max(cM, tH, rH)
lM = max(cM, fH)
self.setViewportMargins(tM, uM, tM, lM)
if self.mainConf.scrollPastEnd:
docFrame = self.qDocument.rootFrame().frameFormat()
docFrame.setBottomMargin(max(0, 0.6*(wH - uM - lM - 4*tB)))
self.qDocument.rootFrame().setFrameFormat(docFrame)
return return
@@ -453,10 +490,13 @@ class GuiDocEditor(QTextEdit):
""" """
if not isinstance(thePosition, int): if not isinstance(thePosition, int):
return False return False
if thePosition >= 0: if thePosition >= 0:
theCursor = self.textCursor() theCursor = self.textCursor()
theCursor.setPosition(thePosition) theCursor.setPosition(thePosition)
self.setTextCursor(theCursor) self.setTextCursor(theCursor)
self.docFooter.updateLineCount()
return True return True
def getCursorPosition(self): def getCursorPosition(self):
@@ -483,6 +523,7 @@ class GuiDocEditor(QTextEdit):
theBlock = self.qDocument.findBlockByLineNumber(theLine) theBlock = self.qDocument.findBlockByLineNumber(theLine)
if theBlock: if theBlock:
self.setCursorPosition(theBlock.position()) self.setCursorPosition(theBlock.position())
self.docFooter.updateLineCount()
logger.verbose("Cursor moved to line %d" % theLine) logger.verbose("Cursor moved to line %d" % theLine)
return True return True
@@ -501,7 +542,11 @@ class GuiDocEditor(QTextEdit):
theLang = self.theProject.projLang theLang = self.theProject.projLang
self.theDict.setLanguage(theLang, self.theProject.projDict) self.theDict.setLanguage(theLang, self.theProject.projDict)
self.theParent.statusBar.setLanguage(self.theDict.spellLanguage)
aLang, aName = self.theDict.describeDict()
self.theParent.statusBar.setLanguage(
aLang, "%s [%s]" % (self.mainConf.spellTool.title(), aName.title())
)
if not self.bigDoc: if not self.bigDoc:
self.spellCheckDocument() self.spellCheckDocument()
@@ -548,9 +593,8 @@ class GuiDocEditor(QTextEdit):
qApp.restoreOverrideCursor() qApp.restoreOverrideCursor()
afTime = time() afTime = time()
logger.debug( logger.debug(
"Document re-highlighted in %.3f milliseconds" % (1000*(afTime-bfTime)) "Document highlighted in %.3f ms" % (1000*(afTime-bfTime))
) )
self.theParent.statusBar.showMessage("Spell check complete") self.theParent.statusBar.showMessage("Spell check complete")
return True return True
@@ -716,8 +760,8 @@ class GuiDocEditor(QTextEdit):
* The return and enter key redirects here even if the search * The return and enter key redirects here even if the search
box has focus. Since we need these keys to continue search, box has focus. Since we need these keys to continue search,
we block any further interaction here while it's in focus. we block any further interaction here while it's in focus.
* The undo/redo sequences bypasses the doAction pathway from * The undo/redo/select all sequences bypasses the docAction
the menu, so we redirect them back from here. pathway from the menu, so we redirect them back from here.
""" """
isReturn = keyEvent.key() == Qt.Key_Return isReturn = keyEvent.key() == Qt.Key_Return
isReturn |= keyEvent.key() == Qt.Key_Enter isReturn |= keyEvent.key() == Qt.Key_Enter
@@ -725,10 +769,40 @@ class GuiDocEditor(QTextEdit):
return return
elif keyEvent == QKeySequence.Redo: elif keyEvent == QKeySequence.Redo:
self.docAction(nwDocAction.REDO) self.docAction(nwDocAction.REDO)
return
elif keyEvent == QKeySequence.Undo: elif keyEvent == QKeySequence.Undo:
self.docAction(nwDocAction.UNDO) self.docAction(nwDocAction.UNDO)
return
elif keyEvent == QKeySequence.SelectAll:
self.docAction(nwDocAction.SEL_ALL)
return
if self.mainConf.autoScroll:
cOld = self.cursorRect().center().y()
QTextEdit.keyPressEvent(self, keyEvent)
kMod = keyEvent.modifiers()
okMod = kMod == Qt.NoModifier or kMod == Qt.ShiftModifier
okKey = keyEvent.key() not in self.MOVE_KEYS
if okMod and okKey:
cNew = self.cursorRect().center().y()
cMov = cNew - cOld
mPos = self.mainConf.autoScrollPos * self.viewport().height() * 0.01
if abs(cMov) > 0 and cOld > mPos:
# Move the scroll bar
vBar = self.verticalScrollBar()
doAnim = QPropertyAnimation(vBar, b"value", self)
doAnim.setDuration(120)
doAnim.setStartValue(vBar.value())
doAnim.setEndValue(vBar.value() + cMov)
doAnim.start()
else: else:
QTextEdit.keyPressEvent(self, keyEvent) QTextEdit.keyPressEvent(self, keyEvent)
self.docFooter.updateLineCount()
return return
def focusNextPrevChild(self, toNext): def focusNextPrevChild(self, toNext):
@@ -743,15 +817,18 @@ class GuiDocEditor(QTextEdit):
return self.docSearch.cycleFocus(toNext) return self.docSearch.cycleFocus(toNext)
return True return True
def mouseReleaseEvent(self, mEvent): def mouseReleaseEvent(self, theEvent):
"""If the mouse button is released and the control key is """If the mouse button is released and the control key is
pressed, check if we're clicking on a tag, and trigger the pressed, check if we're clicking on a tag, and trigger the
follow tag function. follow tag function.
""" """
if qApp.keyboardModifiers() == Qt.ControlModifier: if qApp.keyboardModifiers() == Qt.ControlModifier:
theCursor = self.cursorForPosition(mEvent.pos()) theCursor = self.cursorForPosition(theEvent.pos())
self._followTag(theCursor) self._followTag(theCursor)
QTextEdit.mouseReleaseEvent(self, mEvent)
QTextEdit.mouseReleaseEvent(self, theEvent)
self.docFooter.updateLineCount()
return return
def resizeEvent(self, theEvent): def resizeEvent(self, theEvent):
@@ -795,11 +872,18 @@ class GuiDocEditor(QTextEdit):
""" """
userCursor = self.textCursor() userCursor = self.textCursor()
userSelection = userCursor.hasSelection() userSelection = userCursor.hasSelection()
posCursor = self.cursorForPosition(thePos)
mnuContext = QMenu() mnuContext = QMenu()
# Cut, Copy and Paste # Follow, Cut, Copy and Paste
# =================== # ===========================
if self._followTag(theCursor=posCursor, loadTag=False):
mnuTag = QAction("Follow Tag", mnuContext)
mnuTag.triggered.connect(lambda: self._followTag(theCursor=posCursor))
mnuContext.addAction(mnuTag)
mnuContext.addSeparator()
if userSelection: if userSelection:
mnuCut = QAction("Cut", mnuContext) mnuCut = QAction("Cut", mnuContext)
@@ -838,10 +922,13 @@ class GuiDocEditor(QTextEdit):
# Spell Checking # Spell Checking
# ============== # ==============
posCursor = self.cursorForPosition(thePos)
spellCheck = self.spellCheck spellCheck = self.spellCheck
if posCursor.block().text().startswith("@"):
spellCheck = False
if spellCheck: if spellCheck:
posCursor = self.cursorForPosition(thePos)
posCursor.select(QTextCursor.WordUnderCursor) posCursor.select(QTextCursor.WordUnderCursor)
theWord = posCursor.selectedText().strip().strip(self.nonWord) theWord = posCursor.selectedText().strip().strip(self.nonWord)
spellCheck &= theWord != "" spellCheck &= theWord != ""
@@ -855,7 +942,7 @@ class GuiDocEditor(QTextEdit):
mnuHead = QAction("Spelling Suggestion(s)", mnuContext) mnuHead = QAction("Spelling Suggestion(s)", mnuContext)
mnuContext.addAction(mnuHead) mnuContext.addAction(mnuHead)
theSuggest = self.theDict.suggestWords(theWord) theSuggest = self.theDict.suggestWords(theWord)[:15]
if len(theSuggest) > 0: if len(theSuggest) > 0:
for aWord in theSuggest: for aWord in theSuggest:
mnuWord = QAction("%s %s" % (nwUnicode.U_ENDASH, aWord), mnuContext) mnuWord = QAction("%s %s" % (nwUnicode.U_ENDASH, aWord), mnuContext)
@@ -905,24 +992,21 @@ class GuiDocEditor(QTextEdit):
@pyqtSlot() @pyqtSlot()
def _runCounter(self): def _runCounter(self):
"""Decide whether to run the word counter, or stop the timer due """Decide whether to run the word counter, or not due to
to inactivity. inactivity.
""" """
sinceActive = time()-self.lastEdit if self.wCounter.isRunning():
if sinceActive > 5*self.wcInterval: logger.verbose("Word counter is busy")
logger.debug( return
"Stopping word count timer: no activity last %.1f seconds" % sinceActive
) if time() - self.lastEdit < 5*self.wcInterval:
self.wcTimer.stop() logger.verbose("Running word counter")
elif self.wCounter.isRunning(): self.theParent.threadPool.start(self.wCounter)
logger.verbose("Word counter thread is busy")
else:
logger.verbose("Starting word counter")
self.wCounter.start()
return return
@pyqtSlot() @pyqtSlot(int, int, int)
def _updateCounts(self): def _updateCounts(self, cCount, wCount, pCount):
"""Slot for the word counter's finished signal """Slot for the word counter's finished signal
""" """
theItem = self.nwDocument.getCurrentItem() theItem = self.nwDocument.getCurrentItem()
@@ -931,19 +1015,17 @@ class GuiDocEditor(QTextEdit):
logger.verbose("Updating word count") logger.verbose("Updating word count")
self.charCount = self.wCounter.charCount self.charCount = cCount
self.wordCount = self.wCounter.wordCount self.wordCount = wCount
self.paraCount = self.wCounter.paraCount self.paraCount = pCount
theItem.setCharCount(self.charCount) theItem.setCharCount(cCount)
theItem.setWordCount(self.wordCount) theItem.setWordCount(wCount)
theItem.setParaCount(self.paraCount) theItem.setParaCount(pCount)
self.theParent.treeView.propagateCount(self.theHandle, self.wordCount) self.theParent.treeView.propagateCount(self.theHandle, wCount)
self.theParent.treeView.projectWordCount() self.theParent.treeView.projectWordCount()
self.theParent.treeMeta.updateCounts( self.theParent.treeMeta.updateCounts(self.theHandle, cCount, wCount, pCount)
self.theHandle, self.charCount, self.wordCount, self.paraCount self._checkDocSize(self.qDocument.characterCount())
)
self._checkDocSize(self.charCount)
self.docFooter.updateCounts() self.docFooter.updateCounts()
return return
@@ -975,7 +1057,7 @@ class GuiDocEditor(QTextEdit):
# Internal Functions # Internal Functions
## ##
def _followTag(self, theCursor=None): def _followTag(self, theCursor=None, loadTag=True):
"""Activated by Ctrl+Enter. Checks that we're in a block """Activated by Ctrl+Enter. Checks that we're in a block
starting with '@'. We then find the word under the cursor and starting with '@'. We then find the word under the cursor and
check that it is after the ':'. If all this is fine, we have a check that it is after the ':'. If all this is fine, we have a
@@ -1000,10 +1082,15 @@ class GuiDocEditor(QTextEdit):
if wPos <= cPos: if wPos <= cPos:
return False return False
logger.verbose("Attempting to follow tag '%s'" % theWord) if loadTag:
self.theParent.docViewer.loadFromTag(theWord) logger.verbose("Attempting to follow tag '%s'" % theWord)
self.theParent.docViewer.loadFromTag(theWord)
else:
logger.verbose("Potential tag '%s'" % theWord)
return True return True
return False
def _openSpellContext(self): def _openSpellContext(self):
"""Opens the spell check context menu at the current point of """Opens the spell check context menu at the current point of
@@ -1126,20 +1213,19 @@ class GuiDocEditor(QTextEdit):
"""Check if document size crosses the big document limit set in """Check if document size crosses the big document limit set in
config. If so, we will set the big document flag to True. config. If so, we will set the big document flag to True.
""" """
newState = theSize > self.mainConf.bigDocLimit*1000 bigLim = self.mainConf.bigDocLimit*1000
newState = theSize > bigLim
if newState != self.bigDoc: if newState != self.bigDoc:
if newState: if newState:
logger.info( logger.info(
"The document size is {:n} > {:n}, big doc mode has been enabled".format( f"The document size is {theSize:n} > {bigLim:n}, "
theSize, self.mainConf.bigDocLimit*1000 f"big doc mode has been enabled"
)
) )
else: else:
logger.info( logger.info(
"The document size is {:n} <= {:n}, big doc mode has been disabled".format( f"The document size is {theSize:n} <= {bigLim:n}, "
theSize, self.mainConf.bigDocLimit*1000 f"big doc mode has been disabled"
)
) )
self.bigDoc = newState self.bigDoc = newState
@@ -1160,6 +1246,11 @@ class GuiDocEditor(QTextEdit):
posS = theCursor.selectionStart() posS = theCursor.selectionStart()
posE = theCursor.selectionEnd() posE = theCursor.selectionEnd()
blockS = self.qDocument.findBlock(posS)
blockE = self.qDocument.findBlock(posE)
if blockS != blockE:
posE = blockS.position() + blockS.length() - 1
theCursor.clearSelection() theCursor.clearSelection()
theCursor.beginEditBlock() theCursor.beginEditBlock()
theCursor.setPosition(posE) theCursor.setPosition(posE)
@@ -1168,8 +1259,8 @@ class GuiDocEditor(QTextEdit):
theCursor.insertText(tBefore) theCursor.insertText(tBefore)
theCursor.endEditBlock() theCursor.endEditBlock()
theCursor.setPosition(posE + len(tBefore)) theCursor.setPosition(posE + len(tBefore), QTextCursor.MoveAnchor)
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, posE-posS) theCursor.setPosition(posS + len(tBefore), QTextCursor.KeepAnchor)
self.setTextCursor(theCursor) self.setTextCursor(theCursor)
else: else:
@@ -1217,20 +1308,33 @@ class GuiDocEditor(QTextEdit):
reSelect = True reSelect = True
if reSelect: if reSelect:
theCursor.clearSelection() theCursor.clearSelection()
theCursor.setPosition(posE-1) theCursor.setPosition(posS, QTextCursor.MoveAnchor)
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, posE-posS-1) theCursor.setPosition(posE-1, QTextCursor.KeepAnchor)
self.setTextCursor(theCursor) self.setTextCursor(theCursor)
return theCursor return theCursor
def _toggleFormat(self, fLen, fChar): def _toggleFormat(self, fLen, fChar):
"""Toggle strikethrough text. """Toggle the formatting of a specific type for a piece of text.
If more than one block is selected, the formatting is applied to
the first block.
""" """
theCursor = self._autoSelect() theCursor = self._autoSelect()
if theCursor.hasSelection(): if theCursor.hasSelection():
posS = theCursor.selectionStart() posS = theCursor.selectionStart()
posE = theCursor.selectionEnd() posE = theCursor.selectionEnd()
blockS = self.qDocument.findBlock(posS)
blockE = self.qDocument.findBlock(posE)
if blockS != blockE:
posE = blockS.position() + blockS.length() - 1
theCursor.clearSelection()
theCursor.setPosition(posS, QTextCursor.MoveAnchor)
theCursor.setPosition(posE, QTextCursor.KeepAnchor)
self.setTextCursor(theCursor)
numB = 0 numB = 0
for n in range(fLen): for n in range(fLen):
if self.qDocument.characterAt(posS-n-1) == fChar: if self.qDocument.characterAt(posS-n-1) == fChar:
@@ -1342,7 +1446,10 @@ class GuiDocEditor(QTextEdit):
theCursor.clearSelection() theCursor.clearSelection()
theCursor.select(selMode) theCursor.select(selMode)
if selMode == QTextCursor.BlockUnderCursor: if selMode == QTextCursor.WordUnderCursor:
theCursor = self._autoSelect()
elif selMode == QTextCursor.BlockUnderCursor:
# This selection mode also selects the preceding oaragraph # This selection mode also selects the preceding oaragraph
# separator, which we want to avoid. # separator, which we want to avoid.
posS = theCursor.selectionStart() posS = theCursor.selectionStart()
@@ -1517,33 +1624,42 @@ class GuiDocEditor(QTextEdit):
# END Class GuiDocEditor # END Class GuiDocEditor
# =============================================================================================== # # =============================================================================================== #
# The Off GUI Thread Word Counter # The Off-GUI Thread Word Counter
# Runs the word counter in the background for the DocEditor # A runnable for the word counter to be run in the thread pool off the main GUI thread.
# =============================================================================================== # # =============================================================================================== #
class BackgroundWordCounter(QThread): class BackgroundWordCounter(QRunnable):
def __init__(self, docEditor): def __init__(self, docEditor):
QThread.__init__(self, docEditor) QRunnable.__init__(self)
self.docEditor = docEditor self.docEditor = docEditor
self.charCount = 0 self.signals = BackgroundWordCounterSignals()
self.wordCount = 0 self._isRunning = False
self.paraCount = 0
return return
def isRunning(self):
return self._isRunning
@pyqtSlot()
def run(self): def run(self):
"""Overloaded run function for the word counter, forwarding the """Overloaded run function for the word counter, forwarding the
call to the function that does the actual counting. call to the function that does the actual counting.
""" """
self._isRunning = True
theText = self.docEditor.getText() theText = self.docEditor.getText()
cC, wC, pC = countWords(theText) cC, wC, pC = countWords(theText)
self.charCount = cC self.signals.countsReady.emit(cC, wC, pC)
self.wordCount = wC self._isRunning = False
self.paraCount = pC
return return
## END Class BackgroundWordCounter ## END Class BackgroundWordCounter
class BackgroundWordCounterSignals(QObject):
countsReady = pyqtSignal(int, int, int)
# END Class BackgroundWordCounterSignals
# =============================================================================================== # # =============================================================================================== #
# The Embedded Document Search/Replace Feature # The Embedded Document Search/Replace Feature
# Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport # Only used by DocEditor, and is at a fixed position in the QTextEdit's viewport
@@ -2154,6 +2270,7 @@ class GuiDocEditFooter(QWidget):
self.sPx = int(round(0.9*self.theTheme.baseIconSize)) self.sPx = int(round(0.9*self.theTheme.baseIconSize))
fPx = int(0.9*self.theTheme.fontPixelSize) fPx = int(0.9*self.theTheme.fontPixelSize)
bSp = self.mainConf.pxInt(4) bSp = self.mainConf.pxInt(4)
hSp = self.mainConf.pxInt(6)
lblFont = self.font() lblFont = self.font()
lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize) lblFont.setPointSizeF(0.9*self.theTheme.fontPointSize)
@@ -2179,6 +2296,23 @@ class GuiDocEditFooter(QWidget):
self.statusText.setPalette(self.thePalette) self.statusText.setPalette(self.thePalette)
self.statusText.setFont(lblFont) self.statusText.setFont(lblFont)
# Lines
self.linesIcon = QLabel("")
self.linesIcon.setPixmap(self.theTheme.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)
self.linesText = QLabel("Line: 0")
self.linesText.setIndent(0)
self.linesText.setMargin(0)
self.linesText.setContentsMargins(0, 0, 0, 0)
self.linesText.setAutoFillBackground(True)
self.linesText.setFixedHeight(fPx)
self.linesText.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.linesText.setPalette(self.thePalette)
self.linesText.setFont(lblFont)
# Words # Words
self.wordsIcon = QLabel("") self.wordsIcon = QLabel("")
self.wordsIcon.setPixmap(self.theTheme.getPixmap("status_stats", (self.sPx, self.sPx))) self.wordsIcon.setPixmap(self.theTheme.getPixmap("status_stats", (self.sPx, self.sPx)))
@@ -2202,6 +2336,9 @@ class GuiDocEditFooter(QWidget):
self.outerBox.addWidget(self.statusIcon) self.outerBox.addWidget(self.statusIcon)
self.outerBox.addWidget(self.statusText) self.outerBox.addWidget(self.statusText)
self.outerBox.addStretch(1) self.outerBox.addStretch(1)
self.outerBox.addWidget(self.linesIcon)
self.outerBox.addWidget(self.linesText)
self.outerBox.addSpacing(hSp)
self.outerBox.addWidget(self.wordsIcon) self.outerBox.addWidget(self.wordsIcon)
self.outerBox.addWidget(self.wordsText) self.outerBox.addWidget(self.wordsText)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
@@ -2251,8 +2388,23 @@ class GuiDocEditFooter(QWidget):
return return
def updateLineCount(self):
"""Update the word count.
"""
if self.theItem is None:
iLine = 0
iDist = 0
else:
theCursor = self.docEditor.textCursor()
iLine = theCursor.blockNumber() + 1
iDist = 100*iLine/self.docEditor.qDocument.blockCount()
self.linesText.setText(f"Line: {iLine:n} ({iDist:.0f}\u202f%)")
return
def updateCounts(self): def updateCounts(self):
"""Update the word counts. """Update the word count.
""" """
if self.theItem is None: if self.theItem is None:
wCount = 0 wCount = 0
@@ -2261,10 +2413,10 @@ class GuiDocEditFooter(QWidget):
wCount = self.theItem.wordCount wCount = self.theItem.wordCount
wDiff = wCount - self.theItem.initCount wDiff = wCount - self.theItem.initCount
self.wordsText.setText("Words: {:n} ({:+n})".format(wCount, wDiff)) self.wordsText.setText(f"Words: {wCount:n} ({wDiff:+n})")
byteSize = self.docEditor.qDocument.characterCount() byteSize = self.docEditor.qDocument.characterCount()
self.wordsText.setToolTip("Document size is {:n} bytes".format(byteSize)) self.wordsText.setToolTip(f"Document size is {byteSize:n} bytes")
return return
+24 -13
View File
@@ -3,7 +3,7 @@
novelWriter GUI Document Highlighter novelWriter GUI Document Highlighter
======================================== ========================================
Syntax highlighting for MarkDown Subclass for the main editor syntax highlighting
File History: File History:
Created: 2019-04-06 [0.0.1] Created: 2019-04-06 [0.0.1]
@@ -28,6 +28,8 @@
import nw import nw
import logging import logging
from time import time
from PyQt5.QtCore import Qt, QRegularExpression from PyQt5.QtCore import Qt, QRegularExpression
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush
@@ -66,7 +68,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.colDialN = QColor(0, 0, 0) self.colDialN = QColor(0, 0, 0)
self.colDialD = QColor(0, 0, 0) self.colDialD = QColor(0, 0, 0)
self.colDialS = QColor(0, 0, 0) self.colDialS = QColor(0, 0, 0)
self.colComm = QColor(0, 0, 0) self.colHidden = QColor(0, 0, 0)
self.colKey = QColor(0, 0, 0) self.colKey = QColor(0, 0, 0)
self.colVal = QColor(0, 0, 0) self.colVal = QColor(0, 0, 0)
self.colSpell = QColor(0, 0, 0) self.colSpell = QColor(0, 0, 0)
@@ -90,7 +92,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
self.colDialN = QColor(*self.theTheme.colDialN) self.colDialN = QColor(*self.theTheme.colDialN)
self.colDialD = QColor(*self.theTheme.colDialD) self.colDialD = QColor(*self.theTheme.colDialD)
self.colDialS = QColor(*self.theTheme.colDialS) self.colDialS = QColor(*self.theTheme.colDialS)
self.colComm = QColor(*self.theTheme.colComm) self.colHidden = QColor(*self.theTheme.colHidden)
self.colKey = QColor(*self.theTheme.colKey) self.colKey = QColor(*self.theTheme.colKey)
self.colVal = QColor(*self.theTheme.colVal) self.colVal = QColor(*self.theTheme.colVal)
self.colSpell = QColor(*self.theTheme.colSpell) self.colSpell = QColor(*self.theTheme.colSpell)
@@ -116,14 +118,14 @@ class GuiDocHighlighter(QSyntaxHighlighter):
"header4h" : self._makeFormat(self.colHeadH, "bold", 1.2), "header4h" : self._makeFormat(self.colHeadH, "bold", 1.2),
"bold" : self._makeFormat(self.colEmph, "bold"), "bold" : self._makeFormat(self.colEmph, "bold"),
"italic" : self._makeFormat(self.colEmph, "italic"), "italic" : self._makeFormat(self.colEmph, "italic"),
"strike" : self._makeFormat(self.colEmph, "strike"), "strike" : self._makeFormat(self.colHidden, "strike"),
"trailing" : self._makeFormat(self.colTrail, "background"), "trailing" : self._makeFormat(self.colTrail, "background"),
"nobreak" : self._makeFormat(self.colTrail, "background"), "nobreak" : self._makeFormat(self.colTrail, "background"),
"dialogue1" : self._makeFormat(self.colDialN), "dialogue1" : self._makeFormat(self.colDialN),
"dialogue2" : self._makeFormat(self.colDialD), "dialogue2" : self._makeFormat(self.colDialD),
"dialogue3" : self._makeFormat(self.colDialS), "dialogue3" : self._makeFormat(self.colDialS),
"replace" : self._makeFormat(self.colRepTag), "replace" : self._makeFormat(self.colRepTag),
"hidden" : self._makeFormat(self.colComm), "hidden" : self._makeFormat(self.colHidden),
"keyword" : self._makeFormat(self.colKey), "keyword" : self._makeFormat(self.colKey),
"modifier" : self._makeFormat(self.colMod), "modifier" : self._makeFormat(self.colMod),
"value" : self._makeFormat(self.colVal, "underline"), "value" : self._makeFormat(self.colVal, "underline"),
@@ -147,32 +149,36 @@ class GuiDocHighlighter(QSyntaxHighlighter):
# Quoted Strings # Quoted Strings
if self.mainConf.highlightQuotes: if self.mainConf.highlightQuotes:
fmtDO = self.mainConf.fmtDoubleQuotes[0]
fmtDC = self.mainConf.fmtDoubleQuotes[1]
fmtSO = self.mainConf.fmtSingleQuotes[0]
fmtSC = self.mainConf.fmtSingleQuotes[1]
self.hRules.append(( self.hRules.append((
"\\B{:s}(.*?){:s}\\B".format('"', '"'), { "\\B\"(.*?)\"\\B", {
0 : self.hStyles["dialogue1"], 0 : self.hStyles["dialogue1"],
} }
)) ))
self.hRules.append(( self.hRules.append((
"\\B{:s}(.*?){:s}\\B".format(*self.mainConf.fmtDoubleQuotes), { f"\\B{fmtDO:s}(.*?){fmtDC:s}\\B", {
0 : self.hStyles["dialogue2"], 0 : self.hStyles["dialogue2"],
} }
)) ))
self.hRules.append(( self.hRules.append((
"\\B{:s}(.*?){:s}\\B".format(*self.mainConf.fmtSingleQuotes), { f"\\B{fmtSO:s}(.*?){fmtSC:s}\\B", {
0 : self.hStyles["dialogue3"], 0 : self.hStyles["dialogue3"],
} }
)) ))
# Markdown # Markdown
self.hRules.append(( self.hRules.append((
nwRegEx.FMT_I, { nwRegEx.FMT_EI, {
1 : self.hStyles["hidden"], 1 : self.hStyles["hidden"],
2 : self.hStyles["italic"], 2 : self.hStyles["italic"],
3 : self.hStyles["hidden"], 3 : self.hStyles["hidden"],
} }
)) ))
self.hRules.append(( self.hRules.append((
nwRegEx.FMT_B, { nwRegEx.FMT_EB, {
1 : self.hStyles["hidden"], 1 : self.hStyles["hidden"],
2 : self.hStyles["bold"], 2 : self.hStyles["bold"],
3 : self.hStyles["hidden"], 3 : self.hStyles["hidden"],
@@ -203,7 +209,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
# Build a QRegExp for spell checker # Build a QRegExp for spell checker
# Include additional characters that the highlighter should # Include additional characters that the highlighter should
# consider to be word separators # consider to be word separators
wordSep = r"_\+/" wordSep = r"\-_\+/"
wordSep += nwUnicode.U_ENDASH wordSep += nwUnicode.U_ENDASH
wordSep += nwUnicode.U_EMDASH wordSep += nwUnicode.U_EMDASH
self.spellRx = QRegularExpression(r"\b[^\s"+wordSep+r"]+\b") self.spellRx = QRegularExpression(r"\b[^\s"+wordSep+r"]+\b")
@@ -244,10 +250,15 @@ class GuiDocHighlighter(QSyntaxHighlighter):
""" """
qDocument = self.document() qDocument = self.document()
nBlocks = qDocument.blockCount() nBlocks = qDocument.blockCount()
bfTime = time()
for i in range(nBlocks): for i in range(nBlocks):
theBlock = qDocument.findBlockByNumber(i) theBlock = qDocument.findBlockByNumber(i)
if theBlock.userState() & theType == theType: if theBlock.userState() & theType > 0:
self.rehighlightBlock(theBlock) self.rehighlightBlock(theBlock)
afTime = time()
logger.debug(
"Document highlighted in %.3f ms" % (1000*(afTime-bfTime))
)
return return
## ##
@@ -343,7 +354,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
while rxSpell.hasNext(): while rxSpell.hasNext():
rxMatch = rxSpell.next() rxMatch = rxSpell.next()
if not self.theDict.checkWord(rxMatch.captured(0)): if not self.theDict.checkWord(rxMatch.captured(0)):
if rxMatch.captured(0) == rxMatch.captured(0).upper(): if rxMatch.captured(0).isupper() or rxMatch.captured(0).isnumeric():
continue continue
xPos = rxMatch.capturedStart(0) xPos = rxMatch.capturedStart(0)
xLen = rxMatch.capturedLength(0) xLen = rxMatch.capturedLength(0)
+2 -2
View File
@@ -3,7 +3,7 @@
novelWriter GUI Doc Merge novelWriter GUI Doc Merge
============================= =============================
Tool for merging multiple documents to one Tool for merging multiple documents to one document
File History: File History:
Created: 2020-01-23 [0.4.3] Created: 2020-01-23 [0.4.3]
@@ -127,7 +127,7 @@ class GuiDocMerge(QDialog):
), nwAlert.ERROR) ), nwAlert.ERROR)
return return
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.parHandle) nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent)
newItem = self.theProject.projTree[nHandle] newItem = self.theProject.projTree[nHandle]
newItem.setStatus(srcItem.itemStatus) newItem.setStatus(srcItem.itemStatus)
+2 -2
View File
@@ -154,7 +154,7 @@ class GuiDocSplit(QDialog):
return return
# Check that another folder can be created # Check that another folder can be created
parTree = self.theProject.projTree.getItemPath(srcItem.parHandle) parTree = self.theProject.projTree.getItemPath(srcItem.itemParent)
if len(parTree) >= nwConst.maxDepth - 1: if len(parTree) >= nwConst.maxDepth - 1:
self.theParent.makeAlert(( self.theParent.makeAlert((
"Cannot add new folder for the document split. " "Cannot add new folder for the document split. "
@@ -176,7 +176,7 @@ class GuiDocSplit(QDialog):
# Create the folder # Create the folder
fHandle = self.theProject.newFolder( fHandle = self.theProject.newFolder(
srcItem.itemName, srcItem.itemClass, srcItem.parHandle srcItem.itemName, srcItem.itemClass, srcItem.itemParent
) )
self.theParent.treeView.revealNewTreeItem(fHandle) self.theParent.treeView.revealNewTreeItem(fHandle)
logger.verbose("Creating folder %s" % fHandle) logger.verbose("Creating folder %s" % fHandle)
+44 -30
View File
@@ -3,7 +3,7 @@
novelWriter GUI Document Viewer novelWriter GUI Document Viewer
=================================== ===================================
Class holding the document html viewer Class holding the main document viewer
File History: File History:
Created: 2019-05-10 [0.0.1] GuiDocViewer Created: 2019-05-10 [0.0.1] GuiDocViewer
@@ -34,10 +34,10 @@ import logging
from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor
) )
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton, qApp, QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton,
QAction, QMenu QAction, QMenu
) )
@@ -124,11 +124,26 @@ class GuiDocViewer(QTextBrowser):
theOpt.setAlignment(Qt.AlignJustify) theOpt.setAlignment(Qt.AlignJustify)
self.qDocument.setDefaultTextOption(theOpt) self.qDocument.setDefaultTextOption(theOpt)
# 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)
# Refresh the tab stops
if self.mainConf.verQtValue >= 51000:
self.setTabStopDistance(self.mainConf.getTabWidth())
else:
self.setTabStopWidth(self.mainConf.getTabWidth())
# If we have a document open, we should reload it in case the font changed # If we have a document open, we should reload it in case the font changed
if self.theHandle is not None: if self.theHandle is not None:
tHandle = self.theHandle self.redrawText()
self.clearViewer()
self.loadText(tHandle)
return True return True
@@ -144,6 +159,8 @@ class GuiDocViewer(QTextBrowser):
return False return False
logger.debug("Generating preview for item %s" % tHandle) logger.debug("Generating preview for item %s" % tHandle)
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
sPos = self.verticalScrollBar().value() sPos = self.verticalScrollBar().value()
aDoc = ToHtml(self.theProject, self.theParent) aDoc = ToHtml(self.theProject, self.theParent)
aDoc.setPreview(True, self.mainConf.viewComments, self.mainConf.viewSynopsis) aDoc.setPreview(True, self.mainConf.viewComments, self.mainConf.viewSynopsis)
@@ -195,7 +212,8 @@ class GuiDocViewer(QTextBrowser):
# Since we change the content while it may still be rendering, we mark # 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. # the document dirty again to make sure it's re-rendered properly.
self.qDocument.markContentsDirty(0, self.qDocument.characterCount()) self.redrawText()
qApp.restoreOverrideCursor()
return True return True
@@ -205,6 +223,12 @@ class GuiDocViewer(QTextBrowser):
self.loadText(self.theHandle, updateHistory=False) self.loadText(self.theHandle, updateHistory=False)
return return
def redrawText(self):
"""Redraw the text by marking the document content as "dirty".
"""
self.qDocument.markContentsDirty(0, self.qDocument.characterCount())
return
def loadFromTag(self, theTag): def loadFromTag(self, theTag):
"""Load text in the document from a reference given by a meta """Load text in the document from a reference given by a meta
tag rather than a known handle. This function depends on the tag rather than a known handle. This function depends on the
@@ -477,55 +501,45 @@ class GuiDocViewer(QTextBrowser):
""" """
styleSheet = ( styleSheet = (
"body {{" "body {{"
" color: rgb({tColR},{tColG},{tColB});" " color: rgb({tColR}, {tColG}, {tColB});"
" font-size: {textSize:.1f}pt;"
"}}\n" "}}\n"
"h1, h2, h3, h4 {{" "h1, h2, h3, h4 {{"
" color: rgb({hColR},{hColG},{hColB});" " color: rgb({hColR}, {hColG}, {hColB});"
"}}\n" "}}\n"
"a {{" "a {{"
" color: rgb({aColR},{aColG},{aColB});" " color: rgb({aColR}, {aColG}, {aColB});"
"}}\n" "}}\n"
"mark {{" "mark {{"
" color: rgb({eColR},{eColG},{eColB});" " color: rgb({eColR}, {eColG}, {eColB});"
"}}\n"
"table {{"
" margin: 10px 0px;"
"}}\n"
"td {{"
" padding: 0px 4px;"
"}}\n" "}}\n"
".tags {{" ".tags {{"
" color: rgb({kColR},{kColG},{kColB});" " color: rgb({kColR}, {kColG}, {kColB});"
" font-wright: bold;"
"}}\n" "}}\n"
".comment {{" ".comment {{"
" color: rgb({cColR},{cColG},{cColB});" " color: rgb({cColR}, {cColG}, {cColB});"
"}}\n" "}}\n"
".synopsis {{" ".synopsis {{"
" color: rgb({mColR},{mColG},{mColB});" " color: rgb({mColR}, {mColG}, {mColB});"
" font-wright: bold;"
"}}\n" "}}\n"
).format( ).format(
textSize = self.mainConf.textSize,
tColR = self.theTheme.colText[0], tColR = self.theTheme.colText[0],
tColG = self.theTheme.colText[1], tColG = self.theTheme.colText[1],
tColB = self.theTheme.colText[2], tColB = self.theTheme.colText[2],
hColR = self.theTheme.colHead[0], hColR = self.theTheme.colHead[0],
hColG = self.theTheme.colHead[1], hColG = self.theTheme.colHead[1],
hColB = self.theTheme.colHead[2], hColB = self.theTheme.colHead[2],
cColR = self.theTheme.colComm[0],
cColG = self.theTheme.colComm[1],
cColB = self.theTheme.colComm[2],
eColR = self.theTheme.colEmph[0],
eColG = self.theTheme.colEmph[1],
eColB = self.theTheme.colEmph[2],
aColR = self.theTheme.colVal[0], aColR = self.theTheme.colVal[0],
aColG = self.theTheme.colVal[1], aColG = self.theTheme.colVal[1],
aColB = self.theTheme.colVal[2], 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], kColR = self.theTheme.colKey[0],
kColG = self.theTheme.colKey[1], kColG = self.theTheme.colKey[1],
kColB = self.theTheme.colKey[2], 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], mColR = self.theTheme.colMod[0],
mColG = self.theTheme.colMod[1], mColG = self.theTheme.colMod[1],
mColB = self.theTheme.colMod[2], mColB = self.theTheme.colMod[2],
+7 -7
View File
@@ -3,7 +3,7 @@
novelWriter GUI Document Details novelWriter GUI Document Details
==================================== ====================================
Class holding the left side document details panel Class holding the project tree item details panel
File History: File History:
Created: 2019-04-24 [0.0.1] Created: 2019-04-24 [0.0.1]
@@ -195,9 +195,9 @@ class GuiItemDetails(QWidget):
we're already showing. we're already showing.
""" """
if tHandle == self.theHandle: if tHandle == self.theHandle:
self.cCountData.setText("{:n}".format(cC)) self.cCountData.setText(f"{cC:n}")
self.wCountData.setText("{:n}".format(wC)) self.wCountData.setText(f"{wC:n}")
self.pCountData.setText("{:n}".format(pC)) self.pCountData.setText(f"{pC:n}")
return return
def updateViewBox(self, tHandle): def updateViewBox(self, tHandle):
@@ -252,9 +252,9 @@ class GuiItemDetails(QWidget):
self.layoutData.setText(nwLabels.LAYOUT_NAME[nwItem.itemLayout]) self.layoutData.setText(nwLabels.LAYOUT_NAME[nwItem.itemLayout])
if nwItem.itemType == nwItemType.FILE: if nwItem.itemType == nwItemType.FILE:
self.cCountData.setText("{:n}".format(nwItem.charCount)) self.cCountData.setText(f"{nwItem.charCount:n}")
self.wCountData.setText("{:n}".format(nwItem.wordCount)) self.wCountData.setText(f"{nwItem.wordCount:n}")
self.pCountData.setText("{:n}".format(nwItem.paraCount)) self.pCountData.setText(f"{nwItem.paraCount:n}")
else: else:
self.cCountData.setText("") self.cCountData.setText("")
self.wCountData.setText("") self.wCountData.setText("")
+1 -1
View File
@@ -3,7 +3,7 @@
novelWriter GUI Item Editor novelWriter GUI Item Editor
=============================== ===============================
Class holding the item editor Class holding the item editor dialog
File History: File History:
Created: 2019-04-27 [0.0.1] Created: 2019-04-27 [0.0.1]
+9 -5
View File
@@ -3,10 +3,10 @@
novelWriter GUI Main Menu novelWriter GUI Main Menu
============================= =============================
Class holding the main window Class holding the main window menu
File History: File History:
Created: 2019-04-27 [0.0.1] (Split from winmain) Created: 2019-04-27 [0.0.1]
This file is a part of novelWriter This file is a part of novelWriter
Copyright 20182020, Veronica Berglyd Olsen Copyright 20182020, Veronica Berglyd Olsen
@@ -274,8 +274,9 @@ class GuiMainMenu(QMenuBar):
# Project > Exit # Project > Exit
self.aExitNW = QAction("Exit", self) self.aExitNW = QAction("Exit", self)
self.aExitNW.setStatusTip("Exit %s" % self.mainConf.appName) self.aExitNW.setStatusTip("Exit novelWriter")
self.aExitNW.setShortcut("Ctrl+Q") self.aExitNW.setShortcut("Ctrl+Q")
self.aExitNW.setMenuRole(QAction.QuitRole)
self.aExitNW.triggered.connect(lambda: self.theParent.closeMain()) self.aExitNW.triggered.connect(lambda: self.theParent.closeMain())
self.projMenu.addAction(self.aExitNW) self.projMenu.addAction(self.aExitNW)
@@ -843,6 +844,7 @@ class GuiMainMenu(QMenuBar):
self.aPreferences = QAction("Preferences", self) self.aPreferences = QAction("Preferences", self)
self.aPreferences.setStatusTip("Preferences") self.aPreferences.setStatusTip("Preferences")
self.aPreferences.setShortcut("Ctrl+,") self.aPreferences.setShortcut("Ctrl+,")
self.aPreferences.setMenuRole(QAction.PreferencesRole)
self.aPreferences.triggered.connect(lambda: self.theParent.showPreferencesDialog()) self.aPreferences.triggered.connect(lambda: self.theParent.showPreferencesDialog())
self.toolsMenu.addAction(self.aPreferences) self.toolsMenu.addAction(self.aPreferences)
@@ -855,14 +857,16 @@ class GuiMainMenu(QMenuBar):
self.helpMenu = self.addMenu("&Help") self.helpMenu = self.addMenu("&Help")
# Help > About # Help > About
self.aAboutNW = QAction("About %s" % self.mainConf.appName, self) self.aAboutNW = QAction("About novelWriter", self)
self.aAboutNW.setStatusTip("About %s" % self.mainConf.appName) self.aAboutNW.setStatusTip("About novelWriter")
self.aAboutNW.setMenuRole(QAction.AboutRole)
self.aAboutNW.triggered.connect(lambda: self.theParent.showAboutNWDialog()) self.aAboutNW.triggered.connect(lambda: self.theParent.showAboutNWDialog())
self.helpMenu.addAction(self.aAboutNW) self.helpMenu.addAction(self.aAboutNW)
# Help > About Qt5 # Help > About Qt5
self.aAboutQt = QAction("About Qt5", self) self.aAboutQt = QAction("About Qt5", self)
self.aAboutQt.setStatusTip("About Qt5") self.aAboutQt.setStatusTip("About Qt5")
self.aAboutQt.setMenuRole(QAction.AboutQtRole)
self.aAboutQt.triggered.connect(lambda: self.theParent.showAboutQtDialog()) self.aAboutQt.triggered.connect(lambda: self.theParent.showAboutQtDialog())
self.helpMenu.addAction(self.aAboutQt) self.helpMenu.addAction(self.aAboutQt)
+24 -3
View File
@@ -118,6 +118,7 @@ class GuiOutline(QTreeWidget):
self.colIndex = {} self.colIndex = {}
self.treeNCols = 0 self.treeNCols = 0
self.initOutline()
self.clearOutline() self.clearOutline()
self.headerMenu.setHiddenState(self.colHidden) self.headerMenu.setHiddenState(self.colHidden)
@@ -125,6 +126,22 @@ class GuiOutline(QTreeWidget):
return return
def initOutline(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 clearOutline(self): def clearOutline(self):
"""Clear the tree and header and set the default values for the """Clear the tree and header and set the default values for the
columns arrays. columns arrays.
@@ -422,6 +439,10 @@ class GuiOutline(QTreeWidget):
newItem = QTreeWidgetItem() newItem = QTreeWidgetItem()
hIcon = "doc_%s" % tLevel.lower() hIcon = "doc_%s" % tLevel.lower()
cC = int(novIdx["cCount"])
wC = int(novIdx["wCount"])
pC = int(novIdx["pCount"])
newItem.setText(self.colIndex[nwOutline.TITLE], novIdx["title"]) newItem.setText(self.colIndex[nwOutline.TITLE], novIdx["title"])
newItem.setData(self.colIndex[nwOutline.TITLE], Qt.UserRole, tHandle) newItem.setData(self.colIndex[nwOutline.TITLE], Qt.UserRole, tHandle)
newItem.setIcon(self.colIndex[nwOutline.TITLE], self.theTheme.getIcon(hIcon)) newItem.setIcon(self.colIndex[nwOutline.TITLE], self.theTheme.getIcon(hIcon))
@@ -431,9 +452,9 @@ class GuiOutline(QTreeWidget):
newItem.setText(self.colIndex[nwOutline.LINE], sTitle[1:].lstrip("0")) newItem.setText(self.colIndex[nwOutline.LINE], sTitle[1:].lstrip("0"))
newItem.setData(self.colIndex[nwOutline.LINE], Qt.UserRole, sTitle) newItem.setData(self.colIndex[nwOutline.LINE], Qt.UserRole, sTitle)
newItem.setText(self.colIndex[nwOutline.SYNOP], novIdx["synopsis"]) newItem.setText(self.colIndex[nwOutline.SYNOP], novIdx["synopsis"])
newItem.setText(self.colIndex[nwOutline.CCOUNT], str(novIdx["cCount"])) newItem.setText(self.colIndex[nwOutline.CCOUNT], f"{cC:n}")
newItem.setText(self.colIndex[nwOutline.WCOUNT], str(novIdx["wCount"])) newItem.setText(self.colIndex[nwOutline.WCOUNT], f"{wC:n}")
newItem.setText(self.colIndex[nwOutline.PCOUNT], str(novIdx["pCount"])) newItem.setText(self.colIndex[nwOutline.PCOUNT], f"{pC:n}")
newItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight) newItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight) newItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight) newItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight)
+29 -7
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter GUI Project Outline """novelWriter GUI Project Outline Details
novelWriter GUI Project Outline novelWriter GUI Project Outline Details
=================================== ===========================================
Class holding the project outline view Class holding the project outline details panel
File History: File History:
Created: 2020-06-02 [0.7.0] Created: 2020-06-02 [0.7.0]
@@ -224,10 +224,28 @@ class GuiOutlineDetails(QScrollArea):
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setWidgetResizable(True) self.setWidgetResizable(True)
self.initDetails()
logger.debug("GuiOutlineDetails initialisation complete") logger.debug("GuiOutlineDetails initialisation complete")
return 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 showItem(self, tHandle, sTitle): def showItem(self, tHandle, sTitle):
"""Update the content of the tree with the given handle and line """Update the content of the tree with the given handle and line
number pointing to a header. number pointing to a header.
@@ -248,9 +266,13 @@ class GuiOutlineDetails(QScrollArea):
self.fileValue.setText(nwItem.itemName) self.fileValue.setText(nwItem.itemName)
self.itemValue.setText(nwItem.itemStatus) self.itemValue.setText(nwItem.itemStatus)
self.cCValue.setText("{:n}".format(checkInt(novIdx["cCount"], 0))) cC = checkInt(novIdx["cCount"], 0)
self.wCValue.setText("{:n}".format(checkInt(novIdx["wCount"], 0))) wC = checkInt(novIdx["wCount"], 0)
self.pCValue.setText("{:n}".format(checkInt(novIdx["pCount"], 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.synopValue.setText(novIdx["synopsis"])
+147 -45
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter GUI Config Editor """novelWriter GUI Preferences
novelWriter GUI Config Editor novelWriter GUI Preferences
================================= ===============================
Class holding the config dialog Class holding the preferences dialog
File History: File History:
Created: 2019-06-10 [0.1.5] Created: 2019-06-10 [0.1.5]
@@ -55,15 +55,17 @@ class GuiPreferences(PagedDialog):
self.setWindowTitle("Preferences") self.setWindowTitle("Preferences")
self.tabGeneral = GuiConfigEditGeneralTab(self.theParent) self.tabGeneral = GuiConfigEditGeneralTab(self.theParent)
self.tabLayout = GuiConfigEditLayoutTab(self.theParent) self.tabProjects = GuiConfigEditProjectsTab(self.theParent)
self.tabEditing = GuiConfigEditEditingTab(self.theParent) self.tabLayout = GuiConfigEditLayoutTab(self.theParent)
self.tabAutoRep = GuiConfigEditAutoReplaceTab(self.theParent) self.tabEditing = GuiConfigEditEditingTab(self.theParent)
self.tabAutoRep = GuiConfigEditAutoReplaceTab(self.theParent)
self.addTab(self.tabGeneral, "General") self.addTab(self.tabGeneral, "General")
self.addTab(self.tabLayout, "Text Layout") self.addTab(self.tabProjects, "Projects")
self.addTab(self.tabEditing, "Editor") self.addTab(self.tabLayout, "Text Layout")
self.addTab(self.tabAutoRep, "Auto-Replace") self.addTab(self.tabEditing, "Editor")
self.addTab(self.tabAutoRep, "Auto-Replace")
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self._doSave) self.buttonBox.accepted.connect(self._doSave)
@@ -91,6 +93,10 @@ class GuiPreferences(PagedDialog):
validEntries &= retA validEntries &= retA
needsRestart |= retB needsRestart |= retB
retA, retB = self.tabProjects.saveValues()
validEntries &= retA
needsRestart |= retB
retA, retB = self.tabLayout.saveValues() retA, retB = self.tabLayout.saveValues()
validEntries &= retA validEntries &= retA
needsRestart |= retB needsRestart |= retB
@@ -219,9 +225,94 @@ class GuiConfigEditGeneralTab(QWidget):
self.showFullPath.setChecked(self.mainConf.showFullPath) self.showFullPath.setChecked(self.mainConf.showFullPath)
self.mainForm.addRow( self.mainForm.addRow(
"Show full path in document header", "Show full path in document header",
self.showFullPath self.showFullPath,
"Shows the document title and parent folder names."
) )
self.hideVScroll = QSwitch()
self.hideVScroll.setChecked(self.mainConf.hideVScroll)
self.mainForm.addRow(
"Hide vertical scroll bars in main windows",
self.hideVScroll,
"Scrolling with mouse wheel and keys only."
)
self.hideHScroll = QSwitch()
self.hideHScroll.setChecked(self.mainConf.hideHScroll)
self.mainForm.addRow(
"Hide horizontal scroll bars in main windows",
self.hideHScroll,
"Scrolling with mouse wheel and keys only."
)
return
def saveValues(self):
"""Save the values set for this tab.
"""
validEntries = True
needsRestart = False
guiTheme = self.selectTheme.currentData()
guiIcons = self.selectIcons.currentData()
guiDark = self.preferDarkIcons.isChecked()
guiFont = self.guiFont.text()
guiFontSize = self.guiFontSize.value()
showFullPath = self.showFullPath.isChecked()
hideVScroll = self.hideVScroll.isChecked()
hideHScroll = self.hideHScroll.isChecked()
# Check if restart is needed
needsRestart |= self.mainConf.guiTheme != guiTheme
needsRestart |= self.mainConf.guiIcons != guiIcons
needsRestart |= self.mainConf.guiFont != guiFont
needsRestart |= self.mainConf.guiFontSize != guiFontSize
self.mainConf.guiTheme = guiTheme
self.mainConf.guiIcons = guiIcons
self.mainConf.guiDark = guiDark
self.mainConf.guiFont = guiFont
self.mainConf.guiFontSize = guiFontSize
self.mainConf.showFullPath = showFullPath
self.mainConf.hideVScroll = hideVScroll
self.mainConf.hideHScroll = hideHScroll
self.mainConf.confChanged = True
return validEntries, needsRestart
##
# Slots
##
def _selectFont(self):
"""Open the QFontDialog and set a font for the font style.
"""
currFont = QFont()
currFont.setFamily(self.mainConf.guiFont)
currFont.setPointSize(self.mainConf.guiFontSize)
theFont, theStatus = QFontDialog.getFont(currFont, self)
if theStatus:
self.guiFont.setText(theFont.family())
self.guiFontSize.setValue(theFont.pointSize())
return
# END Class GuiConfigEditGeneralTab
class GuiConfigEditProjectsTab(QWidget):
def __init__(self, theParent):
QWidget.__init__(self, theParent)
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
# The Form
self.mainForm = QConfigLayout()
self.mainForm.setHelpTextStyle(self.theTheme.helpText)
self.setLayout(self.mainForm)
# AutoSave Settings # AutoSave Settings
# ================= # =================
self.mainForm.addGroupLabel("Automatic Save") self.mainForm.addGroupLabel("Automatic Save")
@@ -292,30 +383,12 @@ class GuiConfigEditGeneralTab(QWidget):
validEntries = True validEntries = True
needsRestart = False needsRestart = False
guiTheme = self.selectTheme.currentData()
guiIcons = self.selectIcons.currentData()
guiDark = self.preferDarkIcons.isChecked()
guiFont = self.guiFont.text()
guiFontSize = self.guiFontSize.value()
showFullPath = self.showFullPath.isChecked()
autoSaveDoc = self.autoSaveDoc.value() autoSaveDoc = self.autoSaveDoc.value()
autoSaveProj = self.autoSaveProj.value() autoSaveProj = self.autoSaveProj.value()
backupPath = self.backupPath backupPath = self.backupPath
backupOnClose = self.backupOnClose.isChecked() backupOnClose = self.backupOnClose.isChecked()
askBeforeBackup = self.askBeforeBackup.isChecked() askBeforeBackup = self.askBeforeBackup.isChecked()
# Check if restart is needed
needsRestart |= self.mainConf.guiTheme != guiTheme
needsRestart |= self.mainConf.guiIcons != guiIcons
needsRestart |= self.mainConf.guiFont != guiFont
needsRestart |= self.mainConf.guiFontSize != guiFontSize
self.mainConf.guiTheme = guiTheme
self.mainConf.guiIcons = guiIcons
self.mainConf.guiDark = guiDark
self.mainConf.guiFont = guiFont
self.mainConf.guiFontSize = guiFontSize
self.mainConf.showFullPath = showFullPath
self.mainConf.autoSaveDoc = autoSaveDoc self.mainConf.autoSaveDoc = autoSaveDoc
self.mainConf.autoSaveProj = autoSaveProj self.mainConf.autoSaveProj = autoSaveProj
self.mainConf.backupPath = backupPath self.mainConf.backupPath = backupPath
@@ -340,7 +413,7 @@ class GuiConfigEditGeneralTab(QWidget):
dlgOpt = QFileDialog.Options() dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.ShowDirsOnly dlgOpt |= QFileDialog.ShowDirsOnly
dlgOpt |= QFileDialog.DontUseNativeDialog dlgOpt |= QFileDialog.DontUseNativeDialog
newDir = QFileDialog.getExistingDirectory( newDir = QFileDialog.getExistingDirectory(
self, "Backup Directory", currDir, options=dlgOpt self, "Backup Directory", currDir, options=dlgOpt
) )
if newDir: if newDir:
@@ -357,19 +430,7 @@ class GuiConfigEditGeneralTab(QWidget):
self.askBeforeBackup.setEnabled(theState) self.askBeforeBackup.setEnabled(theState)
return return
def _selectFont(self): # END Class GuiConfigEditProjectsTab
"""Open the QFontDialog and set a font for the font style.
"""
currFont = QFont()
currFont.setFamily(self.mainConf.guiFont)
currFont.setPointSize(self.mainConf.guiFontSize)
theFont, theStatus = QFontDialog.getFont(currFont, self)
if theStatus:
self.guiFont.setText(theFont.family())
self.guiFontSize.setValue(theFont.pointSize())
return
# END Class GuiConfigEditGeneralTab
class GuiConfigEditLayoutTab(QWidget): class GuiConfigEditLayoutTab(QWidget):
@@ -494,6 +555,41 @@ class GuiConfigEditLayoutTab(QWidget):
theUnit="px" theUnit="px"
) )
# Scroll Behaviour
# ================
self.mainForm.addGroupLabel("Scroll Behaviour")
## Scroll Past End
self.scrollPastEnd = QSwitch()
self.scrollPastEnd.setChecked(self.mainConf.scrollPastEnd)
self.mainForm.addRow(
"Scroll past end of the document",
self.scrollPastEnd,
"Allows scrolling until last line is at the top."
)
## Typewriter Scrolling
self.autoScroll = QSwitch()
self.autoScroll.setChecked(self.mainConf.autoScroll)
self.mainForm.addRow(
"Typewriter style scrolling when you type",
self.autoScroll,
"Tries to keep the cursor at a fixed vertical position."
)
## Font Size
self.autoScrollPos = QSpinBox(self)
self.autoScrollPos.setMinimum(10)
self.autoScrollPos.setMaximum(90)
self.autoScrollPos.setSingleStep(1)
self.autoScrollPos.setValue(int(self.mainConf.autoScrollPos))
self.mainForm.addRow(
"Minimum position for Typewriter scrolling",
self.autoScrollPos,
"In units of percentage of the editor height.",
theUnit = "%"
)
return return
def saveValues(self): def saveValues(self):
@@ -511,6 +607,9 @@ class GuiConfigEditLayoutTab(QWidget):
doJustify = self.textJustify.isChecked() doJustify = self.textJustify.isChecked()
textMargin = self.textMargin.value() textMargin = self.textMargin.value()
tabWidth = self.tabWidth.value() tabWidth = self.tabWidth.value()
scrollPastEnd = self.scrollPastEnd.isChecked()
autoScroll = self.autoScroll.isChecked()
autoScrollPos = self.autoScrollPos.value()
self.mainConf.textFont = textFont self.mainConf.textFont = textFont
self.mainConf.textSize = textSize self.mainConf.textSize = textSize
@@ -521,6 +620,9 @@ class GuiConfigEditLayoutTab(QWidget):
self.mainConf.doJustify = doJustify self.mainConf.doJustify = doJustify
self.mainConf.textMargin = textMargin self.mainConf.textMargin = textMargin
self.mainConf.tabWidth = tabWidth self.mainConf.tabWidth = tabWidth
self.mainConf.scrollPastEnd = scrollPastEnd
self.mainConf.autoScroll = autoScroll
self.mainConf.autoScrollPos = autoScrollPos
self.mainConf.confChanged = True self.mainConf.confChanged = True
+1 -1
View File
@@ -3,7 +3,7 @@
novelWriter GUI Open Project novelWriter GUI Open Project
================================ ================================
New and open project dialog Class holding the load/browse/new project dialog
File History: File History:
Created: 2020-02-26 [0.4.5] Created: 2020-02-26 [0.4.5]
+7 -6
View File
@@ -270,11 +270,12 @@ class GuiProjectEditMeta(QWidget):
self.revLabel = QLabel("Revision count:") self.revLabel = QLabel("Revision count:")
self.revLabel.setIndent(xInd) self.revLabel.setIndent(xInd)
self.revValue = QLabel("{:n}".format(self.theProject.saveCount)) self.revValue = QLabel(f"{self.theProject.saveCount:n}")
editHours = self.theProject.editTime/3600
self.editLabel = QLabel("Edit time:") self.editLabel = QLabel("Edit time:")
self.editLabel.setIndent(xInd) self.editLabel.setIndent(xInd)
self.editValue = QLabel("{:.2f} hours".format(self.theProject.editTime/3600)) self.editValue = QLabel(f"{editHours:.2f} hours")
self.statsLabel = QLabel("<b>Project Stats</b>") self.statsLabel = QLabel("<b>Project Stats</b>")
@@ -282,19 +283,19 @@ class GuiProjectEditMeta(QWidget):
self.nRootLabel = QLabel("Root folders:") self.nRootLabel = QLabel("Root folders:")
self.nRootLabel.setIndent(xInd) self.nRootLabel.setIndent(xInd)
self.nRootValue = QLabel("{:n}".format(nR)) self.nRootValue = QLabel(f"{nR:n}")
self.nDirLabel = QLabel("Folders:") self.nDirLabel = QLabel("Folders:")
self.nDirLabel.setIndent(xInd) self.nDirLabel.setIndent(xInd)
self.nDirValue = QLabel("{:n}".format(nD)) self.nDirValue = QLabel(f"{nD:n}")
self.nFileLabel = QLabel("Documents:") self.nFileLabel = QLabel("Documents:")
self.nFileLabel.setIndent(xInd) self.nFileLabel.setIndent(xInd)
self.nFileValue = QLabel("{:n}".format(nF)) self.nFileValue = QLabel(f"{nF:n}")
self.wordsLabel = QLabel("Word count:") self.wordsLabel = QLabel("Word count:")
self.wordsLabel.setIndent(xInd) self.wordsLabel.setIndent(xInd)
self.wordsValue = QLabel("{:n}".format(self.theProject.currWCount)) self.wordsValue = QLabel(f"{self.theProject.currWCount:n}")
self.mainForm.addWidget(self.headLabel, 0, 0, 1, 2, Qt.AlignTop) self.mainForm.addWidget(self.headLabel, 0, 0, 1, 2, Qt.AlignTop)
self.mainForm.addWidget(self.nameLabel, 1, 0, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.nameLabel, 1, 0, 1, 1, Qt.AlignTop)
+39 -16
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter GUI Document Tree """novelWriter GUI Project Tree
novelWriter GUI Document Tree novelWriter GUI project Tree
================================= ================================
Class holding the left side document tree view Class holding the project tree view
File History: File History:
Created: 2018-09-29 [0.0.1] GuiProjectTree Created: 2018-09-29 [0.0.1] GuiProjectTree
@@ -115,6 +115,9 @@ class GuiProjectTree(QTreeWidget):
# The last column should just auto-scale # The last column should just auto-scale
self.resizeColumnToContents(self.C_FLAGS) self.resizeColumnToContents(self.C_FLAGS)
# Set custom settings
self.initTree()
logger.debug("GuiProjectTree initialisation complete") logger.debug("GuiProjectTree initialisation complete")
# Internal Mapping # Internal Mapping
@@ -122,6 +125,22 @@ class GuiProjectTree(QTreeWidget):
return return
def initTree(self):
"""Set or update tree widget 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
## ##
# Class Methods # Class Methods
## ##
@@ -200,7 +219,7 @@ class GuiProjectTree(QTreeWidget):
pItem = self.theProject.projTree[pHandle] pItem = self.theProject.projTree[pHandle]
if pItem.itemType == nwItemType.FILE: if pItem.itemType == nwItemType.FILE:
nHandle = pHandle nHandle = pHandle
pHandle = pItem.parHandle pHandle = pItem.itemParent
# If we again have no home, give up # If we again have no home, give up
if pHandle is None: if pHandle is None:
@@ -241,7 +260,8 @@ class GuiProjectTree(QTreeWidget):
# Add the new item to the tree # Add the new item to the tree
if tHandle is not None: if tHandle is not None:
self.revealNewTreeItem(tHandle, nHandle) self.revealNewTreeItem(tHandle, nHandle)
self.theParent.editItem(tHandle) if self.mainConf.showGUI:
self.theParent.editItem(tHandle)
return True return True
@@ -250,7 +270,7 @@ class GuiProjectTree(QTreeWidget):
""" """
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.projTree[tHandle]
trItem = self._addTreeItem(nwItem, nHandle) trItem = self._addTreeItem(nwItem, nHandle)
pHandle = nwItem.parHandle pHandle = nwItem.itemParent
if pHandle is not None and pHandle in self.theMap: if pHandle is not None and pHandle in self.theMap:
self.theMap[pHandle].setExpanded(True) self.theMap[pHandle].setExpanded(True)
self.clearSelection() self.clearSelection()
@@ -401,7 +421,7 @@ class GuiProjectTree(QTreeWidget):
if nwItemS is None: if nwItemS is None:
return False return False
wCount = int(trItemS.text(self.C_COUNT)) wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole))
if nwItemS.itemType == nwItemType.FILE: if nwItemS.itemType == nwItemType.FILE:
logger.debug("User requested file %s moved to trash" % tHandle) logger.debug("User requested file %s moved to trash" % tHandle)
trItemP = trItemS.parent() trItemP = trItemS.parent()
@@ -410,7 +430,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("Could not delete item") logger.error("Could not delete item")
return False return False
pHandle = nwItemS.parHandle pHandle = nwItemS.itemParent
if self.theProject.projTree.isTrashRoot(pHandle): if self.theProject.projTree.isTrashRoot(pHandle):
# If the file is in the trash folder already, as the # If the file is in the trash folder already, as the
# user if they want to permanently delete the file. # user if they want to permanently delete the file.
@@ -550,12 +570,13 @@ class GuiProjectTree(QTreeWidget):
""" """
tItem = self._getTreeItem(tHandle) tItem = self._getTreeItem(tHandle)
if tItem is not None: if tItem is not None:
tItem.setText(self.C_COUNT, str(theCount)) tItem.setText(self.C_COUNT, f"{theCount:n}")
tItem.setData(self.C_COUNT, Qt.UserRole, int(theCount))
pItem = tItem.parent() pItem = tItem.parent()
if pItem is not None: if pItem is not None:
pCount = 0 pCount = 0
for i in range(pItem.childCount()): for i in range(pItem.childCount()):
pCount += int(pItem.child(i).text(self.C_COUNT)) pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
pHandle = pItem.data(self.C_NAME, Qt.UserRole) pHandle = pItem.data(self.C_NAME, Qt.UserRole)
if not nDepth > nwConst.maxDepth + 1 and pHandle != "": if not nDepth > nwConst.maxDepth + 1 and pHandle != "":
@@ -575,7 +596,7 @@ class GuiProjectTree(QTreeWidget):
tItem = self.topLevelItem(n) tItem = self.topLevelItem(n)
if tItem == self.orphRoot: if tItem == self.orphRoot:
continue continue
nWords += int(tItem.text(self.C_COUNT)) nWords += int(tItem.data(self.C_COUNT, Qt.UserRole))
self.theProject.setProjectWordCount(nWords) self.theProject.setProjectWordCount(nWords)
sWords = self.theProject.getSessionWordCount() sWords = self.theProject.getSessionWordCount()
@@ -715,7 +736,7 @@ class GuiProjectTree(QTreeWidget):
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR) self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
return return
wCount = int(sItem.text(self.C_COUNT)) wCount = int(sItem.data(self.C_COUNT, Qt.UserRole))
isSame = snItem.itemClass == dnItem.itemClass isSame = snItem.itemClass == dnItem.itemClass
isNone = snItem.itemClass == nwItemClass.NO_CLASS isNone = snItem.itemClass == nwItemClass.NO_CLASS
isNote = snItem.itemLayout == nwItemLayout.NOTE isNote = snItem.itemLayout == nwItemLayout.NOTE
@@ -794,7 +815,7 @@ class GuiProjectTree(QTreeWidget):
project tree. project tree.
""" """
tHandle = nwItem.itemHandle tHandle = nwItem.itemHandle
pHandle = nwItem.parHandle pHandle = nwItem.itemParent
tClass = nwItem.itemClass tClass = nwItem.itemClass
newItem = QTreeWidgetItem([""]*4) newItem = QTreeWidgetItem([""]*4)
@@ -809,6 +830,7 @@ class GuiProjectTree(QTreeWidget):
newItem.setTextAlignment(self.C_FLAGS, Qt.AlignLeft | Qt.AlignVCenter) newItem.setTextAlignment(self.C_FLAGS, Qt.AlignLeft | Qt.AlignVCenter)
newItem.setData(self.C_NAME, Qt.UserRole, tHandle) newItem.setData(self.C_NAME, Qt.UserRole, tHandle)
newItem.setData(self.C_COUNT, Qt.UserRole, 0)
self.theMap[tHandle] = newItem self.theMap[tHandle] = newItem
if pHandle is None: if pHandle is None:
@@ -881,6 +903,7 @@ class GuiProjectTree(QTreeWidget):
self.orphRoot = newItem self.orphRoot = newItem
newItem.setExpanded(True) newItem.setExpanded(True)
newItem.setData(self.C_NAME, Qt.UserRole, "") newItem.setData(self.C_NAME, Qt.UserRole, "")
newItem.setData(self.C_COUNT, Qt.UserRole, 0)
newItem.setIcon(self.C_NAME, self.theTheme.getIcon("proj_orphan")) newItem.setIcon(self.C_NAME, self.theTheme.getIcon("proj_orphan"))
return return
@@ -999,11 +1022,11 @@ class GuiProjectTreeMenu(QMenu):
trashHandle = self.theTree.theProject.projTree.trashRoot() trashHandle = self.theTree.theProject.projTree.trashRoot()
inTrash = theItem.parHandle == trashHandle and trashHandle is not None inTrash = theItem.itemParent == trashHandle and trashHandle is not None
isTrash = theItem.itemHandle == trashHandle and trashHandle is not None isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
isFile = theItem.itemType == nwItemType.FILE isFile = theItem.itemType == nwItemType.FILE
isArch = theRoot.itemClass == nwItemClass.ARCHIVE isArch = theRoot.itemClass == nwItemClass.ARCHIVE
isOrph = isFile and theItem.parHandle is None isOrph = isFile and theItem.itemParent is None
showOpen = isFile showOpen = isFile
showView = isFile showView = isFile
+12 -17
View File
@@ -35,6 +35,7 @@ from PyQt5.QtGui import QColor, QPainter
from PyQt5.QtWidgets import qApp, QStatusBar, QLabel, QAbstractButton from PyQt5.QtWidgets import qApp, QStatusBar, QLabel, QAbstractButton
from nw.core import NWSpellCheck from nw.core import NWSpellCheck
from nw.common import formatTime
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -152,13 +153,17 @@ class GuiMainStatus(QStatusBar):
qApp.processEvents() qApp.processEvents()
return return
def setLanguage(self, theLanguage): def setLanguage(self, theLanguage, theProvider=""):
"""Set the language code for the spell checker. """Set the language code for the spell checker.
""" """
if theLanguage is None: if theLanguage is None:
self.langText.setText("None") self.langText.setText("None")
self.langText.setToolTip("")
else: else:
self.langText.setText(NWSpellCheck.expandLanguage(theLanguage)) self.langText.setText(NWSpellCheck.expandLanguage(theLanguage))
self.langText.setToolTip(
"Provider: %s" % (theProvider if theProvider else "unknown")
)
return return
def setProjectStatus(self, isChanged): def setProjectStatus(self, isChanged):
@@ -191,28 +196,18 @@ class GuiMainStatus(QStatusBar):
self.statsText.setToolTip( self.statsText.setToolTip(
"Project word count (session change)" "Project word count (session change)"
) )
self.statsText.setText(( self.statsText.setText(
"Words: {pWC:n} ({sWC:+n})" f"Words: {self.projWords:n} ({self.sessWords:+n})"
).format( )
pWC = self.projWords,
sWC = self.sessWords,
))
return return
def _updateTime(self): def _updateTime(self):
"""Update the session clock. """Update the session clock.
""" """
if self.refTime is None: if self.refTime is None:
theTime = "00:00:00" self.timeText.setText("00:00:00")
else: else:
# This is much faster than using datetime format self.timeText.setText(formatTime(round(time() - self.refTime)))
tS = int(time() - self.refTime)
tM = int(tS/60)
tH = int(tM/60)
tM = tM - tH*60
tS = tS - tM*60 - tH*3600
theTime = "%02d:%02d:%02d" % (tH, tM, tS)
self.timeText.setText(theTime)
return return
# END Class GuiMainStatus # END Class GuiMainStatus
@@ -233,7 +228,7 @@ class StatusLED(QAbstractButton):
return return
## ##
# Getters and Setters # Setters
## ##
def setState(self, theState): def setState(self, theState):
+7 -6
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter Theme Class """novelWriter Theme and Icons Classes
novelWriter Theme Class novelWriter Theme and Icons Classs
=========================== ======================================
This class reads and store the main theme Class managing and caching themes and icons
File History: File History:
Created: 2019-05-18 [0.1.3] GuiTheme Created: 2019-05-18 [0.1.3] GuiTheme
@@ -103,7 +103,7 @@ class GuiTheme:
self.colDialN = [0, 0, 0] self.colDialN = [0, 0, 0]
self.colDialD = [0, 0, 0] self.colDialD = [0, 0, 0]
self.colDialS = [0, 0, 0] self.colDialS = [0, 0, 0]
self.colComm = [0, 0, 0] self.colHidden = [0, 0, 0]
self.colKey = [0, 0, 0] self.colKey = [0, 0, 0]
self.colVal = [0, 0, 0] self.colVal = [0, 0, 0]
self.colSpell = [0, 0, 0] self.colSpell = [0, 0, 0]
@@ -356,7 +356,7 @@ class GuiTheme:
self.colDialN = self._loadColour(confParser, cnfSec, "straightquotes") self.colDialN = self._loadColour(confParser, cnfSec, "straightquotes")
self.colDialD = self._loadColour(confParser, cnfSec, "doublequotes") self.colDialD = self._loadColour(confParser, cnfSec, "doublequotes")
self.colDialS = self._loadColour(confParser, cnfSec, "singlequotes") self.colDialS = self._loadColour(confParser, cnfSec, "singlequotes")
self.colComm = self._loadColour(confParser, cnfSec, "hidden") self.colHidden = self._loadColour(confParser, cnfSec, "hidden")
self.colKey = self._loadColour(confParser, cnfSec, "keyword") self.colKey = self._loadColour(confParser, cnfSec, "keyword")
self.colVal = self._loadColour(confParser, cnfSec, "value") self.colVal = self._loadColour(confParser, cnfSec, "value")
self.colSpell = self._loadColour(confParser, cnfSec, "spellcheckline") self.colSpell = self._loadColour(confParser, cnfSec, "spellcheckline")
@@ -529,6 +529,7 @@ class GuiIcons:
"status_lang" : (None, None), "status_lang" : (None, None),
"status_time" : (None, None), "status_time" : (None, None),
"status_stats" : (None, None), "status_stats" : (None, None),
"status_lines" : (None, None),
"doc_h1" : (QStyle.SP_FileIcon, "x-office-document"), "doc_h1" : (QStyle.SP_FileIcon, "x-office-document"),
"doc_h2" : (QStyle.SP_FileIcon, "x-office-document"), "doc_h2" : (QStyle.SP_FileIcon, "x-office-document"),
"doc_h3" : (QStyle.SP_FileIcon, "x-office-document"), "doc_h3" : (QStyle.SP_FileIcon, "x-office-document"),
+21 -33
View File
@@ -3,7 +3,7 @@
novelWriter GUI Writing Statistics novelWriter GUI Writing Statistics
====================================== ======================================
Class showing the word count and session statistics Class holding the word count and session statistics dialog
File History: File History:
Created: 2019-10-20 [0.3] Created: 2019-10-20 [0.3]
@@ -39,6 +39,7 @@ from PyQt5.QtWidgets import (
QLabel, QGroupBox, QMenu, QAction, QFileDialog, QSpinBox, QHBoxLayout QLabel, QGroupBox, QMenu, QAction, QFileDialog, QSpinBox, QHBoxLayout
) )
from nw.common import formatTime
from nw.constants import nwConst, nwFiles, nwAlert from nw.constants import nwConst, nwFiles, nwAlert
from nw.gui.custom import QSwitch from nw.gui.custom import QSwitch
@@ -123,11 +124,11 @@ class GuiWritingStats(QDialog):
self.infoForm = QGridLayout(self) self.infoForm = QGridLayout(self)
self.infoBox.setLayout(self.infoForm) self.infoBox.setLayout(self.infoForm)
self.labelTotal = QLabel(self._formatTime(0)) self.labelTotal = QLabel(formatTime(0))
self.labelTotal.setFont(self.theTheme.guiFontFixed) self.labelTotal.setFont(self.theTheme.guiFontFixed)
self.labelTotal.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.labelTotal.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.labelFilter = QLabel(self._formatTime(0)) self.labelFilter = QLabel(formatTime(0))
self.labelFilter.setFont(self.theTheme.guiFontFixed) self.labelFilter.setFont(self.theTheme.guiFontFixed)
self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
@@ -334,12 +335,11 @@ class GuiWritingStats(QDialog):
dlgOpt = QFileDialog.Options() dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog dlgOpt |= QFileDialog.DontUseNativeDialog
saveTo = QFileDialog.getSaveFileName( savePath, _ = QFileDialog.getSaveFileName(
self, "Save Document As", savePath, options=dlgOpt self, "Save Document As", savePath, options=dlgOpt
) )
if saveTo:
savePath = saveTo[0] if not savePath:
else:
return False return False
self.mainConf.setLastPath(savePath) self.mainConf.setLastPath(savePath)
@@ -363,37 +363,33 @@ class GuiWritingStats(QDialog):
"novelWords": wA, "novelWords": wA,
"noteWords": wB, "noteWords": wB,
}) })
outFile.write(json.dumps(jsonData, indent=2)) json.dump(jsonData, outFile, indent=2)
wSuccess = True wSuccess = True
elif dataFmt == self.FMT_CSV: elif dataFmt == self.FMT_CSV:
outFile.write( outFile.write(
"\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n" % ( '"Date","Length (sec)","Words Changed","Novel Words","Note Words"\n'
"Date", "Length (sec)", "Words Changed", "Novel Words", "Note Words"
)
) )
for _, sD, tT, wD, wA, wB in self.filterData: for _, sD, tT, wD, wA, wB in self.filterData:
outFile.write( outFile.write(f'"{sD}",{tT:.0f},{wD},{wA},{wB}\n')
"\"%s\",%d,%d,%d,%d\n" % (sD, tT, wD, wA, wB)
)
wSuccess = True wSuccess = True
else: else:
errMsg = "Unknown format" errMsg = "Unknown format"
except Exception as e: except Exception as e:
errMsg = str(e) errMsg = str(e).replace("\n", "<br>")
# Report to user # Report to user
if wSuccess: if wSuccess:
self.theParent.makeAlert( self.theParent.makeAlert(
"%s file successfully written to:<br> %s" % ( "%s file successfully written to:<br>%s" % (
textFmt, savePath textFmt, savePath
), nwAlert.INFO ), nwAlert.INFO
) )
else: else:
self.theParent.makeAlert( self.theParent.makeAlert(
"Failed to write %s file. %s" % ( "Failed to write %s file.<br>%s" % (
textFmt, errMsg textFmt, errMsg
), nwAlert.ERROR ), nwAlert.ERROR
) )
@@ -455,10 +451,11 @@ class GuiWritingStats(QDialog):
) )
return False return False
self.labelTotal.setText(self._formatTime(ttTime)) ttWords = ttNovel + ttNotes
self.novelWords.setText("{:n}".format(ttNovel)) self.labelTotal.setText(formatTime(round(ttTime)))
self.notesWords.setText("{:n}".format(ttNotes)) self.novelWords.setText(f"{ttNovel:n}")
self.totalWords.setText("{:n}".format(ttNovel + ttNotes)) self.notesWords.setText(f"{ttNotes:n}")
self.totalWords.setText(f"{ttWords:n}")
return True return True
@@ -544,8 +541,8 @@ class GuiWritingStats(QDialog):
newItem = QTreeWidgetItem() newItem = QTreeWidgetItem()
newItem.setText(self.C_TIME, sStart) newItem.setText(self.C_TIME, sStart)
newItem.setText(self.C_LENGTH, self._formatTime(sDiff)) newItem.setText(self.C_LENGTH, formatTime(round(sDiff)))
newItem.setText(self.C_COUNT, "{:n}".format(nWords)) newItem.setText(self.C_COUNT, f"{nWords:n}")
if nWords > 0 and listMax > 0: if nWords > 0 and listMax > 0:
theBar = self.barImage.scaled( theBar = self.barImage.scaled(
@@ -567,17 +564,8 @@ class GuiWritingStats(QDialog):
self.listBox.addTopLevelItem(newItem) self.listBox.addTopLevelItem(newItem)
self.timeFilter += sDiff self.timeFilter += sDiff
self.labelFilter.setText(self._formatTime(self.timeFilter)) self.labelFilter.setText(formatTime(round(self.timeFilter)))
return True return True
def _formatTime(self, tS):
"""Format the time spent in 00:00:00 format.
"""
tM = int(tS/60)
tH = int(tM/60)
tM = tM - tH*60
tS = tS - tM*60 - tH*3600
return "%02d:%02d:%02d" % (tH, tM, tS)
# END Class GuiWritingStats # END Class GuiWritingStats
+51 -31
View File
@@ -3,7 +3,7 @@
novelWriter GUI Main Window novelWriter GUI Main Window
=============================== ===============================
Class holding the main window Class holding the main application window
File History: File History:
Created: 2018-09-22 [0.0.1] Created: 2018-09-22 [0.0.1]
@@ -32,7 +32,7 @@ import os
from datetime import datetime from datetime import datetime
from time import time from time import time
from PyQt5.QtCore import Qt, QTimer from PyQt5.QtCore import Qt, QTimer, QThreadPool
from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence, QCursor from PyQt5.QtGui import QIcon, QPixmap, QColor, QKeySequence, QCursor
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut, qApp, QMainWindow, QVBoxLayout, QWidget, QSplitter, QFileDialog, QShortcut,
@@ -60,8 +60,11 @@ class GuiMain(QMainWindow):
logger.debug("Initialising GUI ...") logger.debug("Initialising GUI ...")
self.setObjectName("GuiMain") self.setObjectName("GuiMain")
self.mainConf = nw.CONFIG self.mainConf = nw.CONFIG
self.threadPool = QThreadPool()
# System Info
# ===========
# Some runtime info useful for debugging
logger.info("OS: %s" % self.mainConf.osType) logger.info("OS: %s" % self.mainConf.osType)
logger.info("Kernel: %s" % self.mainConf.kernelVer) logger.info("Kernel: %s" % self.mainConf.kernelVer)
logger.info("Host: %s" % self.mainConf.hostName) logger.info("Host: %s" % self.mainConf.hostName)
@@ -75,6 +78,9 @@ class GuiMain(QMainWindow):
self.mainConf.verPyString, self.mainConf.verPyHexVal) self.mainConf.verPyString, self.mainConf.verPyHexVal)
) )
# Core Classes
# ============
# Core Classes and settings # Core Classes and settings
self.theTheme = GuiTheme(self) self.theTheme = GuiTheme(self)
self.theProject = NWProject(self) self.theProject = NWProject(self)
@@ -88,7 +94,7 @@ class GuiMain(QMainWindow):
self.setWindowIcon(QIcon(self.mainConf.appIcon)) self.setWindowIcon(QIcon(self.mainConf.appIcon))
# Build the GUI # Build the GUI
################ # =============
# Main GUI Elements # Main GUI Elements
self.statusBar = GuiMainStatus(self) self.statusBar = GuiMainStatus(self)
@@ -105,7 +111,7 @@ class GuiMain(QMainWindow):
self.statusIcons = [] self.statusIcons = []
self.importIcons = [] self.importIcons = []
# Assemble Main Window # Project Tree View
self.treePane = QWidget() self.treePane = QWidget()
self.treeBox = QVBoxLayout() self.treeBox = QVBoxLayout()
self.treeBox.setContentsMargins(0, 0, 0, 0) self.treeBox.setContentsMargins(0, 0, 0, 0)
@@ -113,20 +119,24 @@ class GuiMain(QMainWindow):
self.treeBox.addWidget(self.treeMeta) self.treeBox.addWidget(self.treeMeta)
self.treePane.setLayout(self.treeBox) self.treePane.setLayout(self.treeBox)
# Splitter : Document Viewer / Document Meta
self.splitView = QSplitter(Qt.Vertical) self.splitView = QSplitter(Qt.Vertical)
self.splitView.addWidget(self.docViewer) self.splitView.addWidget(self.docViewer)
self.splitView.addWidget(self.viewMeta) self.splitView.addWidget(self.viewMeta)
self.splitView.setSizes(self.mainConf.getViewPanePos()) self.splitView.setSizes(self.mainConf.getViewPanePos())
# Splitter : Document Editor / Document Viewer
self.splitDocs = QSplitter(Qt.Horizontal) self.splitDocs = QSplitter(Qt.Horizontal)
self.splitDocs.addWidget(self.docEditor) self.splitDocs.addWidget(self.docEditor)
self.splitDocs.addWidget(self.splitView) self.splitDocs.addWidget(self.splitView)
# Splitter : Project Outlie / Outline Details
self.splitOutline = QSplitter(Qt.Vertical) self.splitOutline = QSplitter(Qt.Vertical)
self.splitOutline.addWidget(self.projView) self.splitOutline.addWidget(self.projView)
self.splitOutline.addWidget(self.projMeta) self.splitOutline.addWidget(self.projMeta)
self.splitOutline.setSizes(self.mainConf.getOutlinePanePos()) self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
# Main Tabs : Edirot / Outline
self.tabWidget = QTabWidget() self.tabWidget = QTabWidget()
self.tabWidget.setTabPosition(QTabWidget.East) self.tabWidget.setTabPosition(QTabWidget.East)
self.tabWidget.setStyleSheet("QTabWidget::pane {border: 0;}") self.tabWidget.setStyleSheet("QTabWidget::pane {border: 0;}")
@@ -134,6 +144,7 @@ class GuiMain(QMainWindow):
self.tabWidget.addTab(self.splitOutline, "Outline") self.tabWidget.addTab(self.splitOutline, "Outline")
self.tabWidget.currentChanged.connect(self._mainTabChanged) self.tabWidget.currentChanged.connect(self._mainTabChanged)
# Splitter : Project Tree / Main Tabs
xCM = self.mainConf.pxInt(4) xCM = self.mainConf.pxInt(4)
self.splitMain = QSplitter(Qt.Horizontal) self.splitMain = QSplitter(Qt.Horizontal)
self.splitMain.setContentsMargins(xCM, xCM, xCM, xCM) self.splitMain.setContentsMargins(xCM, xCM, xCM, xCM)
@@ -141,6 +152,7 @@ class GuiMain(QMainWindow):
self.splitMain.addWidget(self.tabWidget) self.splitMain.addWidget(self.tabWidget)
self.splitMain.setSizes(self.mainConf.getMainPanePos()) self.splitMain.setSizes(self.mainConf.getMainPanePos())
# Indices of All Splitter Widgets
self.idxTree = self.splitMain.indexOf(self.treePane) self.idxTree = self.splitMain.indexOf(self.treePane)
self.idxMain = self.splitMain.indexOf(self.tabWidget) self.idxMain = self.splitMain.indexOf(self.tabWidget)
self.idxEditor = self.splitDocs.indexOf(self.docEditor) self.idxEditor = self.splitDocs.indexOf(self.docEditor)
@@ -150,6 +162,7 @@ class GuiMain(QMainWindow):
self.idxTabEdit = self.tabWidget.indexOf(self.splitDocs) self.idxTabEdit = self.tabWidget.indexOf(self.splitDocs)
self.idxTabProj = self.tabWidget.indexOf(self.splitOutline) self.idxTabProj = self.tabWidget.indexOf(self.splitOutline)
# Splitter Behaviour
self.splitMain.setCollapsible(self.idxTree, False) self.splitMain.setCollapsible(self.idxTree, False)
self.splitMain.setCollapsible(self.idxMain, False) self.splitMain.setCollapsible(self.idxMain, False)
self.splitDocs.setCollapsible(self.idxEditor, False) self.splitDocs.setCollapsible(self.idxEditor, False)
@@ -157,10 +170,11 @@ class GuiMain(QMainWindow):
self.splitView.setCollapsible(self.idxViewDoc, False) self.splitView.setCollapsible(self.idxViewDoc, False)
self.splitView.setCollapsible(self.idxViewMeta, False) self.splitView.setCollapsible(self.idxViewMeta, False)
# Editor / Viewer Default State
self.splitView.setVisible(False) self.splitView.setVisible(False)
self.docEditor.closeSearch() self.docEditor.closeSearch()
# Build the Tree View # Initialise the Project Tree
self.treeView.itemSelectionChanged.connect(self._treeSingleClick) self.treeView.itemSelectionChanged.connect(self._treeSingleClick)
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.rebuildTree() self.rebuildTree()
@@ -171,13 +185,13 @@ class GuiMain(QMainWindow):
self.setStatusBar(self.statusBar) self.setStatusBar(self.statusBar)
# Finalise Initialisation # Finalise Initialisation
########################## # =======================
# Set Up Autosaving Project Timer # Set Up Auto-Save Project Timer
self.asProjTimer = QTimer() self.asProjTimer = QTimer()
self.asProjTimer.timeout.connect(self._autoSaveProject) self.asProjTimer.timeout.connect(self._autoSaveProject)
# Set Up Autosaving Document Timer # Set Up Auto-Save Document Timer
self.asDocTimer = QTimer() self.asDocTimer = QTimer()
self.asDocTimer.timeout.connect(self._autoSaveDocument) self.asDocTimer.timeout.connect(self._autoSaveDocument)
@@ -202,11 +216,13 @@ class GuiMain(QMainWindow):
# Check that config loaded fine # Check that config loaded fine
self.reportConfErr() self.reportConfErr()
# Initialise Main GUI
self.initMain() self.initMain()
self.asProjTimer.start() self.asProjTimer.start()
self.asDocTimer.start() self.asDocTimer.start()
self.statusBar.clearStatus() self.statusBar.clearStatus()
# Handle Windows Mode
self.showNormal() self.showNormal()
if self.mainConf.isFullScreen: if self.mainConf.isFullScreen:
self.toggleFullScreenMode() self.toggleFullScreenMode()
@@ -223,7 +239,7 @@ class GuiMain(QMainWindow):
self.showProjectLoadDialog() self.showProjectLoadDialog()
logger.debug("novelWriter is ready ...") logger.debug("novelWriter is ready ...")
self.statusBar.setStatus("novelWriter is ready ...") self.setStatus("novelWriter is ready ...")
return return
@@ -248,8 +264,7 @@ class GuiMain(QMainWindow):
## ##
def newProject(self, projData=None): def newProject(self, projData=None):
"""Create new project with a few default files and folders. """Create new project via the new project wizard.
The variable forceNew is used for testing.
""" """
if self.hasProject: if self.hasProject:
self.makeAlert( self.makeAlert(
@@ -292,7 +307,7 @@ class GuiMain(QMainWindow):
def closeProject(self, isYes=False): def closeProject(self, isYes=False):
"""Closes the project if one is open. isYes is passed on from """Closes the project if one is open. isYes is passed on from
the close application event so the user doesn't get prompted the close application event so the user doesn't get prompted
twice. twice to confirm.
""" """
if not self.hasProject: if not self.hasProject:
# There is no project loaded, everything OK # There is no project loaded, everything OK
@@ -301,7 +316,7 @@ class GuiMain(QMainWindow):
if not isYes: if not isYes:
msgBox = QMessageBox() msgBox = QMessageBox()
msgRes = msgBox.question( msgRes = msgBox.question(
self, "Close Project", "Save changes and close current project?" self, "Close Project", "Save changes and close the current project?"
) )
if msgRes != QMessageBox.Yes: if msgRes != QMessageBox.Yes:
return False return False
@@ -317,7 +332,7 @@ class GuiMain(QMainWindow):
if self.mainConf.askBeforeBackup: if self.mainConf.askBeforeBackup:
msgBox = QMessageBox() msgBox = QMessageBox()
msgRes = msgBox.question( msgRes = msgBox.question(
self, "Backup Project", "Backup current project?" self, "Backup Project", "Backup the current project?"
) )
if msgRes != QMessageBox.Yes: if msgRes != QMessageBox.Yes:
doBackup = False doBackup = False
@@ -432,6 +447,7 @@ class GuiMain(QMainWindow):
if self.theProject.projPath is None: if self.theProject.projPath is None:
projPath = self.selectProjectPath() projPath = self.selectProjectPath()
self.theProject.setProjectPath(projPath) self.theProject.setProjectPath(projPath)
if self.theProject.projPath is None: if self.theProject.projPath is None:
return False return False
@@ -453,6 +469,7 @@ class GuiMain(QMainWindow):
if self.docEditor.docChanged: if self.docEditor.docChanged:
self.saveDocument() self.saveDocument()
self.docEditor.clearEditor() self.docEditor.clearEditor()
return True return True
def openDocument(self, tHandle, tLine=None, changeFocus=True, doScroll=False): def openDocument(self, tHandle, tLine=None, changeFocus=True, doScroll=False):
@@ -468,6 +485,7 @@ class GuiMain(QMainWindow):
self.treeView.setSelectedHandle(tHandle, doScroll=doScroll) self.treeView.setSelectedHandle(tHandle, doScroll=doScroll)
else: else:
return False return False
return True return True
def openNextDocument(self, tHandle, wrapAround=False): def openNextDocument(self, tHandle, wrapAround=False):
@@ -545,6 +563,7 @@ class GuiMain(QMainWindow):
vPos[1] = bPos[1] - vPos[0] vPos[1] = bPos[1] - vPos[0]
self.splitDocs.setSizes(vPos) self.splitDocs.setSizes(vPos)
self.viewMeta.setVisible(self.mainConf.showRefPanel) self.viewMeta.setVisible(self.mainConf.showRefPanel)
self.docViewer.navigateTo(tAnchor) self.docViewer.navigateTo(tAnchor)
return True return True
@@ -558,16 +577,15 @@ class GuiMain(QMainWindow):
extFilter = [ extFilter = [
"Text files (*.txt)", "Text files (*.txt)",
"Markdown files (*.md)", "Markdown files (*.md)",
"novelWriter files (*.nwd)",
"All files (*.*)", "All files (*.*)",
] ]
dlgOpt = QFileDialog.Options() dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.DontUseNativeDialog dlgOpt |= QFileDialog.DontUseNativeDialog
inPath = QFileDialog.getOpenFileName( loadFile, _ = QFileDialog.getOpenFileName(
self, "Import File", lastPath, options=dlgOpt, filter=";;".join(extFilter) self, "Import File", lastPath, options=dlgOpt, filter=";;".join(extFilter)
) )
if inPath: if not loadFile:
loadFile = inPath[0]
else:
return False return False
if loadFile.strip() == "": if loadFile.strip() == "":
@@ -697,9 +715,9 @@ class GuiMain(QMainWindow):
for nDone, tItem in enumerate(self.theProject.projTree): for nDone, tItem in enumerate(self.theProject.projTree):
if tItem is not None: if tItem is not None:
self.statusBar.setStatus("Indexing: '%s'" % tItem.itemName) self.setStatus("Indexing: '%s'" % tItem.itemName)
else: else:
self.statusBar.setStatus("Indexing: Unknown item") self.setStatus("Indexing: Unknown item")
if tItem is not None and tItem.itemType == nwItemType.FILE: if tItem is not None and tItem.itemType == nwItemType.FILE:
logger.verbose("Scanning: %s" % tItem.itemName) logger.verbose("Scanning: %s" % tItem.itemName)
@@ -717,7 +735,7 @@ class GuiMain(QMainWindow):
self.treeView.projectWordCount() self.treeView.projectWordCount()
tEnd = time() tEnd = time()
self.statusBar.setStatus("Indexing completed in %.1f ms" % ((tEnd - tStart)*1000.0)) self.setStatus("Indexing completed in %.1f ms" % ((tEnd - tStart)*1000.0))
self.docEditor.updateTagHighLighting() self.docEditor.updateTagHighLighting()
qApp.restoreOverrideCursor() qApp.restoreOverrideCursor()
@@ -754,7 +772,8 @@ class GuiMain(QMainWindow):
def showProjectLoadDialog(self): def showProjectLoadDialog(self):
"""Opens the projects dialog for selecting either existing """Opens the projects dialog for selecting either existing
projects from a cache of recently opened projects, or provide a projects from a cache of recently opened projects, or provide a
browse button for projects not yet cached. browse button for projects not yet cached. Selecting to create a
new project is forwarded to the new project wizard.
""" """
dlgProj = GuiProjectLoad(self) dlgProj = GuiProjectLoad(self)
dlgProj.exec_() dlgProj.exec_()
@@ -767,7 +786,7 @@ class GuiMain(QMainWindow):
return True return True
def showNewProjectDialog(self): def showNewProjectDialog(self):
"""Open the wizard and assemble the project options dict. """Open the wizard and assemble a project options dict.
""" """
newProj = GuiProjectWizard(self) newProj = GuiProjectWizard(self)
newProj.exec_() newProj.exec_()
@@ -790,6 +809,9 @@ class GuiMain(QMainWindow):
self.saveDocument() self.saveDocument()
self.docEditor.initEditor() self.docEditor.initEditor()
self.docViewer.initViewer() self.docViewer.initViewer()
self.treeView.initTree()
self.projView.initOutline()
self.projMeta.initDetails()
return return
@@ -862,8 +884,7 @@ class GuiMain(QMainWindow):
def makeAlert(self, theMessage, theLevel=nwAlert.INFO): def makeAlert(self, theMessage, theLevel=nwAlert.INFO):
"""Alert both the user and the logger at the same time. Message """Alert both the user and the logger at the same time. Message
can be either a string or an array of strings. Severity level is can be either a string or an array of strings.
0 = info, 1 = warning, and 2 = error.
""" """
if isinstance(theMessage, list): if isinstance(theMessage, list):
popMsg = "<br>".join(theMessage) popMsg = "<br>".join(theMessage)
@@ -952,7 +973,7 @@ class GuiMain(QMainWindow):
return True return True
def setFocus(self, paneNo): def setFocus(self, paneNo):
"""Switch focus to one of the three main gUi panes. """Switch focus to one of the three main GUI panes.
""" """
if paneNo == 1: if paneNo == 1:
self.treeView.setFocus() self.treeView.setFocus()
@@ -1233,9 +1254,9 @@ class GuiMain(QMainWindow):
return return
def _treeKeyPressReturn(self): def _treeKeyPressReturn(self):
"""The user pressed return an item in the tree. If it is a file, """The user pressed return on an item in the tree. If it is a
we open it. Otherwise, we do nothing. Pressing return does not file, we open it. Otherwise, we do nothing. Pressing return does
change focus to the editor as double click does. not change focus to the editor as double click does.
""" """
tHandle = self.treeView.getSelectedHandle() tHandle = self.treeView.getSelectedHandle()
logger.verbose("User pressed return on tree item with handle %s" % tHandle) logger.verbose("User pressed return on tree item with handle %s" % tHandle)
@@ -1254,7 +1275,6 @@ class GuiMain(QMainWindow):
""" """
if self.docEditor.docSearch.isVisible(): if self.docEditor.docSearch.isVisible():
self.docEditor.closeSearch() self.docEditor.closeSearch()
return
elif self.isFocusMode: elif self.isFocusMode:
self.toggleFocusMode() self.toggleFocusMode()
return return
+3
View File
@@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ 14298de4d9524:f7e2d9f330615:f6622b4617424:CHARACTER:NOTE:John Smith %%~name: John Smith
%%~path: f7e2d9f330615/14298de4d9524
%%~kind: CHARACTER/NOTE
# John Smith # John Smith
@tag: John @tag: John
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ 53b69b83cdafc:7031beac91f75:NOVEL:TITLE:Title Page %%~name: Title Page
%%~path: 7031beac91f75/53b69b83cdafc
%%~kind: NOVEL/TITLE
# My Novel # My Novel
**By Jane Doh** **By Jane Doh**
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ 5eaea4e8cdee8:15c4492bd5107:WORLD:NOTE:Mars %%~name: Mars
%%~path: 15c4492bd5107/5eaea4e8cdee8
%%~kind: WORLD/NOTE
# Mars # Mars
@tag: Mars @tag: Mars
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ 636b6aa9b697b:e7ded148d6e4a:7031beac91f75:NOVEL:SCENE:Making a Scene %%~name: Making a Scene
%%~path: e7ded148d6e4a/636b6aa9b697b
%%~kind: NOVEL/SCENE
### Making a Scene ### Making a Scene
@pov: Jane @pov: Jane
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ 6a2d6d5f4f401:e7ded148d6e4a:7031beac91f75:NOVEL:CHAPTER:Chapter One %%~name: Chapter One
%%~path: e7ded148d6e4a/6a2d6d5f4f401
%%~kind: NOVEL/CHAPTER
## So it Begins ## So it Begins
@pov: Jane @pov: Jane
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ 88706ddc78b1b:e7ded148d6e4a:7031beac91f75:NOVEL:CHAPTER:Chapter Two %%~name: Chapter Two
%%~path: e7ded148d6e4a/88706ddc78b1b
%%~kind: NOVEL/CHAPTER
## Where has John Gone? ## Where has John Gone?
@pov: Jane @pov: Jane
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ 8a5deb88c0e97:6827118336ac1:NOVEL:SCENE:Old File %%~name: Old File
%%~path: ae9bf3c3ea159/8a5deb88c0e97
%%~kind: NOVEL/SCENE
### Discarded Scene ### Discarded Scene
If you have files you no longer want in your main project, you can move them to the “Outtakes” folder. This is equivalent to turning off the “Include when building project” switch, just that you also put the file away, although the switch can be ignored when building the project, this folder cannot. If you have files you no longer want in your main project, you can move them to the “Outtakes” folder. This is equivalent to turning off the “Include when building project” switch, just that you also put the file away, although the switch can be ignored when building the project, this folder cannot.
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ 96b68994dfa3d:e7ded148d6e4a:7031beac91f75:NOVEL:NOTE:A Note on Structure %%~name: A Note on Structure
%%~path: e7ded148d6e4a/96b68994dfa3d
%%~kind: NOVEL/NOTE
# A Note on Structure # A Note on Structure
This file is just a note. You can save notes anywhere you like in the project tree. Notes can be filtered out when you export the project. This file is just a note. You can save notes anywhere you like in the project tree. Notes can be filtered out when you export the project.
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ 974e400180a99:7031beac91f75:NOVEL:PAGE:Page %%~name: Page
%%~path: 7031beac91f75/974e400180a99
%%~kind: NOVEL/PAGE
This is a plain page with some text on it. This is a plain page with some text on it.
This file should receive no special formatting, but the text will always be left aligned and the content will always start on a fresh page when the project is exported. This file should receive no special formatting, but the text will always be left aligned and the content will always start on a fresh page when the project is exported.
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ ae7339df26ded:e7ded148d6e4a:7031beac91f75:NOVEL:SCENE:We Found John! %%~name: We Found John!
%%~path: e7ded148d6e4a/ae7339df26ded
%%~kind: NOVEL/SCENE
### We Found John! ### We Found John!
@pov: John @pov: John
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ b3e74dbc1f584:15c4492bd5107:WORLD:NOTE:Earth %%~name: Earth
%%~path: 15c4492bd5107/b3e74dbc1f584
%%~kind: WORLD/NOTE
# Earth # Earth
@tag: Earth @tag: Earth
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ b8136a5a774a0:98acd8c76c93a:NOVEL:SCENE:Delete Me! %%~name: Delete Me!
%%~path: 98acd8c76c93a/b8136a5a774a0
%%~kind: NOVEL/SCENE
### Delete Me! ### Delete Me!
This scene is trash. This scene is trash.
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ ba8a28a246524:e7ded148d6e4a:7031beac91f75:NOVEL:UNNUMBERED:Interlude %%~name: Interlude
%%~path: e7ded148d6e4a/ba8a28a246524
%%~kind: NOVEL/UNNUMBERED
## Interlude ## Interlude
% Notice that this is a file with the flag N.Un. The N means its a novel file, and the Un means its an unnumbered chapter. Unnumbered chapters can be treated separately from numbered chapters during export. Perfect for when you want to add an interlude, or for a prologue or epilogue. % Notice that this is a file with the flag N.Un. The N means its a novel file, and the Un means its an unnumbered chapter. Unnumbered chapters can be treated separately from numbered chapters during export. Perfect for when you want to add an interlude, or for a prologue or epilogue.
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ bb2c23b3c42cc:f7e2d9f330615:f6622b4617424:CHARACTER:NOTE:Jane Smith %%~name: Jane Smith
%%~path: f7e2d9f330615/bb2c23b3c42cc
%%~kind: CHARACTER/NOTE
# Jane Smith # Jane Smith
@tag: Jane @tag: Jane
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ bc0cbd2a407f3:e7ded148d6e4a:7031beac91f75:NOVEL:SCENE:Another Scene %%~name: Another Scene
%%~path: e7ded148d6e4a/bc0cbd2a407f3
%%~kind: NOVEL/SCENE
### Another Scene ### Another Scene
@pov: John @pov: John
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ edca4be2fcaf8:7031beac91f75:NOVEL:PARTITION:Part One %%~name: Part One
%%~path: 7031beac91f75/edca4be2fcaf8
%%~kind: NOVEL/PARTITION
# Part One # Part One
The first part. The first part.
+3 -1
View File
@@ -1,4 +1,6 @@
%%~ f1471bef9f2ae:15c4492bd5107:WORLD:NOTE:Space %%~name: Space
%%~path: 15c4492bd5107/f1471bef9f2ae
%%~kind: WORLD/NOTE
# Space # Space
@tag: Space @tag: Space
+5 -5
View File
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.0b3" hexVersion="0x010000b3" fileVersion="1.2" timeStamp="2020-09-19 23:16:35"> <novelWriterXML appVersion="1.0b5" hexVersion="0x010000b5" fileVersion="1.2" timeStamp="2020-10-24 18:57:05">
<project> <project>
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
<author>Jane Smith</author> <author>Jane Smith</author>
<author>Jay Doh</author> <author>Jay Doh</author>
<saveCount>744</saveCount> <saveCount>766</saveCount>
<autoCount>144</autoCount> <autoCount>148</autoCount>
<editTime>36162</editTime> <editTime>37687</editTime>
</project> </project>
<settings> <settings>
<doBackup>False</doBackup> <doBackup>False</doBackup>
@@ -120,7 +120,7 @@
<charCount>1811</charCount> <charCount>1811</charCount>
<wordCount>318</wordCount> <wordCount>318</wordCount>
<paraCount>8</paraCount> <paraCount>8</paraCount>
<cursorPos>1880</cursorPos> <cursorPos>1332</cursorPos>
</item> </item>
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a"> <item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
<name>Another Scene</name> <name>Another Scene</name>
+46 -2
View File
@@ -1,10 +1,54 @@
[metadata] [metadata]
license_files = LICENSE.md name = novelWriter
version = attr: nw.__version__
author = Veronica Berglyd Olsen
author_email = code@vkbo.net
description = A markdown-like document editor for writing novels
url = https://novelwriter.io
long_description = file: README.md
long_description_content_type = text/markdown
license_file = LICENSE.md
license = GNU General Public License v3
classifiers =
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: Implementation :: CPython
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Development Status :: 4 - Beta
Operating System :: OS Independent
Intended Audience :: End Users/Desktop
Natural Language :: English
Topic :: Text Editors
python_requires = >=3.6
install_requires =
pyqt5>=5.2.1
lxml>=4.2.0
pyenchant>=3.0.0
project_urls =
Bug Tracker = https://github.com/vkbo/novelWriter/issues
Documentation = https://github.com/vkbo/novelWriter/issues
Source Code = https://github.com/vkbo/novelWriter
[options]
include_package_data = True
packages = find:
[options.packages.find]
exclude = docs, tests, sample
[options.entry_points]
console_script =
novelWriter-cli = nw:main
gui_scripts =
novelWriter = nw:main
[bdist_wheel] [bdist_wheel]
universal = 0 universal = 0
[flake8] [flake8]
ignore = E203,E221,E226,E241,E251,E261,E266,E302,E305 ignore = E203,E221,E226,E228,E241,E251,E261,E266,E302,E305
max-line-length = 99 max-line-length = 99
exclude = docs/* exclude = docs/*
+247 -79
View File
@@ -1,32 +1,43 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
The main setup script for novelWeiter.
It runs the standard setuptool.setup() with all options taken from the
setup.cfg file.
In addtion, a few speicalised commands are available:
* sample: Will build a sample.zip file, which is the way the sample project is
included into distributable packages.
* qthelp: Will build a QtAssistant readable version of the novelWriter
documentation. This should also be a part of distributed packages. It allows
for reading the help offline. Otherwise, the F1 button redirects to the
online documentation only.
* launcher: Will attempt to install novelWriter icons, mime type and create a
launcher for the application.
"""
import os import os
import sys import sys
import shutil
import subprocess import subprocess
import setuptools
from nw import __version__, __url__, __docurl__, __issuesurl__, __sourceurl__ # =============================================================================================== #
# Qt Assistant Documentation Builder
# =============================================================================================== #
## def buildQtDocs():
# Build the Package """This function will build the documentation as a Qt help file. The
## file is then copied into the nw/assets/help directory and can be
included in builds.
buildDocs = False Depends on packages:
buildSample = False * pip install sphinx
* pip install sphinx-rtd-theme
if "qthelp" in sys.argv: * pip install sphinxcontrib-qthelp
buildDocs = True
sys.argv.remove("qthelp")
if "sample" in sys.argv:
buildSample = True
sys.argv.remove("sample")
##
# Qt Assistant Documentation
##
if buildDocs:
It also requires the qhelpgenerator to be available on the system.
"""
buildDir = os.path.join("docs", "build", "qthelp") buildDir = os.path.join("docs", "build", "qthelp")
helpDir = os.path.join("nw", "assets", "help") helpDir = os.path.join("nw", "assets", "help")
@@ -43,14 +54,14 @@ if buildDocs:
try: try:
subprocess.call(["make", "-C", "docs", "qthelp"]) subprocess.call(["make", "-C", "docs", "qthelp"])
except Exception as e: except Exception as e:
print("Failed with error:") print("QtHelp Build Error:")
print(str(e)) print(str(e))
buildFail = True buildFail = True
try: try:
subprocess.call(["qhelpgenerator", os.path.join(buildDir, inFile)]) subprocess.call(["qhelpgenerator", os.path.join(buildDir, inFile)])
except Exception as e: except Exception as e:
print("Failed with error:") print("QtHelp Build Error:")
print(str(e)) print(str(e))
buildFail = True buildFail = True
@@ -58,7 +69,7 @@ if buildDocs:
try: try:
os.mkdir(helpDir) os.mkdir(helpDir)
except Exception as e: except Exception as e:
print("Failed with error:") print("QtHelp Build Error:")
print(str(e)) print(str(e))
buildFail = True buildFail = True
@@ -70,22 +81,32 @@ if buildDocs:
os.rename(os.path.join(buildDir, outFile), os.path.join(helpDir, outFile)) os.rename(os.path.join(buildDir, outFile), os.path.join(helpDir, outFile))
os.rename(os.path.join(buildDir, datFile), os.path.join(helpDir, datFile)) os.rename(os.path.join(buildDir, datFile), os.path.join(helpDir, datFile))
except Exception as e: except Exception as e:
print("Failed with error:") print("QtHelp Build Error:")
print(str(e)) print(str(e))
buildFail = True buildFail = True
print("") print("")
if buildFail: if buildFail:
print("Documentation build: FAILED") print("Documentation build: FAILED")
sys.exit(1)
else: else:
print("Documentation build: OK") print("Documentation build: OK")
print("") print("")
## return
# Sample Project ZIP file
##
if buildSample: # =============================================================================================== #
# Sample Project ZIP File Builder
# =============================================================================================== #
def buildSampleZip():
"""Bundle the sample project into a single zip file to be saved into
the nw/assets folder for further bundling into builds.
"""
print("")
print("Building Sample ZIP File")
print("========================")
print("")
srcSample = "sample" srcSample = "sample"
dstSample = os.path.join("nw", "assets", "sample.zip") dstSample = os.path.join("nw", "assets", "sample.zip")
@@ -97,8 +118,10 @@ if buildSample:
from zipfile import ZipFile from zipfile import ZipFile
with ZipFile(dstSample, "w") as zipObj: with ZipFile(dstSample, "w") as zipObj:
print("Compressing: nwProject.nwx")
zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx") zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx")
for docFile in os.listdir(os.path.join(srcSample, "content")): for docFile in os.listdir(os.path.join(srcSample, "content")):
print("Compressing: content/%s" % docFile)
srcDoc = os.path.join(srcSample, "content", docFile) srcDoc = os.path.join(srcSample, "content", docFile)
zipObj.write(srcDoc, "content/"+docFile) zipObj.write(srcDoc, "content/"+docFile)
@@ -106,57 +129,202 @@ if buildSample:
print("Error: Could not find sample project source directory.") print("Error: Could not find sample project source directory.")
sys.exit(1) sys.exit(1)
if len(sys.argv) == 1: print("")
# Nothing more to do print("Built file: %s" % dstSample)
sys.exit(0) print("")
## return
# Build the Package
##
# Read content from files # =============================================================================================== #
with open("README.md", "r") as inFile: # Create Launcher
longDescription = inFile.read() # =============================================================================================== #
with open("requirements.txt", "r") as inFile: def xdgInstall():
pkgRequirements = inFile.read().strip().splitlines() """Will attempt to install icons and make a launcher.
"""
print("")
print("XDG Install")
print("===========")
print("")
setuptools.setup( # Find Executable(s)
name = "novelWriter", # ==================
version = __version__,
author = "Veronica Berglyd Olsen", exOpts = []
author_email = "code@vkbo.net",
description = "A markdown-like document editor for writing novels", testExec = shutil.which("novelWriter")
long_description = longDescription, if testExec is not None:
long_description_content_type = "text/markdown", exOpts.append(testExec)
license = "GNU General Public License v3",
url = __url__, testExec = shutil.which("novelwriter")
entry_points = { if testExec is not None:
"console_scripts" : ["novelWriter-cli=nw:main"], exOpts.append(testExec)
"gui_scripts" : ["novelWriter=nw:main"],
}, testExec = os.path.join(os.getcwd(), "novelWriter.py")
packages = setuptools.find_packages(exclude=["docs", "tests", "sample"]), if os.path.isfile(testExec):
include_package_data = True, exOpts.append(testExec)
package_data = {"": ["*.conf"]},
project_urls = { useExec = ""
"Bug Tracker": __issuesurl__, nOpts = len(exOpts)
"Documentation": __docurl__, if nOpts == 0:
"Source Code": __sourceurl__, print("Error: No executables for novelWriter found.")
}, sys.exit(1)
classifiers = [ elif nOpts == 1:
"Programming Language :: Python :: 3 :: Only", useExec = exOpts[0]
"Programming Language :: Python :: 3.6", else:
"Programming Language :: Python :: 3.7", print("Found multiple novelWriter executables:")
"Programming Language :: Python :: 3.8", print("")
"Programming Language :: Python :: 3.9", for iExec, anExec in enumerate(exOpts):
"Programming Language :: Python :: Implementation :: CPython", print(" [%d] %s" % (iExec, anExec))
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)", print("")
"Development Status :: 4 - Beta", intVal = int(input("Please select which novelWriter executable to use: "))
"Operating System :: OS Independent", print("")
"Intended Audience :: End Users/Desktop",
"Natural Language :: English", if intVal >= 0 and intVal < nOpts:
"Topic :: Text Editors", useExec = exOpts[intVal]
], else:
python_requires = ">=3.6", print("Error: Invalid selection.")
install_requires = pkgRequirements, sys.exit(1)
)
print("Using executable: %s " % useExec)
print("")
# Create and Install Launcher
# ===========================
desktopData = ""
with open("./setup/novelwriter.desktop", mode="r") as inFile:
desktopData = inFile.read()
desktopData = desktopData.replace(r"%%exec%%", useExec)
with open("./novelwriter.desktop", mode="w+") as outFile:
outFile.write(desktopData)
exCode = subprocess.call(
["xdg-desktop-menu", "install", "--novendor", "./novelwriter.desktop"]
)
if exCode == 0:
print("Installed menu desktop file")
else:
print(f"Error {exCode}: Could not install menu desktop file")
exCode = subprocess.call(
["xdg-desktop-icon", "install", "--novendor", "./novelwriter.desktop"]
)
if exCode == 0:
print("Installed icon desktop file")
else:
print(f"Error {exCode}: Could not install icon desktop file")
print("")
# Install MimeType
# ================
exCode = subprocess.call([
"xdg-mime", "install",
"./setup/mime/x-novelwriter-project.xml"
])
if exCode == 0:
print("Installed mimetype")
else:
print(f"Error {exCode}: Could not install mimetype")
print("")
# Install Icons
# =============
sizeArr = ["16", "22", "24", "32", "48", "64", "96", "128", "256", "512"]
# App Icon
for aSize in sizeArr:
exCode = subprocess.call([
"xdg-icon-resource", "install",
"--novendor", "--noupdate",
"--context", "apps",
"--size", aSize,
f"./setup/icons/scaled/icon-novelwriter-{aSize}.png",
"novelwriter"
])
if exCode == 0:
print(f"Installed app icon size {aSize}")
else:
print(f"Error {exCode}: Could not install app icon size {aSize}")
# Mimetype
for aSize in sizeArr:
exCode = subprocess.call([
"xdg-icon-resource", "install",
"--noupdate",
"--context", "mimetypes",
"--size", aSize,
f"./setup/icons/scaled/mime-novelwriter-{aSize}.png",
"application-x-novelwriter-project"
])
if exCode == 0:
print(f"Installed mime icon size {aSize}")
else:
print(f"Error {exCode}: Could not install mime icon size {aSize}")
# Update Cache
exCode = subprocess.call(["xdg-icon-resource", "forceupdate"])
if exCode == 0:
print("Updated icon cache")
else:
print(f"Error {exCode}: Could not update icon cache")
print("")
print("Done!")
print("")
return
# =============================================================================================== #
# Process Jobs
# =============================================================================================== #
if __name__ == "__main__":
helpMsg = (
"\n"
"novelWriter Setup Tool\n"
"======================\n"
"This tool provides some additional setup commands for novelWriter.\n"
"\n"
"help Print the help message.\n"
"qthelp Build the help documentation for use with the QtAssistant.\n"
"sample Build the sample project as a zip file.\n"
"xdg-install Install launcher and icons for freedesktop systems.\n"
)
if "help" in sys.argv:
sys.argv.remove("help")
print(helpMsg)
sys.exit(0)
if "qthelp" in sys.argv:
sys.argv.remove("qthelp")
buildQtDocs()
if "sample" in sys.argv:
sys.argv.remove("sample")
buildSampleZip()
if "xdg-install" in sys.argv:
sys.argv.remove("xdg-install")
if not sys.platform.startswith("win32"):
xdgInstall()
else:
print("ERROR: xdg-install cannot be used on Windows")
sys.exit(1)
if len(sys.argv) <= 1:
# Nothing more to do
sys.exit(0)
# Run the standard setup
import setuptools # noqa: F401
setuptools.setup()
# END Main
+48
View File
@@ -0,0 +1,48 @@
# Build and Install novelWriter
The root folder of the repository contains two scripts for setup and install:
## Script `setup.py`
The `setup.py` is a standard Python setup script with a couple of additional options:
* `qthelp`: Will attempt to build a single file QtAssistand documentation file.
This requires the Qt tools to be installed on the local system, as well as the sphinx build tools
for the documentation.
* `sample`: Will create a `sample.zip` file in the `nw/assets` folder.
This is the file the New Project Wizard uses to generate an example project.
If novelWriter is run from source, this file is not needed.
* `xdg-install`: Will install novelWriter icons, mimetype, and desktop and menu launcher on Linux desktops.
the application. This should work on standard Linux desktops.
By default, this is installed for the current user. Run with `sudo` to install system-wide.
To install novelWriter as a local Python package, run:
```bash
sudo python setup.py install
```
## Script `make.py`
The `make.py` script provides a number of convenient options for building packages if novelWriter.
Usage:
```bash
python make.py [command]
```
It currently accept the following commands:
* `help`: Print the help message.
* `freeze`: Freeze the package and produces a folder of all dependencies using pyinstaller.
* `onefile`: Build a standalone executable with all dependencies bundled.
Implies `freeze`, cannot be used with `setup`.
* `pip`: Run pip to install all package dependencies for novelWriter and this build tool.
* `setup`: Build a setup.exe installer for Windows.
This option automaticall disables the `onefile` option.
* `clean`: This will attempt to delete the `build` and `dist` folders in the current folder.
For instance, to create a Windows installer, run:
```bash
python make.py freeze setup
```

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Before

Width:  |  Height:  |  Size: 683 B

After

Width:  |  Height:  |  Size: 683 B

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 985 B

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Before

Width:  |  Height:  |  Size: 660 B

After

Width:  |  Height:  |  Size: 660 B

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 B

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

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