Merge branch 'main' into i18n-de_DE-created

This commit is contained in:
Veronica Berglyd Olsen
2022-11-17 16:23:14 +01:00
committed by GitHub
50 changed files with 3478 additions and 1244 deletions
+2 -2
View File
@@ -15,12 +15,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Python Setup - name: Python Setup
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: 3 python-version: 3
architecture: x64 architecture: x64
- name: Checkout Source - name: Checkout Source
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Install flake8 - name: Install flake8
run: pip install flake8 run: pip install flake8
- name: Syntax Error Check - name: Syntax Error Check
+3 -3
View File
@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Python Setup - name: Python Setup
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
architecture: x64 architecture: x64
@@ -27,7 +27,7 @@ jobs:
sudo apt update sudo apt update
sudo apt install libenchant-dev qttools5-dev-tools aspell-en sudo apt install libenchant-dev qttools5-dev-tools aspell-en
- name: Checkout Source - name: Checkout Source
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Install Dependencies (pip) - name: Install Dependencies (pip)
run: | run: |
pip install --upgrade pip pip install --upgrade pip
@@ -40,4 +40,4 @@ jobs:
export QT_QPA_PLATFORM=offscreen export QT_QPA_PLATFORM=offscreen
pytest -v --cov=novelwriter --timeout=60 pytest -v --cov=novelwriter --timeout=60
- name: Upload to Codecov - name: Upload to Codecov
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v3
+3 -3
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- name: Python Setup - name: Python Setup
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: "3.10" python-version: "3.10"
architecture: x64 architecture: x64
@@ -23,7 +23,7 @@ jobs:
run: | run: |
brew install enchant brew install enchant
- name: Checkout Source - name: Checkout Source
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Install Dependencies - name: Install Dependencies
run: | run: |
pip install --upgrade pip pip install --upgrade pip
@@ -35,4 +35,4 @@ jobs:
export QT_QPA_PLATFORM=offscreen export QT_QPA_PLATFORM=offscreen
pytest -v --cov=novelwriter --timeout=60 pytest -v --cov=novelwriter --timeout=60
- name: Upload to Codecov - name: Upload to Codecov
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v3
+3 -3
View File
@@ -15,12 +15,12 @@ jobs:
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- name: Python Setup - name: Python Setup
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: "3.10" python-version: "3.10"
architecture: x64 architecture: x64
- name: Checkout Source - name: Checkout Source
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Install Dependencies - name: Install Dependencies
run: | run: |
pip install --upgrade pip pip install --upgrade pip
@@ -30,4 +30,4 @@ jobs:
run: | run: |
pytest -v --cov=novelwriter --timeout=60 pytest -v --cov=novelwriter --timeout=60
- name: Upload to Codecov - name: Upload to Codecov
uses: codecov/codecov-action@v1 uses: codecov/codecov-action@v3
+1
View File
@@ -22,6 +22,7 @@ i18n/*.qph
/novelwriter/assets/manual.pdf /novelwriter/assets/manual.pdf
*.qch *.qch
*.qhc *.qhc
.~lock.*
# Python Temp # Python Temp
__pycache__ __pycache__
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

+1 -1
View File
@@ -42,7 +42,7 @@ templates_path = ["_templates"]
source_suffix = ".rst" source_suffix = ".rst"
master_doc = "index" master_doc = "index"
today_fmt = "%A, %d %B %Y at %H:%M" today_fmt = "%A, %d %B %Y at %H:%M"
language = None language = "en"
exclude_patterns = [] exclude_patterns = []
pygments_style = "sphinx" pygments_style = "sphinx"
pygments_dark_style = "monokai" pygments_dark_style = "monokai"
+2 -2
View File
@@ -27,8 +27,8 @@ JSON files. See the :ref:`a_breakdown_storage` section for more details.
Any operating system that can run Python 3 and has the Qt 5 libraries should be able to run Any operating system that can run Python 3 and has the Qt 5 libraries should be able to run
novelWriter. It runs fine on Linux, Windows and macOS, and users have tested it on other platforms novelWriter. It runs fine on Linux, Windows and macOS, and users have tested it on other platforms
too. novelWriter can be run directly from the Python source, installed from the pip tool. See too. novelWriter can be run directly from the Python source, or installed from packages or the pip
:ref:`a_started` for more details. tool. See :ref:`a_started` for more details.
.. note:: .. note::
Version 1.5 introduced a few changes that will require you to make a few minor modifications to Version 1.5 introduced a few changes that will require you to make a few minor modifications to
-15
View File
@@ -58,18 +58,3 @@ For novelWriter to be able to locate the custom theme files, you must copy them
Once the files are copied there, they should show up in :guilabel:`Preferences` with the label you Once the files are copied there, they should show up in :guilabel:`Preferences` with the label you
set as ``name`` inside the file. set as ``name`` inside the file.
Theme CSS Files
---------------
If you wish, you can also modify the CSS styles of the GUI in addition to change colour settings.
This is only available for GUI themes, and you do this by creating a file with the exact same file
name as the ``.conf`` file with colour settings and give it the ``.qss`` extension.
On Windows, file extensions may not be visible by default, so make sure you only have one file
extension, and don't end up with two.
The QSS files are Qt Style Sheet files. See Qt's
`The Style Sheet Syntax <https://doc.qt.io/qt-5/stylesheet-syntax.html>`_ documentation for more
details.
+10 -7
View File
@@ -4,10 +4,10 @@
Key Features Key Features
************ ************
novelWriter is a multi-document plain text editor using a markup syntax inspired by markdown to novelWriter is a multi-document plain text editor using a markup syntax inspired by Markdown to
apply simple formatting to the text. It is designed for writing novels, so the formatting features apply simple formatting to the text. It is designed for writing novels, so the formatting features
are limited. Your novel project is organised as a collection of separate plain text documents are limited to those relevant for this purpose. Your novel project is organised as a collection of
instead of a single, large document. separate plain text documents instead of a single, large document.
Below are some key features of novelWriter. Below are some key features of novelWriter.
@@ -27,7 +27,9 @@ Below are some key features of novelWriter.
You can split your novel project up into as many individual documents as you want to. When you You can split your novel project up into as many individual documents as you want to. When you
build the project, they are all glued together in the top-to-bottom order in which they appear build the project, they are all glued together in the top-to-bottom order in which they appear
in the project tree. You can use as few text documents as you like, but splitting the project up in the project tree. You can use as few text documents as you like, but splitting the project up
into chapters and scenes means you can easily reorder them using the drag and drop feature. into chapters and scenes means you can easily reorder them using the drag and drop feature. You
can start out with a few documents and then later split the document into multiple documents
based on its headers.
**Keep track of your plot elements** **Keep track of your plot elements**
All notes in your project can be assigned a *tag* you can *reference* from any other document or All notes in your project can be assigned a *tag* you can *reference* from any other document or
@@ -36,11 +38,12 @@ Below are some key features of novelWriter.
keywords. keywords.
**Get an overview of your plot elements** **Get an overview of your plot elements**
In the :guilabel:`Outline` tab on the main window you can see an outline of all the chapter and In the :guilabel:`Outline View` on the main window you can see an outline of all the chapters,
scene sections of your project. If they have any references in them, these are listed in scenes, and sections of your project. If they have any references in them, these are listed in
columns. You can also add a synopsis to each document, which can be listed here. You have the columns. You can also add a synopsis to each document, which can be listed here. You have the
option to add or remove columns of information from the outline. A subset of the outline option to add or remove columns of information from the outline. A subset of the outline
information is also available in the :guilabel:`Novel` tab under the main project tree. information is also available in the :guilabel:`Novel View` as a replacement for the main
project tree.
**Building your manuscript** **Building your manuscript**
Whether you want to compile a manuscript, or export all your notes, or generate an outline of Whether you want to compile a manuscript, or export all your notes, or generate an outline of
+2 -2
View File
@@ -15,7 +15,7 @@ language that doesn't require a compiler to build and run. That means that the c
computer right out of the box, or from a zip file. computer right out of the box, or from a zip file.
While it is developed for Linux primarily, it runs just fine on Windows as well. It also works fine While it is developed for Linux primarily, it runs just fine on Windows as well. It also works fine
on macOS, but the author is not a mac user so less attention is paid to that platform. on macOS, but the author is not a mac user, so less attention is paid to that platform.
In order to run novelWriter, you also need a few additional packages. The user interface is built In order to run novelWriter, you also need a few additional packages. The user interface is built
with `Qt 5 <https://www.qt.io/>`_, a cross platform library for building graphical user interface with `Qt 5 <https://www.qt.io/>`_, a cross platform library for building graphical user interface
@@ -86,5 +86,5 @@ meta data for it to extract.
them for tags you've set so that it knows which file to open when you click on a reference. them for tags you've set so that it knows which file to open when you click on a reference.
:ref:`a_export` - Recommended Reading :ref:`a_export` - Recommended Reading
This section explains in more detail how the export tool works. In particular how you can This section explains in more detail how the build tool works. In particular how you can
control the way chapter titles are formatted, and how scene and section breaks are handled. control the way chapter titles are formatted, and how scene and section breaks are handled.
+13
View File
@@ -110,6 +110,19 @@ needed package is called `qttools5-dev-tools`.
the ``i18n`` folder of the source code. the ``i18n`` folder of the source code.
.. _a_source_sample:
Building the Example Project
============================
In order to be able to create new projects from example files, you need a ``sample.zip`` file in
the ``assets`` folder of the source. This file can be built from setup script by running:
.. code-block:: bash
python setup.py sample
.. _a_source_docs: .. _a_source_docs:
Building the Documentation Building the Documentation
+18 -9
View File
@@ -15,6 +15,7 @@ Getting Started
.. _python.org: https://www.python.org/downloads/windows .. _python.org: https://www.python.org/downloads/windows
.. _Releases: https://github.com/vkbo/novelWriter/releases .. _Releases: https://github.com/vkbo/novelWriter/releases
.. _RPM: https://github.com/vkbo/novelWriter/issues/907 .. _RPM: https://github.com/vkbo/novelWriter/issues/907
.. _AppImage: https://appimage.org/
If you are using Windows or a Debian-based Linux distribtuion, you can install novelWriter from If you are using Windows or a Debian-based Linux distribtuion, you can install novelWriter from
package installers. If you are on macOS, you have the option to run novelWriter from a standalone package installers. If you are on macOS, you have the option to run novelWriter from a standalone
@@ -46,10 +47,10 @@ If you have any issues, try uninstalling the previous version and making a fresh
already had a version installed via a different method, you should uninstall that first. already had a version installed via a different method, you should uninstall that first.
.. _a_started_debian: .. _a_started_linux:
Install on Debian/Ubuntu/Mint Install on Linux
============================= ================
A Debian package can be downloaded from the `main website`_, or from the Releases_ page on GitHub. A Debian package can be downloaded from the `main website`_, or from the Releases_ page on GitHub.
This package should work on both Debian, Ubuntu and Linux Mint. This package should work on both Debian, Ubuntu and Linux Mint.
@@ -57,8 +58,8 @@ This package should work on both Debian, Ubuntu and Linux Mint.
If you prefer, you can also add the novelWriter repository on Launchpad to your package manager. If you prefer, you can also add the novelWriter repository on Launchpad to your package manager.
Ubuntu and Mint Ubuntu
--------------- ------
You can add the Ubuntu PPA_ and install novelWriter with the following commands. You can add the Ubuntu PPA_ and install novelWriter with the following commands.
@@ -71,11 +72,11 @@ You can add the Ubuntu PPA_ and install novelWriter with the following commands.
If you want pre-releases, add the ``ppa:vkbo/novelwriter-pre`` repository instead. If you want pre-releases, add the ``ppa:vkbo/novelwriter-pre`` repository instead.
Debian Debian and Mint
------ ---------------
Since this is a pure Python package, the Launchpad PPA can in principle also be used on Debian. Since this is a pure Python package, the Launchpad PPA can in principle also be used on Debian or
However, the above command will fail to add the signing key. Mint. However, the above command will fail to add the signing key.
Instead, run the following commands to add the repository and key: Instead, run the following commands to add the repository and key:
@@ -97,6 +98,14 @@ Then run the update and install commands as for Ubuntu:
different compression algorithm that Debian doesn't currently support. different compression algorithm that Debian doesn't currently support.
Other Distros
-------------
For other Linux distros than the ones mentioned above, the primary option is AppImage_. These are
completely standalone images for the app that include the necessary environment to run novelWriter.
They can be run on any Linux distro.
.. _a_started_minimal: .. _a_started_minimal:
Minimal Package Install Minimal Package Install
+30 -31
View File
@@ -1,11 +1,12 @@
.. _a_export: .. _a_export:
****************** ***********************
Exporting Projects Building the Manuscript
****************** ***********************
The novelWriter project can be exported in various formats using the build tool available from You can at any time build a manuscript, an outline of your notes, or any other type of document
:guilabel:`Build Novel Project` in the :guilabel:`Tools` menu, or by pressing :kbd:`F5`. from the text in your project. All of this is handled by the :guilabel:`Build Novel Project` tool.
You can activate it from the sidebar, the :guilabel:`Tools` menu, or by pressing :kbd:`F5`.
.. _a_export_headers: .. _a_export_headers:
@@ -66,7 +67,7 @@ Scene Separators
If you don't want any titles for your scenes (or for your sections if you have them), you can leave If you don't want any titles for your scenes (or for your sections if you have them), you can leave
the formatting boxes empty. If so, an empty paragraph will be inserted between the scenes or the formatting boxes empty. If so, an empty paragraph will be inserted between the scenes or
sections instead resulting in a gap in the text. sections instead, resulting in a gap in the text.
Alternatively, if you want a separator between them, like the common ``* * *``, you can enter the Alternatively, if you want a separator between them, like the common ``* * *``, you can enter the
desired separator text in the formatting box. In fact, if the format is a piece of static text, it desired separator text in the formatting box. In fact, if the format is a piece of static text, it
@@ -78,13 +79,9 @@ will always be treated as a separator.
File Selection File Selection
============== ==============
Which documents and notes are selected for export can be controlled from the options on the left Which documents and notes are selected for the build can be controlled from the options on the left
side of the dialog window. The switch for :guilabel:`Include novel files` will enable or disable side of the dialog window. In addition, you can select to include the synopsis comments, regular
inclusion of novel documents, and the switch for :guilabel:`Include note files` will do the same comments, keywords, and even exclude the body text itself if you just want an outline.
for project notes. This allows for exporting just the novel, just your notes, or both, as you wish.
In addition, you can select to export the synopsis comments, regular comments, keywords, and even
exclude the body text itself.
.. tip:: .. tip::
If you for instance want to export a document with an outline of the novel, you can enable If you for instance want to export a document with an outline of the novel, you can enable
@@ -92,11 +89,9 @@ exclude the body text itself.
followed by the tags and references and the synopsis. followed by the tags and references and the synopsis.
If you need to exclude specific documents from your exports, like draft documents or documents you If you need to exclude specific documents from your exports, like draft documents or documents you
want to take out of your manuscript, but don't want to delete, you can un-check the want to take out of your manuscript, but don't want to delete, you can set the documents as
:guilabel:`Include when building project` option for each such document in the project tree. An "inactive" in the project tree. :guilabel:`Build Novel Project` tool has a switch to collectively
included document has a checkmark after in the third column of the project tree. The exclude inactive documents.
:guilabel:`Build Novel Project` tool has a switch to ignore this flag if you need to collectively
override these settings.
.. _a_export_print: .. _a_export_print:
@@ -108,13 +103,17 @@ The print button allows you to print the content in the preview window. You can
of your system's printers, or print directly to a file as PDF. You can also print to file from the of your system's printers, or print directly to a file as PDF. You can also print to file from the
regular print dialog. The direct to file option is just a shortcut. regular print dialog. The direct to file option is just a shortcut.
.. note::
The paper format should in all cases default to whatever your system default is. Of you want to
change it, you have to select it from the :guilabel:`Print Preview`` dialog.
.. _a_export_formats: .. _a_export_formats:
Export Formats Export Formats
============== ==============
Currently, six formats are supported for exporting. Currently, six formats are supported.
Open Document Format Open Document Format
The Build tool can produce either an ``.odt`` file, or an ``.fodt`` file. The latter is just a The Build tool can produce either an ``.odt`` file, or an ``.fodt`` file. The latter is just a
@@ -122,9 +121,9 @@ Open Document Format
former, and a few the latter. former, and a few the latter.
novelWriter HTML novelWriter HTML
The HTML export format writes a single ``.htm`` file with minimal style formatting. The exported The HTML format writes a single ``.htm`` file with minimal style formatting. The HTML document
HTML document is suitable for further processing by document conversion tools like Pandoc, for is suitable for further processing by document conversion tools like Pandoc, for importing in
importing in word processors, or for printing from browser. word processors, or for printing from browser.
novelWriter Markdown novelWriter Markdown
This is simply a concatenation of the project documents selected by the filters. The documents This is simply a concatenation of the project documents selected by the filters. The documents
@@ -133,23 +132,23 @@ novelWriter Markdown
import back into novelWriter. import back into novelWriter.
Standard/GitHub Markdown Standard/GitHub Markdown
The Markdown export format comes in both Standard and GitHub flavour. The *only* difference in The Markdown format comes in both Standard and GitHub flavour. The *only* difference in terms of
terms of novelWriter functionality is the support for strikethrough text, which is not supported novelWriter functionality is the support for strikethrough text, which is not supported by the
by the Standard flavour, but *is* supported by the GitHub flavour. Standard flavour, but *is* supported by the GitHub flavour.
.. _a_export_options: .. _a_export_options:
Additional Export Options Additional Formats
========================= ==================
In addition to the above document formats, the novelWriter HTML and Markdown formats can also be In addition to the above document formats, the novelWriter HTML and Markdown formats can also be
wrapped in a JSON file. These files will have a meta data entry and a body entry. For HTML, also wrapped in a JSON file. These files will have a meta data entry and a body entry. For HTML, also
the accompanying css styles are exported. the accompanying css styles are included.
The text body is saved in a two-level list. The outer list contains one entry per exported The text body is saved in a two-level list. The outer list contains one entry per document, in the
document, in the order they appear in the project tree. Each document is then split up into a list order they appear in the project tree. Each document is then split up into a list as well, with one
as well, with one entry per paragraph it contains. entry per paragraph it contains.
These files are mainly intended for scripted post-processing for those who want that option. A JSON These files are mainly intended for scripted post-processing for those who want that option. A JSON
file can be imported directly into a Python dict object or a PHP array, to mentions a few options. file can be imported directly into a Python dict object or a PHP array, to mentions a few options.
+29 -7
View File
@@ -25,9 +25,9 @@ Tags in Notes
Each new heading in a note can have a tag associated with it. The format of a tag is Each new heading in a note can have a tag associated with it. The format of a tag is
``@tag: tagname``, where tagname is a unique identifier. Tags can then be referenced in the novel ``@tag: tagname``, where tagname is a unique identifier. Tags can then be referenced in the novel
documents, or cross-referenced in other notes, and will show up in the outline view and in the documents, or cross-referenced in other notes, and will show up in the Outline View and in the
back-reference panel when a document is being viewed. See :ref:`a_struct_tags` for how to reference back-reference panel when a document is opened in the viewer. See :ref:`a_struct_tags` for how to
notes. reference notes.
The syntax highlighter will alert the user that the keyword is correctly used and that the tag is The syntax highlighter will alert the user that the keyword is correctly used and that the tag is
allowed, that is, the tag is unique. Duplicate tags should be detected as long as the index is up allowed, that is, the tag is unique. Duplicate tags should be detected as long as the index is up
@@ -36,15 +36,37 @@ colour that valid tags do.
The tag is the only part of these notes that the application uses. The rest of the document content The tag is the only part of these notes that the application uses. The rest of the document content
is there for the writer to use in whatever way they wish. Of course, the content of the documents is there for the writer to use in whatever way they wish. Of course, the content of the documents
can be exported if you want to compile a single document of all your notes, or include them in an can be added to the manuscript, or an outline document. If you want to compile a single document of
outline. all your notes, you can do this from the :guilabel:`Build Novel Project` tool.
A note can also reference other notes in the same way novel documents do. When the note is opened A note can also reference other notes in the same way novel documents do. When the note is opened
in the view panel, the references become clickable links, making it easier to follow connections in in the view panel, the references become clickable links, making it easier to follow connections in
the plot. Notes don't show up in the outline view though, so referencing between notes is only the plot. Notes don't show up in the Outline View though, so referencing between notes is only
meaningful if you want to be able to click-navigate between them. meaningful if you want to be able to click-navigate between them, or of course if you just want to
highlight that two notes are related.
.. tip:: .. tip::
If you cross-reference between notes and export your project as an HTML document using the If you cross-reference between notes and export your project as an HTML document using the
:guilabel:`Build Novel Project` tool, the cross-references become clickable links in the :guilabel:`Build Novel Project` tool, the cross-references become clickable links in the
exported HTML document. exported HTML document.
Example of a project note with two headers, with separate tags, and with references to other notes:
.. code-block:: none
:linenos:
# Main Characters
## Jane Doe
@tag: Jane
@location: Earth
Something about Jane ...
## John Doh
@tag: John
@location: Mars
Something about John ...
+67 -50
View File
@@ -33,12 +33,14 @@ other root folders. These other root folder types are intended for your notes on
elements of your story. Using them is of course entirely optional. elements of your story. Using them is of course entirely optional.
A new project may not have all of the root folders present, but you can add the ones you want from A new project may not have all of the root folders present, but you can add the ones you want from
:guilabel:`Create Root Folder` in the :guilabel:`Project` menu. the project tree.
Each root folder has one or more reference keyword associated with it that can be used to reference Each root folder has one or more reference keyword associated with it that can be used to reference
content in your notes from other documents and notes. The intended usage of each type of root tags in your notes from other documents and notes. The intended usage of each type of root folder
folder is listed below. However, aside from the :guilabel:`Novel` folder, no restrictions are is listed below. However, aside from the :guilabel:`Novel` folder, no restrictions are applied by
applied by the application. You can use them however you want. the application. You can use them however you want.
You can make multiple root folders of each kind.
:guilabel:`Novel` :guilabel:`Novel`
This is the root folder of all text that goes into the final novel. This class of documents have This is the root folder of all text that goes into the final novel. This class of documents have
@@ -51,9 +53,9 @@ applied by the application. You can use them however you want.
folder can be references using the ``@plot`` keyword. folder can be references using the ``@plot`` keyword.
:guilabel:`Characters` :guilabel:`Characters`
Character notes go in this root folder. These are especially important if one wants to use the Character notes go in this root folder. These are especially important if you want to use the
Outline view to see which character appears where, and which part of the story is told from a Outline View to see which character appears where, and which part of the story is told from a
specific character's point-of-view or focusing on a particular character's storyline. Tags in specific character's point-of-view, or focusing on a particular character's storyline. Tags in
this folder can be referenced using the ``@pov`` keyword for point-of-view characters, this folder can be referenced using the ``@pov`` keyword for point-of-view characters,
``@focus`` for a focus character, or the ``@char`` keyword for any other characters. ``@focus`` for a focus character, or the ``@char`` keyword for any other characters.
@@ -86,6 +88,28 @@ information about the tags listed, see :ref:`a_struct_tags`.
You can rename root folders to whatever you want. However, this doesn't change the reference You can rename root folders to whatever you want. However, this doesn't change the reference
keyword. keyword.
Example of a character note:
.. code-block:: none
:linenos:
# Jane Doe
@tag: Jane
Some information about the character Jane Doe.
Example of a novel scene referencing the above character:
.. code-block:: none
:linenos:
### Chapter 1, Scene 1
@pov: Jane
When Jane woke up that morning ...
.. _a_proj_roots_del: .. _a_proj_roots_del:
@@ -97,9 +121,8 @@ trash folder can then be deleted permanently, either individually, or by emptyin
the menu. Documents in the trash folder are removed from the project index and cannot be the menu. Documents in the trash folder are removed from the project index and cannot be
referenced. referenced.
Folders and root folders can only be deleted when they are empty. Recursive deletion is not A document or a folder can be deleted from the :guilabel:`Project` menu, or by pressing
supported. A document or a folder can be deleted from the :guilabel:`Project` menu, or by pressing :kbd:`Ctrl`:kbd:`Shift`:kbd:`Del`. Root folders can only be deleted when they are empty.
:kbd:`Ctrl`:kbd:`Shift`:kbd:`Del`.
.. _a_proj_roots_out: .. _a_proj_roots_out:
@@ -109,9 +132,7 @@ Archived Documents
If you don't want to delete a document, or put it in the :guilabel:`Trash` folder where it may be If you don't want to delete a document, or put it in the :guilabel:`Trash` folder where it may be
deleted, but still want it out of your main project tree, you can create an :guilabel:`Archive` deleted, but still want it out of your main project tree, you can create an :guilabel:`Archive`
root folder from the :guilabel:`Project` menu. You are not allowed to move entire folders to this root folder.
root folder, only documents. If you need folders in it to organise your documents, you can of
course create new ones there.
You can drag any document to this folder and preserve its settings. The document will always be You can drag any document to this folder and preserve its settings. The document will always be
excluded from the :guilabel:`Build Novel Project` builds. It is also removed from the project excluded from the :guilabel:`Build Novel Project` builds. It is also removed from the project
@@ -179,9 +200,10 @@ and to be able to collapse and hide them in the project tree when you're not wor
documents. documents.
.. tip:: .. tip::
You can use folders to sort your scene documents into chapters. You will still need to add a You can add child documents to other documents. This is particularly useful when you create
chapter document as the first item of your chapter folder, and the scene documents as the chapters and scenes. If you add separate scene documents, you should also add separate chapter
following items. Other ways to use folders is to make a folder for each act or part. documents, even if they only contain a chapter heading. You can then add scene documents as
child items to the chapters.
.. _a_proj_files: .. _a_proj_files:
@@ -189,21 +211,15 @@ documents.
Project Documents Project Documents
================= =================
New documents can be created from the :guilabel:`Document` menu, or by pressing :kbd:`Ctrl`:kbd:`N` New documents can be created from tool bar in the Project Tree, or by pressing :kbd:`Ctrl`:kbd:`N`.
while in the project tree. This will create a new, empty document, and open the :guilabel:`Item This will open the create new item menu and let you choose between a number of pre-defined
Settings` dialog where the document label and various other settings can be changed. This dialog documents and folders. You will be prompted for a label for the new item. You can always rename an
can also be opened again later from either the :guilabel:`Project` menu, selecting :guilabel:`Edit item by selecting :guilabel:`Rename Item` from the :guilabel:`Project` menu, or by pressing
Project Item`, or by pressing :kbd:`F2` with the item selected. :kbd:`F2`.
The layout of the document is also defined here. The two options available are :guilabel:`Novel Other settings for project items are available from the context menu that you can activate by
Document` and :guilabel:`Project Note`. These behave differently when the project is built. A right-clicking on them inb the Project Tree. The :guilabel:`Transform` submenu includes options for
project note is never treated as part of the novel, no matter where in the project it is located. converting, splitting, or merging items.
See :ref:`a_struct_layout` for more details.
You can also select whether the document is by default included when building the project. This
setting can be overridden in the :guilabel:`Build Novel Project` tool if you wish to include them
anyway. This is covered in the :ref:`a_export_files` section. You can also toggle the included
state of a document from the right-click context menu.
.. _a_proj_files_counts: .. _a_proj_files_counts:
@@ -237,17 +253,16 @@ The :guilabel:`Project Settings` can be accessed from the :guilabel:`Project` me
Settings Tab Settings Tab
------------ ------------
The :guilabel:`Settings` tab holds the project title and author settings. The :guilabel:`Settings` tab holds the project name, title, and author settings.
The :guilabel:`Working Title` can be set to a different title than the :guilabel:`Book Title`. The The :guilabel:`Project Name` can be set to a different value than the :guilabel:`Novel Title`. The
difference between them is simply that the :guilabel:`Working Title` is used for the GUI (main difference between them is simply that the :guilabel:`Project Name` is used for the GUI (main
window title) and for generating the backup files. The intention is that the :guilabel:`Working window title) and for generating the backup files. The intention is that the :guilabel:`Project
Title` should remain unchanged throughout the project, otherwise the name of exported files and Name` should remain unchanged throughout the project, otherwise the name of exported files and
backup files may change too. backup files may change too.
The :guilabel:`Book Title` and :guilabel:`Book Authors` settings are currently not used for The :guilabel:`Novel Title` and :guilabel:`Authors` settings are used when building the manuscript,
anything, so setting them is just for the benefit of the author. Future features may use them, and for some formats.
they are exported on some export formats in the :guilabel:`Build Novel Project` tool.
If your project is in a different language than your main spell checking is set to, you can If your project is in a different language than your main spell checking is set to, you can
override the default spell checking language here. You can also override the automatic backup override the default spell checking language here. You can also override the automatic backup
@@ -257,14 +272,14 @@ setting.
Status and Importance Tabs Status and Importance Tabs
-------------------------- --------------------------
Each document or folder of type :guilabel:`Novel` can be given a status level, signified by a Each document or folder of type :guilabel:`Novel` can be given a _Status_ label accompanied by a
coloured icon, and each document or folder of the remaining types can be given an importance level. coloured icon, and each document or folder of the remaining types can be given an _Importance_
These are colour coded icons and labels that can be applied to each document or folder. label.
These are purely there for the user's convenience, and you are not required to use them for any These are purely there for the user's convenience, and you are not required to use them for any
other features to work. No other part of novelWriter accesses this information. The intention is to other features to work. No other part of novelWriter accesses this information. The intention is to
use these to indicate at what stage of completion each novel document is, or how important the use these to indicate at what stage of completion each novel document is, or how important the
content of a note is to the plot. You don't have to use them this way, that's just what they were content of a note is to the story. You don't have to use them this way, that's just what they were
intended for, but you can make them whatever you want. intended for, but you can make them whatever you want.
See also :ref:`a_ui_tree_status`. See also :ref:`a_ui_tree_status`.
@@ -279,7 +294,7 @@ Auto-Replace Tab
A set of automatically replaced keywords can be added in this tab. The keywords in the left column A set of automatically replaced keywords can be added in this tab. The keywords in the left column
will be replaced by the text in the right column when documents are opened in the viewer. They will will be replaced by the text in the right column when documents are opened in the viewer. They will
also be applied to exports. also be applied to manuscript builds.
The auto-replace feature will replace text in angle brackets that are in this list. The syntax The auto-replace feature will replace text in angle brackets that are in this list. The syntax
highlighter will add an alternate colour to text marching the syntax, but it doesn't check if the highlighter will add an alternate colour to text marching the syntax, but it doesn't check if the
@@ -300,16 +315,17 @@ An automatic backup system is built into novelWriter. In order to use it, a back
the backup files are to be stored must be provided in :guilabel:`Preferences`. the backup files are to be stored must be provided in :guilabel:`Preferences`.
Backups can be run automatically when a project is closed, which also implies it is run when the Backups can be run automatically when a project is closed, which also implies it is run when the
application itself is closed. Backups are date stamped zip files of the entire project folder, and application itself is closed. Backups are date stamped zip files of the project files in the
are stored in a subfolder of the backup path. The subfolder will have the same name as the project project folder (files not strictly a part of the project are ignored). The zip archives are stored
:guilabel:`Working Title` set in :ref:`a_proj_settings`. in a subfolder of the backup path. The subfolder will have the same name as the
:guilabel:`Project Name` as defined in :ref:`a_proj_settings`.
The backup feature, when configured, can also be run manually from the :guilabel:`Tools` menu. The backup feature, when configured, can also be run manually from the :guilabel:`Tools` menu.
It is also possible to disable automated backups for a given project in :guilabel:`Project It is also possible to disable automated backups for a given project in
Settings`. :guilabel:`Project Settings`.
.. note:: .. note::
For the backup to be able to run, the :guilabel:`Working Title` must be set in For the backup to be able to run, the :guilabel:`Project Name` must be set in
:guilabel:`Project Settings`. This value is used to generate the folder name for the zip files. :guilabel:`Project Settings`. This value is used to generate the folder name for the zip files.
Without it, the backup will not run at all, but it will produce a warning message. Without it, the backup will not run at all, but it will produce a warning message.
@@ -320,7 +336,8 @@ Writing Statistics
================== ==================
When you work on a project, a log file records when you opened it, when you closed it, and the When you work on a project, a log file records when you opened it, when you closed it, and the
total word counts of your novel documents and notes at the end of the session. You can view this total word counts of your novel documents and notes at the end of the session provided that the
session lasted either more than 5 minutes, or that the total word count changed. You can view this
file in the ``meta`` folder in the directory where you saved your project. The file is named file in the ``meta`` folder in the directory where you saved your project. The file is named
``sessionStats.log``. ``sessionStats.log``.
+42 -31
View File
@@ -22,10 +22,9 @@ Four levels of headings are supported, signified by the number of hashes (``#``)
title. See also the :ref:`a_fmt` section for more details about the markdown syntax. title. See also the :ref:`a_fmt` section for more details about the markdown syntax.
.. note:: .. note::
The header levels are not only important when generating the exported novel file, they are also The header levels are not only important when generating the manuscript, they are also used by
used by the indexer when building the outline tree in the :guilabel:`Outline` tab as well as the the indexer when building the outline tree in the Outline as well as the Novel Tree. Each
:guilabel:`Novel` tab of the project tree. Each heading also starts a new region where new heading also starts a new region where new references and tags can be defined.
references and tags can be defined.
The syntax for the four basic header types, and the two special header types, is listed in section The syntax for the four basic header types, and the two special header types, is listed in section
:ref:`a_fmt_head`. The meaning of the four levels for the structure of your novel is as follows: :ref:`a_fmt_head`. The meaning of the four levels for the structure of your novel is as follows:
@@ -33,8 +32,7 @@ The syntax for the four basic header types, and the two special header types, is
**Header Level 1: Partition** **Header Level 1: Partition**
This header level signifies that the text refers to a top level partition. This is useful when This header level signifies that the text refers to a top level partition. This is useful when
you want to split the manuscript up into books, parts, or acts. These headings are not required. you want to split the manuscript up into books, parts, or acts. These headings are not required.
The novel title itself should use the special header level one code explained in The novel title itself should use the special header level explained in :ref:`a_fmt_head`.
:ref:`a_fmt_head`.
**Header Level 2: Chapter** **Header Level 2: Chapter**
This header level signifies a chapter level partition. Each time you want to start a new This header level signifies a chapter level partition. Each time you want to start a new
@@ -46,7 +44,7 @@ The syntax for the four basic header types, and the two special header types, is
**Header Level 3: Scene** **Header Level 3: Scene**
This header level signifies a scene level partition. You must provide a title text, but the This header level signifies a scene level partition. You must provide a title text, but the
title text can be replaced with a scene separator or just skipped entirely when you export your title text can be replaced with a scene separator or just skipped entirely when you build your
manuscript. manuscript.
**Header Level 4: Section** **Header Level 4: Section**
@@ -55,12 +53,12 @@ The syntax for the four basic header types, and the two special header types, is
mid-scene, like if you change the point-of-view character. You are free to use sections as you mid-scene, like if you change the point-of-view character. You are free to use sections as you
wish, and can filter them out of the final manuscript just like with scene titles. wish, and can filter them out of the final manuscript just like with scene titles.
Page breaks are automatically added before level 1 and 2 headers when you export your project to a Page breaks are automatically added before level 1 and 2 headers when you build your project to a
format that supports page breaks, or when you print the document directly from the build tool. If format that supports page breaks, or when you print the document directly from the build tool. If
you want page breaks in other places, you have to specify them manually. See :ref:`a_fmt_break`. you want page breaks in other places, you have to specify them manually. See :ref:`a_fmt_break`.
.. tip:: .. tip::
There are multiple options of how to process novel titles when exporting the manuscript. For There are multiple options of how to process novel titles when building the manuscript. For
instance, chapter numbers can be applied automatically, and so can scene numbers if you want instance, chapter numbers can be applied automatically, and so can scene numbers if you want
them in a draft manuscript. See the :ref:`a_export` page for more details. them in a draft manuscript. See the :ref:`a_export` page for more details.
@@ -70,19 +68,19 @@ you want page breaks in other places, you have to specify them manually. See :re
Novel Title and Front Matter Novel Title and Front Matter
---------------------------- ----------------------------
It is recommended that you add a document at the very top of your project with the novel title as It is recommended that you add a document at the very top of each Novel root folder with the novel
the first line. You should modify the level 1 header format code with an ``!`` in order to render title as the first line. You should modify the level 1 header format code with an ``!`` in order to
it as a document title that is excluded from any automatic Table of Content in an exported render it as a document title that is excluded from any automatic Table of Content in a manuscript
document, like so: build document, like so:
``#! My Novel`` ``#! My Novel``
The title is by default centred on the page when exported. You can add more text to the page as you The title is by default centred on the page. You can add more text to the page as you wish, like
wish, like for instance the author's name and details. for instance the author's name and details.
If you want an additional page of text after the title page, starting on a fresh page, you can add If you want an additional page of text after the title page, starting on a fresh page, you can add
``[NEW PAGE]`` on a line by itself, and continue the text after it. This will insert a page break ``[NEW PAGE]`` on a line by itself, and continue the text after it. This will insert a page break
when the project is exported. before the text.
.. _a_struct_heads_unnum: .. _a_struct_heads_unnum:
@@ -97,9 +95,9 @@ build tool to skip these chapters.
``##! Unnumbered Chapter Title`` ``##! Unnumbered Chapter Title``
There is a separate formatting feature for such chapters in the :guilabel:`Build Novel Project` There is a separate formatting feature for such chapters in the :guilabel:`Build Novel Project`
tool as well. See the :ref:`a_export` page for more details. When exporting to a format that tool as well. See the :ref:`a_export` page for more details. When building a document of a format
supports page breaks, also unnumbered chapters will have a page break added just like for normal that supports page breaks, also unnumbered chapters will have a page break added just like for
chapters. normal chapters.
.. Note:: .. Note::
Previously, you could also disable the automatic numbering of a chapter by adding an ``*`` as Previously, you could also disable the automatic numbering of a chapter by adding an ``*`` as
@@ -110,14 +108,13 @@ chapters.
.. _a_struct_tags: .. _a_struct_tags:
Tag References Note References
============== ===============
Each text partition, indicated by a heading of any level, can contain references to tags set in the Each text partition, indicated by a heading of any level, can contain references to tags set in the
supporting notes of the project. The references are gathered by the indexer and used to generate an project notes of the project. The references are gathered by the indexer and used to generate the
outline view on the :guilabel:`Outline` tab of how the different parts of the novel are connected. Outline View. This section covers how to make references to tags. See :ref:`a_notes_tags` for how
This section covers how to set references to tags. See :ref:`a_notes_tags` for how to define tags to define tags the references can point to.
the references can point to.
References and tags are also clickable in the document editor and viewer, making it easy to References and tags are also clickable in the document editor and viewer, making it easy to
navigate between reference notes while writing. Clicked links are always opened in the view panel. navigate between reference notes while writing. Clicked links are always opened in the view panel.
@@ -171,6 +168,22 @@ The highlighter may be mistaken if the index of defined tags is out of date. If
to regenerate it, or select :guilabel:`Rebuild Index` from the :guilabel:`Tools` menu. In general, to regenerate it, or select :guilabel:`Rebuild Index` from the :guilabel:`Tools` menu. In general,
the index for a document is regenerated when it is saved, so this shouldn't normally be necessary. the index for a document is regenerated when it is saved, so this shouldn't normally be necessary.
Example of a novel document with references to characters and plots:
.. code-block:: none
:linenos:
## Chapter 1
@pov: Jane
### Scene 1
@char: John, Sam
@plot: Main
Once upon a time ...
.. _a_struct_layout: .. _a_struct_layout:
@@ -181,15 +194,13 @@ All documents in the project can have a layout format set. Previously, there wer
available to change how the documents where formatted on export. These have now been reduced to available to change how the documents where formatted on export. These have now been reduced to
just two layouts: :guilabel:`Novel Document` and :guilabel:`Project Note`. just two layouts: :guilabel:`Novel Document` and :guilabel:`Project Note`.
Novel documents can only live in the :guilabel:`Novel` root folder. You can also move them to Novel documents can only live in a :guilabel:`Novel` type root folder. You can also move them to
:guilabel:`Archive` and :guilabel:`Trash` of course. Project notes can be added anywhere in the :guilabel:`Archive` and :guilabel:`Trash` of course. Project notes can be added anywhere in the
project. project.
Depending on which icon theme you're using, the project tree can distinguish between the different The project tree can distinguish between the different layouts and header levels of the documents
layouts and header levels of the documents to help indicate which are project notes and which are using coloured icons, and optionally add emphasis on the label (See the :guilabel:`Preferences`.)
novel documents containing a partition, chapter, or scene. If the icon theme you've selected For novel documents, the heading level of the first heading is recorded, and indicated by the icon.
doesn't show a difference, you can still see the layout description in the details panel below the
project tree.
.. tip:: .. tip::
You can always start writing with a coarse setup with one or a few documents, and then later use You can always start writing with a coarse setup with one or a few documents, and then later use
+2 -2
View File
@@ -1,4 +1,4 @@
furo furo
sphinx~=4.0 sphinx>=4.0
pygments~=2.7 pygments>=2.7
docutils==0.17.1 docutils==0.17.1
+78 -41
View File
@@ -8,7 +8,7 @@ How it Works
The main features of novelWriter are listed in the :ref:`a_intro` section. Here, we go into some The main features of novelWriter are listed in the :ref:`a_intro` section. Here, we go into some
more details on how they are implemented. Later on in this documentation, these features will be more details on how they are implemented. Later on in this documentation, these features will be
covered in more detail. covered in even more detail.
.. _a_breakdown_design: .. _a_breakdown_design:
@@ -19,52 +19,80 @@ GUI Layout and Design
The user interface of novelWriter is intended to be as minimalistic as practically possible, while The user interface of novelWriter is intended to be as minimalistic as practically possible, while
at the same time provide a complete set of features needed for writing a novel. at the same time provide a complete set of features needed for writing a novel.
The main window does not have a toolbar like many other applications do. This reduces clutter, and The main window does not have an editor toolbar like many other applications do. This reduces
since the documents are formatted with style tags, is more or less redundant. However, most clutter, and since the documents are formatted with style tags, is more or less redundant. However,
formatting features supported are available through convenient keyboard shortcuts. They are also most formatting features supported are available through convenient keyboard shortcuts. They are
available in the main menu so you don't have to look up formatting codes every time you need them. alsoavailable in the main menu so you don't have to look up formatting codes every time you need
However, a list of all shortcuts can be found in the :ref:`a_kb` section. them. However, a list of all shortcuts can be found in the :ref:`a_kb` section.
.. note:: .. note::
novelWriter is not intended to be a full office type word processor. It doesn't support images, novelWriter is not intended to be a full office type word processor. It doesn't support images,
links, tables, and other complex structures and objects often needed for such documents. links, tables, and other complex structures and objects often needed for such documents.
Formatting is limited to headers, emphasis, text alignment, and a few other simple features. Formatting is limited to headers, emphasis, text alignment, and a few other simple features.
On the left edge of the main window, you will find a sidebar. This bar has buttons for the standard
views you can switch between, a quick link to the :guilabel:`Build Novel Project` tool, and a set
of project-related tools as well as quick access to settings at the bottom.
Window Tabs and Areas
---------------------
The main window is split in two, or optionally three, panels. The left-most panel contains the Project Tree View
project tree and all the documents in your project. The second panel is the document editor. An -----------------
optional third panel is a document viewer which can view any document in your project independently
of what is open in the document editor. It is not intended as a preview window, although you can When in :guilabel:`Project Tree View` mode, the main work area of the main window is split in two,
use it for this as well as it will apply the formatting tags you have specified. The main purpose or optionally three, panels. The left-most panel contains the project tree and all the documents in
of the viewer is for viewing your notes next to your editor while you're writing. your project. The second panel is the document editor. An optional third panel is a document viewer
which can view any document in your project independently of what is open in the document editor.
It is not intended as a preview window, although you can use it for this as well as it will apply
the formatting tags you have specified. The main purpose of the viewer is for viewing your notes
next to your editor while you're writing.
The editor also has a :guilabel:`Focus Mode` you can toggle either from the menu, or from the icon The editor also has a :guilabel:`Focus Mode` you can toggle either from the menu, or from the icon
in the editor header. When :guilabel:`Focus Mode` is enabled, all the user interface elements other in the editor header. When :guilabel:`Focus Mode` is enabled, all the user interface elements other
than the document editor itself are hidden away. than the document editor itself are hidden away.
A second tab is also available on the main window. This is the :guilabel:`Outline` tab where the
entire novel structure can be displayed, with all the tags and references listed. Depending on how Novel Tree View
you structure your novel documents, this outline can be quite different from your project tree. ---------------
Your project tree lists individual documents, your Outline tree lists the structure of the novel
itself in terms of partitions, chapters and scenes as it appears in the text of those documents. When in :guilabel:`Novel Tree View` mode, the project tree is replaces by an overview of your novel
structure. Instead of showing individual documents, the tree shows all headings of your novel text.
Each heading is indented according to the heading level. You can open and edit your novel documents
from this view as well. All headings contained in the currently open document should be highlighted
in the view.
If you have multiple Novel root folders, you can switch between them from the dropdown menu from
the buttons at the top of the tree view. You can also select to view an extra column of data. To
select its content, see the menu icon button.
If you click the arrow to the right of each item, a tooltip will pop up showing you all the meta
data collected for that heading entry.
Novel Outline View
------------------
When in :guilabel:`Novel Outline View` mode, the tree, editor and viewer will be replaced by a
large table that shows the entire novel structure with all the tags and references listed. Pretty
much all collected meta data is available here in different columns.
You can select which novel root folder to display from the dropdown box, and you can select which
columns to show or hide from the menu button. You can also rearrange the columns by drag and drop.
Colour Themes Colour Themes
------------- -------------
The colour theme of the user interface defaults to that of the host operating system. Some other The default colour theme of the user interface is the default theme from the Qt library. There is a
light and dark colour themes are provided, and can be enabled in :guilabel:`Preferences` from the standard dark theme provided as well, which is similar to the default Qt theme. Some other light
:guilabel:`Tools` menu. A number of syntax highlighting themes are also available in and dark colour themes are also provided. You can select which one you prefer from in
:guilabel:`Preferences`. Icon themes for light and dark GUIs are also available. The icons are :guilabel:`Preferences` from :guilabel:`Settings` or the :guilabel:`Tools` menu.
based on the Typicons_ icon set designed by Stephen Hutchings.
A number of syntax highlighting themes are also available in :guilabel:`Preferences`. These are
separate settings because there are a lot more options for syntax highlighting.
.. note:: .. note::
The GUI colour theme and the syntax highlighting theme are separate settings in If you switch to dark mode on the GUI, you should also switch the icon theme and syntax
:guilabel:`Preferences`. If you switch to dark mode on the GUI, you should also switch the icon highlighting theme, otherwise icons may be hard to see.
theme and syntax highlighting theme.
.. _a_breakdown_project: .. _a_breakdown_project:
@@ -75,10 +103,11 @@ Project Layout
This is a brief introduction to how you structure your writing projects. All of this will be This is a brief introduction to how you structure your writing projects. All of this will be
covered in more detail later. covered in more detail later.
The main point is that you are free to organise your project documents as you wish into subfolders, The main point of novelWriter is that you are free to organise your project documents as you wish
and split the text between documents in whatever way suits you. All that matters to novelWriter is into subfolders or subdocuments, and split the text between these documents in whatever way suits
the linear order the documents appear at in the project tree (top to bottom). The chapters, scenes you. All that matters to novelWriter is the linear order the documents appear at in the project
and sections of the novel are determined by the headings within those documents. tree (top to bottom). The chapters, scenes and sections of the novel are determined by the headings
within those documents.
The four heading levels (**H1** to **H4**) are treated as follows: The four heading levels (**H1** to **H4**) are treated as follows:
@@ -87,33 +116,40 @@ The four heading levels (**H1** to **H4**) are treated as follows:
* **H3** is used for scene titles optionally replaced by separators. * **H3** is used for scene titles optionally replaced by separators.
* **H4** is for section titles within scenes, if such granularity is needed. * **H4** is for section titles within scenes, if such granularity is needed.
The project tree will select an icon for the document based on the first heading in it.
This header level structure is only taken into account for novel documents. For the project notes, This header level structure is only taken into account for novel documents. For the project notes,
the header levels have no structural meaning, and the user is free to do whatever they want. See the header levels have no structural meaning, and the user is free to do whatever they want. See
:ref:`a_struct` and :ref:`a_notes` for more details. :ref:`a_struct` and :ref:`a_notes` for more details.
.. note::
You can add documents as child items of other documents if you wish. This is often more useful
than adding folders, since you may want to have the chapter heading in a separate document from
your individual scene documents.
.. _a_breakdown_export: .. _a_breakdown_export:
Project Export Building the Manuscript
============== =======================
The project can at any time be exported to a range of different formats through the The project can at any time be assembled into a range of different formats through the
:guilabel:`Build Novel Project` tool. Natively, novelWriter supports export to Open Document, :guilabel:`Build Novel Project` tool. Natively, novelWriter supports Open Document, HTML5, and
HTML5, and various flavours of Markdown. various flavours of Markdown.
The HTML5 export format is suitable for conversion by a number of other tools like Pandoc, or for The HTML5 format is suitable for conversion by a number of other tools like Pandoc, or for
importing into word processors if the Open Document format isn't suitable. In addition, printing importing into word processors if the Open Document format isn't suitable. In addition, printing
and printing to PDF is also possible. and printing to PDF is also possible.
You can also export the content of the project to a JSON file. This is useful if you want to write You can also export the content of the project to a JSON file. This is useful if you want to write
your own processing script in for instance Python as the entire novel can be read into a Python your own processing script in for instance Python, as the entire novel can be read into a Python
dictionary with a couple of lines of code. The JSON file can be populated either with HTML dictionary with a couple of lines of code. The JSON file can be populated either with HTML
formatted text, or with the raw text as typed into the novel documents. See :ref:`a_export_options` formatted text, or with the raw text as typed into the novel documents. See :ref:`a_export_options`
for more details. for more details.
A number of filter options can be applied to the :guilabel:`Build Novel Project` tool, allowing you A number of filter options can be applied to the :guilabel:`Build Novel Project` tool, allowing you
to export a draft manuscript, a reference document of notes, an outline based on chapter and scene to make a draft manuscript, a reference document of notes, an outline based on chapter and scene
titles with a synopsis each, and so on. See :ref:`a_export` for more details on export features and titles with a synopsis each, and so on. See :ref:`a_export` for more details on build features and
formats. formats.
@@ -132,7 +168,8 @@ project is saved directly to your project folder in separate files. Only the pro
the text you are currently editing is stored in memory at any given time. Secondly, having multiple the text you are currently editing is stored in memory at any given time. Secondly, having multiple
small files means it is very easy to sync them between computers with standard file synchronisation small files means it is very easy to sync them between computers with standard file synchronisation
tools. Thirdly, if you use version control software to track the changes to your project, the file tools. Thirdly, if you use version control software to track the changes to your project, the file
formats used for the files are well suited. Also the JSON documents have line breaks and indents. formats used for the files are well suited. Also the JSON documents have line breaks and indents,
which makes it easier to track them with version control software.
.. note:: .. note::
+22 -21
View File
@@ -37,34 +37,34 @@ can select them for :guilabel:`Preferences`.
Headings Headings
======== ========
Four levels of headings are allowed. For project notes they are free to be used as you see fit. Four levels of headings are allowed. For project notes, they are free to be used as you see fit.
That is, novelWriter doesn't assign the different headings any meaning. However, for novel That is, novelWriter doesn't assign the different headings any particular meaning. However, for
documents they indicate the structural level of the novel and must be used correctly to produce the novel documents they indicate the structural level of the novel and must be used correctly to
intended result. See :ref:`a_struct_heads` for more details. produce the intended result. See :ref:`a_struct_heads` for more details.
``# Title Text`` ``# Title Text``
Heading level one. For novel documents, the header level indicates the start of a new partition. Heading level one. For novel documents, the header level indicates the start of a new partition.
``## Title Text`` ``## Title Text``
Heading level two. For novel documents, the header level indicates the start of a new chapter. Heading level two. For novel documents, the header level indicates the start of a new chapter.
Chapter numbers can be inserted automatically when exporting the manuscript. Chapter numbers can be inserted automatically when building the manuscript.
``### Title Text`` ``### Title Text``
Heading level three. For novel documents, the header level indicates the start of a new scene. Heading level three. For novel documents, the header level indicates the start of a new scene.
Scene numbers or scene separators can be inserted automatically when exporting the manuscript, Scene numbers or scene separators can be inserted automatically when building the manuscript,
so you can use the title field as a working title for your scenes if you wish. so you can use the title field as a working title for your scenes if you wish.
``#### Title Text`` ``#### Title Text``
Heading level four. For novel documents, the header level indicates the start of a new section. Heading level four. For novel documents, the header level indicates the start of a new section.
Section titles can be replaced by separators or removed completely when exporting the Section titles can be replaced by separators or removed completely when building the manuscript.
manuscript.
For headers level one and two, adding a ``!`` modifies the behaviour of the heading: For headers level one and two, adding a ``!`` modifies the behaviour of the heading:
``#! Title Text`` ``#! Title Text``
This tells the build tool that the level one heading is intended to be used for the novel's This tells the build tool that the level one heading is intended to be used for the novel's
main title, like for instance on the front page. When exporting, this will use a different main title, like for instance on the front page. When building the manuscript, this will use a
styling and will exclude the title from for instance a Table of Contents in Libre Office. different styling and will exclude the title from for instance a Table of Contents in Libre
Office.
``##! Title Text`` ``##! Title Text``
This tells the build tool to not assign a chapter number to this chapter title if automatic This tells the build tool to not assign a chapter number to this chapter title if automatic
@@ -121,7 +121,7 @@ A minimal set of text emphasis styles are supported.
Strikethrough text. Strikethrough text.
In markdown guides it is often recommended to differentiate between strong importance and emphasis In markdown guides it is often recommended to differentiate between strong importance and emphasis
by using ``**`` for strong and ``_`` for emphasis, although markdown generally also supports ``__`` by using ``**`` for strong and ``_`` for emphasis, although Markdown generally also supports ``__``
for strong and ``*`` for emphasis. However, since the differentiation makes the highlighting and for strong and ``*`` for emphasis. However, since the differentiation makes the highlighting and
conversion significantly simpler and faster, in novelWriter this is a rule, not just a conversion significantly simpler and faster, in novelWriter this is a rule, not just a
recommendation. recommendation.
@@ -144,9 +144,9 @@ In addition, the following rules apply:
Comments and Synopsis Comments and Synopsis
===================== =====================
In addition to these standard markdown features, novelWriter also allows for comments in documents. In addition to these standard Markdown features, novelWriter also allows for comments in documents.
The text of a comment is ignored by the word counter. The text can also be filtered out when The text of a comment is ignored by the word counter. The text can also be filtered out when
exporting or viewing the document. building the manuscript or viewing the document.
If the first word of a comment is ``Synopsis:`` (with the colon included), the comment is treated If the first word of a comment is ``Synopsis:`` (with the colon included), the comment is treated
specially and will show up in the :ref:`a_ui_outline` in a dedicated column. The word ``synopsis`` specially and will show up in the :ref:`a_ui_outline` in a dedicated column. The word ``synopsis``
@@ -154,13 +154,13 @@ is not case sensitive. If it is correctly formatted, the syntax highlighter will
altering the colour of the word. altering the colour of the word.
``% text...`` ``% text...``
This is a comment. The text is not exported by default (this can be overridden), seen in the This is a comment. The text is not renderred by default (this can be overridden), seen in the
document viewer, or counted towards word counts. document viewer, or counted towards word counts.
``% Synopsis: text...`` ``% Synopsis: text...``
This is a synopsis comment. It is generally treated in the same way as a regular comment, except This is a synopsis comment. It is generally treated in the same way as a regular comment, except
that it is also captured by the indexing algorithm and displayed in the :ref:`a_ui_outline`. It that it is also captured by the indexing algorithm and displayed in the :ref:`a_ui_outline`. It
can also be filtered separately when exporting the project to for instance generate an outline can also be filtered separately when building the project to for instance generate an outline
document of the whole project. document of the whole project.
.. note:: .. note::
@@ -174,8 +174,9 @@ Tags and References
=================== ===================
The document editor supports a minimal set of keywords used for setting tags, and making references The document editor supports a minimal set of keywords used for setting tags, and making references
between documents. The tags and references can be set once per section defined by a heading. Using between documents. The tag can be set once per section defined by a heading. Setting it multiple
them multiple times under the same heading will just override the previous setting. times under the same heading will just override the previous setting. References can be set
anywhere within a section, and are collected according to their category.
``@keyword: value`` ``@keyword: value``
A keyword argument followed by a value, or a comma separated list of values. A keyword argument followed by a value, or a comma separated list of values.
@@ -226,7 +227,7 @@ Vertical Space and Page Breaks
============================== ==============================
Adding more than one line break between paragraphs will *not* increase the space between those Adding more than one line break between paragraphs will *not* increase the space between those
paragraphs when exporting the project. To add additional space between paragraphs, add the text paragraphs when building the project. To add additional space between paragraphs, add the text
``[VSPACE]`` on a line of its own, and the build tool will insert a blank paragraph in its place. ``[VSPACE]`` on a line of its own, and the build tool will insert a blank paragraph in its place.
If you need multiple blank paragraphs just add a colon and a number to the above code. For If you need multiple blank paragraphs just add a colon and a number to the above code. For
@@ -242,6 +243,6 @@ Page breaks are automatically added to partition, chapter and unnumbered chapter
documents. If you want such breaks for scenes and sections, you must add them manually. documents. If you want such breaks for scenes and sections, you must add them manually.
.. note:: .. note::
The page break code is applied to the text that follows. It adds a "page break before" mark to The page break code is applied to the text that follows it. It adds a "page break before" mark
the text when exporting to HTML or Open Document. This means that a ``[NEW PAGE]`` which has no to the text when exporting to HTML or Open Document. This means that a ``[NEW PAGE]`` which has
text following it will not result in a page break. no text following it, it will not result in a page break.
+62 -45
View File
@@ -22,7 +22,8 @@ the project, and has four columns:
The first column shows the icon and label of each folder, document, or note in your project. The The first column shows the icon and label of each folder, document, or note in your project. The
label is not the same as the title you set inside the document. However, the document's label label is not the same as the title you set inside the document. However, the document's label
will appear in the header above the document text itself so you know where in the project an will appear in the header above the document text itself so you know where in the project an
open document belongs. open document belongs. The icon is selected based on the type of item, and for novel documents,
the level of the first header in the document text.
**Column 2** **Column 2**
The second column shows the word count of the document, or the sum of words of the child items The second column shows the word count of the document, or the sum of words of the child items
@@ -30,20 +31,34 @@ the project, and has four columns:
from the :guilabel:`Tools` menu, or by pressing :kbd:`F9`. from the :guilabel:`Tools` menu, or by pressing :kbd:`F9`.
**Column 3** **Column 3**
The third column indicates whether the document is included in the final project build or not. The third column indicates whether the document is considered active or inactive in the project.
You may want to filter out documents that you no longer want to keep in the final manuscript, You can use this flag to indicate that a document is still in the project, but should not be
but want to keep in the project tree for reference. considered an active part of it. When you run the :guilabel:`Build Novel Project` tool, you can
filter based on this flag. You can change this value from the context menu.
**Column 4** **Column 4**
The fourth column shows the user-defined status or importance labels you've assigned to each The fourth column shows the user-defined status or importance labels you've assigned to each
project item. See :ref:`a_ui_tree_status` for more details. project item. See :ref:`a_ui_tree_status` for more details. You can change these labels from the
context menu.
Right-clicking an item in the project tree will open a context menu under the cursor, displaying Right-clicking an item in the project tree will open a context menu under the cursor, displaying
a selection of actions that can be performed on the selected item. a selection of actions that can be performed on the selected item.
The label, status or importance setting, the layout, and the include flag can all be edited using At the top of the tree, you will find a set of buttons.
the :guilabel:`Item Settings` dialog box. The dialog can be opened from the :guilabel:`Project`
menu, or by pressing :kbd:`F2` with the item selected. * The first button is a quick links button that will show you a dropdown menu of all the root
folders in your project. Selecting one will scroll to that position in the tree. You can also
activate this menu by pressing :kbd:`Ctrl`:kbd:`L`.
* The next buttons can be used to move items up and down in the project tree. This is the only way
to move root folders.
* The next button opens a dropdown menu for adding new items to the tree. This includes root
folders. You can also activate this dropdown menu by pressing :kbd:`Ctrl`:kbd:`N`.
* The last button is a menu of further actions on the entire project tree.
.. tip::
Under the :guilabel:`Transform` submenu in the context menu of an item, you will find several
options on how to change a document or folder. This includes changing between document and note,
splitting them into multiple documents, or merging child items into a single document.
Below the project tree you will find a small details panel showing the full information of the Below the project tree you will find a small details panel showing the full information of the
currently selected item. This panel also includes the latest paragraph and character counts in currently selected item. This panel also includes the latest paragraph and character counts in
@@ -56,13 +71,15 @@ The Novel Tree
-------------- --------------
An alternative way to view the project structure is the novel tree. You can switch to this view by An alternative way to view the project structure is the novel tree. You can switch to this view by
selecting the :guilabel:`Novel` tab under the project tree. This view is a simplified version of selecting the :guilabel:`Novel Tree View` button in the sidebar. This view is a simplified version
the view in the :guilabel:`Outline`. It is convenient when you want to browse the structure of the of the view in the :guilabel:`Outline`. It is convenient when you want to browse the structure of
story itself rather than the document files. the story itself rather than the document files.
.. note:: .. note::
You cannot reorganise the entries in the novel tree, or add any new ones, as that would imply You cannot reorganise the entries in the novel tree, or add any new documents, as that would
restructuring the content of the document files. Any editing must be done in the project tree. imply restructuring the content of the document files. Any editing must be done in the project
tree. However, you can add new headings to existing documents, which will be updated in this
view.
.. _a_ui_tree_status: .. _a_ui_tree_status:
@@ -96,19 +113,15 @@ Drag & drop has only limited support for moving documents. In general, bulk acti
allowed. This is deliberate to avoid accidentally messing up your project. If you make a mistake, allowed. This is deliberate to avoid accidentally messing up your project. If you make a mistake,
the last move action can be undone by pressing :kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`. the last move action can be undone by pressing :kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`.
Documents and their folders can be rearranged freely within their root folders. Novel documents Documents and their folders can be rearranged freely within their root folders. If you move a Novel
cannot be moved out of the :guilabel:`Novel` folder, except to :guilabel:`Trash` and the documents out of a Novel folder, it will be converted to a project note. Notes can be moved freely
:guilabel:`Archive` folders. Notes can be moved freely between all root folders, but keep in mind between all root folders, but keep in mind that if you move a note into a :guilabel:`Novel`, its
that if you move a note into a :guilabel:`Novel`, its "Importance" setting will be reset to the "Importance" setting will be switched with a "Status" setting. See :ref:`a_ui_tree_status`. The old
default "Status" setting. See :ref:`a_ui_tree_status`. value will not be overwritten though, and should be restored if you move it back.
Folders cannot be moved at all outside their root tree. Neither can a folder containing documents
be deleted. You must first delete the containing documents.
Root folders in the project tree cannot be dragged & dropped at all. If you want to reorder them, Root folders in the project tree cannot be dragged & dropped at all. If you want to reorder them,
you can move them up or down with respect to eachother from the :guilabel:`Project` menu, the you can move them up or down with respect to eachother from the arrow buttons at the top of the
right-click context menu, or by pressing :kbd:`Ctrl`:kbd:`Shift` and the :kbd:`Up` or :kbd:`Down` project tree, or by pressing :kbd:`Ctrl`:kbd:`Shift` and the :kbd:`Up` or :kbd:`Down` key.
key.
.. _a_ui_edit: .. _a_ui_edit:
@@ -118,19 +131,23 @@ Editing and Viewing Documents
To edit a document, double-click it in the project tree, or press the :kbd:`Return` key while To edit a document, double-click it in the project tree, or press the :kbd:`Return` key while
having it selected. This will open the document in the document editor. The editor uses a having it selected. This will open the document in the document editor. The editor uses a
markdown-like syntax for some features, and a novelWriter-specific syntax for others. The syntax Markdown-like syntax for some features, and a novelWriter-specific syntax for others. The syntax
format is described in the :ref:`a_fmt` section. The editor has a maximise button (toggles the format is described in the :ref:`a_fmt` section.
:guilabel:`Focus Mode`) and a close button in the topright corner. On the topleft side you will
find an edit button that opens the :guilabel:`Item Settings` dialog for the currently open The editor has a maximise button (toggles the :guilabel:`Focus Mode`) and a close button in the
document, and a search button to open the search dialog. topright corner. On the topleft side you will find an edit button that opens the
:guilabel:`Item Label` dialog for the currently open document, and a search button to open the
search dialog.
Any document in the project tree can also be viewed in parallel in a right hand side document Any document in the project tree can also be viewed in parallel in a right hand side document
viewer. To view a document, press :kbd:`Ctrl`:kbd:`R`, or select :guilabel:`View Document` in the viewer. To view a document, press :kbd:`Ctrl`:kbd:`R`, or select :guilabel:`View Document` in the
menu. If you have a middle mouse button, middle-clicking on the document will also open it in the menu or context menu. If you have a middle mouse button, middle-clicking on the document will also
viewer. The document viewed does not have to be the same document as currently being edited. open it in the viewer.
However, If you *are* viewing the same document, pressing :kbd:`Ctrl`:kbd:`R` again will update the
document with your latest changes. You can also press the reload button in the topright corner of The document viewed does not have to be the same document as currently being edited. However, If
the view panel, next to the close button, to achieve the same thing. you *are* viewing the same document, pressing :kbd:`Ctrl`:kbd:`R` again will update the document
with your latest changes. You can also press the reload button in the topright corner of the view
panel, next to the close button, to achieve the same thing.
Both the document editor and viewer will show the label of the document in the header at the top of Both the document editor and viewer will show the label of the document in the header at the top of
the edit or view panel. Optionally, the full project path to the document can be shown. This can be the edit or view panel. Optionally, the full project path to the document can be shown. This can be
@@ -218,24 +235,26 @@ tricky for languages that use the same symbol for these, like English does.
Project Outline View Project Outline View
==================== ====================
The project's Outline view is available as the second tab on the right hand side of the main window The project's Outline view is available as another view option from the views bar. The outline
labelled :guilabel:`Outline`. The outline provides an overview of the novel structure, displaying a provides an overview of the novel structure, displaying a tree hierarchy of the elements of the
tree hierarchy of the elements of the novel, that is, the level 1 to 4 headings representing novel, that is, the level 1 to 4 headings representing partitions, chapters, scenes and sections.
partitions, chapters, scenes and sections.
The document containing the heading can also be displayed as a separate column, as well as the line The document containing the heading can also be displayed as a separate column, as well as the line
number where it occurs. Double-clicking an entry will open the corresponding document in the number where it occurs. Double-clicking an entry will open the corresponding document in the
editor. editor.
You can select which novel folder to display from the dropdown menu. You can optionally also choose
to show a combination of all novel folders.
.. note:: .. note::
Since the internal structure of the novel does not depend directly on the folder and document Since the internal structure of the novel does not depend directly on the folder and document
structure of the project tree, these will not necessarily look the same, depending on how you structure of the project tree, these will not necessarily look the same, depending on how you
choose to organise your documents. See the :ref:`a_struct` page for more details. choose to organise your documents. See the :ref:`a_struct` page for more details.
Various meta data and information extracted from tags can be displayed in columns in the outline. Various meta data and information extracted from tags can be displayed in columns in the outline.
A default set of such columns is visible, but you can turn on or off more columns by right clicking A default set of such columns is visible, but you can turn on or off more columns from the menu
the header and selecting the columns you want to show. The order of the columns can also be button in the toolbar. The order of the columns can also be rearranged by dragging them to a
rearranged by dragging them to a different position. different position.
.. note:: .. note::
The :guilabel:`Title` column cannot be disabled or moved. The :guilabel:`Title` column cannot be disabled or moved.
@@ -244,10 +263,8 @@ The information viewed in the outline is based on the project's main index. Whil
its best to keep the index up to date when contents change, you can always rebuild it manually by its best to keep the index up to date when contents change, you can always rebuild it manually by
pressing :kbd:`F9` if something isn't right. pressing :kbd:`F9` if something isn't right.
The outline view itself can be regenerated by pressing :kbd:`F10`. You can also enable automatic The outline view itself can be regenerated by pressing the refresh button. By default, the content
updating in the :guilabel:`Tools` menu, which will trigger an update whenever the index is updated is refreshed each time you switch to this view.
and the :guilabel:`Outline` tab is active. You may want to disable this feature if your project is
very large,
The :guilabel:`Synopsis` column of the outline view takes its information from a specially The :guilabel:`Synopsis` column of the outline view takes its information from a specially
formatted comment. See :ref:`a_fmt_comm`. formatted comment. See :ref:`a_fmt_comm`.
+33 -12
View File
@@ -4,13 +4,20 @@
Project Format Changes Project Format Changes
********************** **********************
.. _File Format Spec 1.5: _static/fileformatspec15.pdf
.. _documentation: https://novelwriter.readthedocs.io
Most of the changes to the file formats over the history of novelWriter have no impact on the Most of the changes to the file formats over the history of novelWriter have no impact on the
user-side of things. The project files are generally updated automatically. However, some of the user-side of things. The project files are generally updated automatically. However, some of the
changes require minor actions from the user. changes require minor actions from the user.
The key changes in the formats are listed below, as well as the user actions required where The key changes in the formats are listed below, as well as the user actions required, where
applicable. applicable.
.. only:: not html
A full project file format specification is available in the online documentation_.
.. caution:: .. caution::
When you update a project from one format version to the next, the project can no longer be When you update a project from one format version to the next, the project can no longer be
@@ -18,13 +25,37 @@ applicable.
introduced. You will get a notification about any updates to your project file format and will introduced. You will get a notification about any updates to your project file format and will
have the option to decline the upgrade. have the option to decline the upgrade.
.. only:: html
**For Developers**
A full description of the current file format is available in the `File Format Spec 1.5`_
document, available as a PDF. This document is intended for contributors to novelWriter, those
building project conversion tools, either to or from tne novelWriter format, and for those who
wish to make their own templating system.
.. _a_prjfmt_1_5:
Format 1.5 Changes
==================
This project format was introduced in novelWriter version 2.0 RC 2.
This is a modification of the 1.4 format. It makes the XML more consistent in that meta data have
been moved to the section nodes, and key/value settings now have a consistent format. Logical flags
are saved as yes/no instead of Python True/False, and the main heading of the document is now saved
to the item rather than in the index.
.. _a_prjfmt_1_4: .. _a_prjfmt_1_4:
Format 1.4 Changes Format 1.4 Changes
================== ==================
This project format was introduced in novelWriter version 1.7. This project format was introduced in novelWriter version 2.0 RC 1. Since this was a release
candidate, it is unlikely that your project uses it, but it may be the case if you've installed a
pre-release.
This format changes the way project items (folders, documents and notes) are stored. It is a more This format changes the way project items (folders, documents and notes) are stored. It is a more
compact format that is simpler and faster to parse, and easier to extend. The conversion is done compact format that is simpler and faster to parse, and easier to extend. The conversion is done
@@ -77,10 +108,6 @@ Format 1.2 Changes
This project format was introduced in novelWriter version 0.10. This project format was introduced in novelWriter version 0.10.
With this format, the way auto-replace entries were stored in the main project XML file changed. With this format, the way auto-replace entries were stored in the main project XML file changed.
Opening an old project automatically converts the storage format up to and including version 1.1.1.
Format 1.2 projects can be opened without loss of information up until version 1.1.1, and if the
auto-replace is not being used, can still be opened in novelWriter as of version |release|.
.. _a_prjfmt_1_1: .. _a_prjfmt_1_1:
@@ -96,9 +123,6 @@ novelWriter documents were saved in a series of folders numbered from ``data_0``
It also reduces the number of meta data and cache files. These files are automatically deleted if It also reduces the number of meta data and cache files. These files are automatically deleted if
an old project is opened. This was also when the Table of Contents file was introduced. an old project is opened. This was also when the Table of Contents file was introduced.
Format 1.1 projects can be opened without loss of information up until version 1.1.1, and if the
auto-replace is not being used, can still be opened in novelWriter as of version |release|.
.. _a_prjfmt_1_0: .. _a_prjfmt_1_0:
@@ -106,6 +130,3 @@ Format 1.0 Changes
================== ==================
This is the original file format and project structure. It was in use up to version 0.6.3. This is the original file format and project structure. It was in use up to version 0.6.3.
Format 1.0 projects can be opened without loss of information up until version 1.1.1, and if the
auto-replace is not being used, can still be opened in novelWriter as of version |release|.
+298 -298
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -154,11 +154,11 @@ def isHandle(value):
def isTitleTag(value): def isTitleTag(value):
"""Check if a string is a valid title string. """Check if a string is a valid title tag string.
""" """
if not isinstance(value, str): if not isinstance(value, str):
return False return False
if len(value) != 7: if len(value) != 5:
return False return False
if not value.startswith("T"): if not value.startswith("T"):
return False return False
-1
View File
@@ -61,7 +61,6 @@ class nwHeaders:
H_VALID = ("H0", "H1", "H2", "H3", "H4") H_VALID = ("H0", "H1", "H2", "H3", "H4")
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4} H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
TT_NONE = "T000000"
# END Class nwHeaders # END Class nwHeaders
+123 -115
View File
@@ -41,6 +41,8 @@ from novelwriter.common import (
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
TT_NONE = "T0000"
class NWIndex: class NWIndex:
"""This class holds the entire index for a given project. The index """This class holds the entire index for a given project. The index
@@ -102,7 +104,7 @@ class NWIndex:
""" """
self.clearIndex() self.clearIndex()
for nwItem in self._project.tree: for nwItem in self._project.tree:
if nwItem is not None and nwItem.isFileType(): if nwItem.isFileType():
tHandle = nwItem.itemHandle tHandle = nwItem.itemHandle
theDoc = self._project.storage.getDocument(tHandle) theDoc = self._project.storage.getDocument(tHandle)
self.scanText(tHandle, theDoc.readDocument() or "") self.scanText(tHandle, theDoc.readDocument() or "")
@@ -281,50 +283,58 @@ class NWIndex:
def _scanActive(self, tHandle, theItem, theText, itemTags): def _scanActive(self, tHandle, theItem, theText, itemTags):
"""Scan an active document for meta data. """Scan an active document for meta data.
""" """
nTitle = 0 nTitle = 0 # Line Number of the previous title
findHeader = True cTitle = TT_NONE # Tag of the current title
theLines = theText.splitlines() pTitle = TT_NONE # Tag of the previous title
firstHeader = True # First header has been seen
theLines = theText.splitlines()
for nLine, aLine in enumerate(theLines, start=1): for nLine, aLine in enumerate(theLines, start=1):
if len(aLine.strip()) == 0: if aLine.strip() == "":
continue continue
if aLine.startswith("#"): if aLine.startswith("#"):
if findHeader: hDepth, hText = self._splitHeading(aLine)
hDepth, _ = self._splitHeading(aLine) if hDepth == "H0":
if hDepth != "H0": continue
theItem.setMainHeading(hDepth)
findHeader = False
isTitle = self._indexTitle(tHandle, aLine, nLine) if firstHeader:
if isTitle and nLine > 0: theItem.setMainHeading(hDepth)
firstHeader = False
cTitle = self._itemIndex.addItemHeading(tHandle, nLine, hDepth, hText)
if cTitle != TT_NONE:
if nTitle > 0: if nTitle > 0:
# We have a new title, so we need to count the words of the previous one
lastText = "\n".join(theLines[nTitle-1:nLine-1]) lastText = "\n".join(theLines[nTitle-1:nLine-1])
self._indexWordCounts(tHandle, lastText, nTitle) self._indexWordCounts(tHandle, lastText, pTitle)
nTitle = nLine nTitle = nLine
pTitle = cTitle
elif aLine.startswith("@"): elif aLine.startswith("@"):
self._indexKeyword(tHandle, aLine, nTitle, theItem.itemClass, itemTags) if cTitle != TT_NONE:
self._indexKeyword(tHandle, aLine, cTitle, theItem.itemClass, itemTags)
elif aLine.startswith("%"): elif aLine.startswith("%"):
if nTitle > 0: if cTitle != TT_NONE:
toCheck = aLine[1:].lstrip() toCheck = aLine[1:].lstrip()
synTag = toCheck[:9].lower() synTag = toCheck[:9].lower()
tLen = len(aLine) tLen = len(aLine)
cLen = len(toCheck) cLen = len(toCheck)
cOff = tLen - cLen cOff = tLen - cLen
if synTag == "synopsis:": if synTag == "synopsis:":
self._indexSynopsis(tHandle, aLine[cOff+9:].strip(), nTitle) sText = aLine[cOff+9:].strip()
self._itemIndex.setHeadingSynopsis(tHandle, cTitle, sText)
# Count words for remaining text after last heading # Count words for remaining text after last heading
if nTitle > 0: if pTitle != TT_NONE:
lastText = "\n".join(theLines[nTitle-1:]) lastText = "\n".join(theLines[nTitle-1:])
self._indexWordCounts(tHandle, lastText, nTitle) self._indexWordCounts(tHandle, lastText, pTitle)
# Also count words on a page with no titles # Also count words on a page with no titles
if nTitle == 0: if cTitle == TT_NONE:
self._indexWordCounts(tHandle, theText, nTitle) self._indexWordCounts(tHandle, theText, cTitle)
# Prune no longer used tags # Prune no longer used tags
for tTag, isActive in itemTags.items(): for tTag, isActive in itemTags.items():
@@ -362,34 +372,14 @@ class NWIndex:
return "H2", aLine[4:].strip() return "H2", aLine[4:].strip()
return "H0", "" return "H0", ""
def _indexTitle(self, tHandle, aLine, nTitle): def _indexWordCounts(self, tHandle, theText, sTitle):
"""Save information about the title and its location in the
file to the index.
"""
hDepth, hText = self._splitHeading(aLine)
if hDepth == "H0":
return False
sTitle = f"T{nTitle:06d}"
self._itemIndex.addItemHeading(tHandle, sTitle, hDepth, hText)
return True
def _indexWordCounts(self, tHandle, theText, nTitle):
"""Count text stats and save the counts to the index. """Count text stats and save the counts to the index.
""" """
sTitle = f"T{nTitle:06d}"
cC, wC, pC = countWords(theText) cC, wC, pC = countWords(theText)
self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC) self._itemIndex.setHeadingCounts(tHandle, sTitle, cC, wC, pC)
return return
def _indexSynopsis(self, tHandle, theText, nTitle): def _indexKeyword(self, tHandle, aLine, sTitle, itemClass, itemTags):
"""Save the synopsis to the index.
"""
sTitle = f"T{nTitle:06d}"
self._itemIndex.setHeadingSynopsis(tHandle, sTitle, theText)
return
def _indexKeyword(self, tHandle, aLine, nTitle, itemClass, itemTags):
"""Validate and save the information about a reference to a tag """Validate and save the information about a reference to a tag
in another file, or the setting of a tag in the file. A record in another file, or the setting of a tag in the file. A record
of active tags is updated so that no longer used tags can be of active tags is updated so that no longer used tags can be
@@ -404,7 +394,6 @@ class NWIndex:
logger.warning("Skipping invalid keyword '%s' in '%s'", theBits[0], tHandle) logger.warning("Skipping invalid keyword '%s' in '%s'", theBits[0], tHandle)
return return
sTitle = f"T{nTitle:06d}"
if theBits[0] == nwKeyWords.TAG_KEY: if theBits[0] == nwKeyWords.TAG_KEY:
tagName = theBits[1] tagName = theBits[1]
self._tagsIndex.add(tagName, tHandle, sTitle, itemClass) self._tagsIndex.add(tagName, tHandle, sTitle, itemClass)
@@ -491,6 +480,19 @@ class NWIndex:
# Extract Data # Extract Data
## ##
def getItemData(self, tHandle):
"""Get the index data for a given item.
"""
return self._itemIndex[tHandle]
def getItemHeader(self, tHandle, sTitle):
"""Get the header entry for a specific item and heading.
"""
tItem = self._itemIndex[tHandle]
if isinstance(tItem, IndexItem):
return tItem[sTitle]
return None
def novelStructure(self, rootHandle=None, skipExcl=True): def novelStructure(self, rootHandle=None, skipExcl=True):
"""Iterate over all titles in the novel, in the correct order as """Iterate over all titles in the novel, in the correct order as
they appear in the tree view and in the respective document they appear in the tree view and in the respective document
@@ -518,21 +520,13 @@ class NWIndex:
hCount[iLevel] += 1 hCount[iLevel] += 1
return hCount return hCount
def getHandleWordCounts(self, tHandle): def getHandleHeaderCount(self, tHandle):
"""Get all header word counts for a specific handle. """Get the number of headers in an item.
""" """
return [ tItem = self._itemIndex[tHandle]
(f"{tHandle}:{sTitle}", hItem.wordCount) if isinstance(tItem, IndexItem):
for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle) return len(tItem)
] return 0
def getHandleHeaders(self, tHandle):
"""Get all headers for a specific handle.
"""
return [
(sTitle, hItem.level, hItem.title)
for sTitle, hItem in self._itemIndex.iterItemHeaders(tHandle)
]
def getTableOfContents(self, rootHandle, maxDepth, skipExcl=True): def getTableOfContents(self, rootHandle, maxDepth, skipExcl=True):
"""Generate a table of contents up to a maximum depth. """Generate a table of contents up to a maximum depth.
@@ -598,13 +592,6 @@ class NWIndex:
return theRefs return theRefs
def getNovelData(self, tHandle, sTitle):
"""Return the novel data of a given handle and title.
"""
if tHandle in self._itemIndex:
return self._itemIndex[tHandle][sTitle]
return None
def getBackReferenceList(self, tHandle): def getBackReferenceList(self, tHandle):
"""Build a list of files referring back to our file, specified """Build a list of files referring back to our file, specified
by tHandle. by tHandle.
@@ -644,10 +631,22 @@ class TagsIndex:
control of the keys. control of the keys.
""" """
__slots__ = ("_tags")
def __init__(self): def __init__(self):
self._tags = {} self._tags = {}
return return
def __contains__(self, tagKey):
return tagKey in self._tags
def __delitem__(self, tagKey):
self._tags.pop(tagKey, None)
return
def __getitem__(self, tagKey):
return self._tags.get(tagKey, None)
## ##
# Methods # Methods
## ##
@@ -658,22 +657,6 @@ class TagsIndex:
self._tags = {} self._tags = {}
return return
def __contains__(self, tagKey):
"""Check if a tag exists in the index,
"""
return tagKey in self._tags
def __delitem__(self, tagKey):
"""Delete an entry in the index.
"""
self._tags.pop(tagKey, None)
return
def __getitem__(self, tagKey):
"""Return a tag, or return None if it isn't found.
"""
return self._tags.get(tagKey, None)
def add(self, tagKey, tHandle, sTitle, itemClass): def add(self, tagKey, tHandle, sTitle, itemClass):
"""Add a key to the index and set all values. """Add a key to the index and set all values.
""" """
@@ -690,7 +673,7 @@ class TagsIndex:
def tagHeading(self, tagKey): def tagHeading(self, tagKey):
"""Get the heading of a given tag. """Get the heading of a given tag.
""" """
return self._tags.get(tagKey, {}).get("heading", nwHeaders.TT_NONE) return self._tags.get(tagKey, {}).get("heading", TT_NONE)
def tagClass(self, tagKey): def tagClass(self, tagKey):
"""Get the class of a given tag. """Get the class of a given tag.
@@ -749,11 +732,23 @@ class ItemIndex:
IndexHeading object for each header of the text. IndexHeading object for each header of the text.
""" """
__slots__ = ("_project", "_items")
def __init__(self, project): def __init__(self, project):
self._project = project self._project = project
self._items = {} self._items = {}
return return
def __contains__(self, tHandle):
return tHandle in self._items
def __delitem__(self, tHandle):
self._items.pop(tHandle, None)
return
def __getitem__(self, tHandle):
return self._items.get(tHandle, None)
## ##
# Methods # Methods
## ##
@@ -764,22 +759,6 @@ class ItemIndex:
self._items = {} self._items = {}
return return
def __contains__(self, tHandle):
"""Check if an item exists in the index,
"""
return tHandle in self._items
def __delitem__(self, tHandle):
"""Delete an entry in the index.
"""
self._items.pop(tHandle, None)
return
def __getitem__(self, tHandle):
"""Return an item, or return None if it isn't found.
"""
return self._items.get(tHandle, None)
def add(self, tHandle, tItem): def add(self, tHandle, tItem):
"""Add a new item to the index. This will overwrite the item if """Add a new item to the index. This will overwrite the item if
it already exists. it already exists.
@@ -815,8 +794,6 @@ class ItemIndex:
a given root handle, or for all if root handle is None. a given root handle, or for all if root handle is None.
""" """
for tItem in self._project.tree: for tItem in self._project.tree:
if tItem is None:
continue
if tItem.isNoteLayout(): if tItem.isNoteLayout():
continue continue
if skipExcl and not tItem.isActive: if skipExcl and not tItem.isActive:
@@ -839,13 +816,15 @@ class ItemIndex:
# Setters # Setters
## ##
def addItemHeading(self, tHandle, sTitle, hDepth, hText): def addItemHeading(self, tHandle, lineNo, hDepth, hText):
"""Set the main heading level of an item. """Add a heading to an item.
""" """
if tHandle in self._items: if tHandle in self._items:
tItem = self._items[tHandle] tItem = self._items[tHandle]
tItem.addHeading(IndexHeading(sTitle, hDepth, hText)) sTitle = tItem.nextHeading()
return tItem.addHeading(IndexHeading(sTitle, lineNo, hDepth, hText))
return sTitle
return TT_NONE
def setHeadingCounts(self, tHandle, sTitle, cC, wC, pC): def setHeadingCounts(self, tHandle, sTitle, cC, wC, pC):
"""Set the character, word and paragraph counts of a heading """Set the character, word and paragraph counts of a heading
@@ -916,20 +895,31 @@ class IndexItem:
must be reset each time the item is re-indexed. must be reset each time the item is re-indexed.
""" """
__slots__ = ("_handle", "_item", "_headings", "_headings", "_count")
def __init__(self, tHandle, tItem): def __init__(self, tHandle, tItem):
self._handle = tHandle self._handle = tHandle
self._item = tItem self._item = tItem
self._headings = {} self._headings = {}
self._index = 0 self._count = 0
# Add a placeholder heading # Add a placeholder heading
self._headings[nwHeaders.TT_NONE] = IndexHeading(nwHeaders.TT_NONE) self._headings[TT_NONE] = IndexHeading(TT_NONE)
return return
def __repr__(self): def __repr__(self):
return f"<IndexItem handle='{self._handle}'>" return f"<IndexItem handle='{self._handle}'>"
def __len__(self):
return len(self._headings)
def __getitem__(self, sTitle):
return self._headings.get(sTitle, None)
def __contains__(self, sTitle):
return sTitle in self._headings
## ##
# Properties # Properties
## ##
@@ -946,8 +936,8 @@ class IndexItem:
"""Add a heading to the item. Also remove the placeholder entry """Add a heading to the item. Also remove the placeholder entry
if it exists. if it exists.
""" """
if nwHeaders.TT_NONE in self._headings: if TT_NONE in self._headings:
self._headings.pop(nwHeaders.TT_NONE) self._headings.pop(TT_NONE)
self._headings[tHeading.key] = tHeading self._headings[tHeading.key] = tHeading
return return
@@ -984,12 +974,6 @@ class IndexItem:
# Data Methods # Data Methods
## ##
def __getitem__(self, sTitle):
return self._headings.get(sTitle, None)
def __contains__(self, sTitle):
return sTitle in self._headings
def items(self): def items(self):
return self._headings.items() return self._headings.items()
@@ -1006,6 +990,12 @@ class IndexItem:
tags.append(tag) tags.append(tag)
return tags return tags
def nextHeading(self):
"""Return the next heading key to be used.
"""
self._count += 1
return f"T{self._count:04d}"
## ##
# Pack/Unpack # Pack/Unpack
## ##
@@ -1051,8 +1041,14 @@ class IndexHeading:
of all references made under each heading. of all references made under each heading.
""" """
def __init__(self, key, level="H0", title=""): __slots__ = (
"_key", "_line", "_level", "_title", "_charCount", "_wordCount",
"_paraCount", "_synopsis", "_tag", "_refs",
)
def __init__(self, key, line=0, level="H0", title=""):
self._key = key self._key = key
self._line = line
self._level = level self._level = level
self._title = title self._title = title
@@ -1077,6 +1073,10 @@ class IndexHeading:
def key(self): def key(self):
return self._key return self._key
@property
def line(self):
return self._line
@property @property
def level(self): def level(self):
return self._level return self._level
@@ -1120,6 +1120,12 @@ class IndexHeading:
self._level = level self._level = level
return return
def setLine(self, line):
"""Set the line number of a heading.
"""
self._line = max(0, checkInt(line, 0))
return
def setCounts(self, charCount, wordCount, paraCount): def setCounts(self, charCount, wordCount, paraCount):
"""Set the character, word and paragraph count. Make sure the """Set the character, word and paragraph count. Make sure the
value is an integer and is not smaller than 0. value is an integer and is not smaller than 0.
@@ -1161,6 +1167,7 @@ class IndexHeading:
return { return {
"level": self._level, "level": self._level,
"title": self._title, "title": self._title,
"line": self._line,
"tag": self._tag, "tag": self._tag,
"cCount": self._charCount, "cCount": self._charCount,
"wCount": self._wordCount, "wCount": self._wordCount,
@@ -1182,6 +1189,7 @@ class IndexHeading:
self.setLevel(data.get("level", "H0")) self.setLevel(data.get("level", "H0"))
self._title = str(data.get("title", "")) self._title = str(data.get("title", ""))
self._tag = str(data.get("tag", "")) self._tag = str(data.get("tag", ""))
self.setLine(data.get("line", 0))
self.setCounts( self.setCounts(
data.get("cCount", 0), data.get("cCount", 0),
data.get("wCount", 0), data.get("wCount", 0),
+16 -33
View File
@@ -50,7 +50,6 @@ class NWTree:
self._treeRoots = {} # The root items of the tree self._treeRoots = {} # The root items of the tree
self._trashRoot = None # The handle of the trash root folder self._trashRoot = None # The handle of the trash root folder
self._archRoot = None # The handle of the archive root folder self._archRoot = None # The handle of the archive root folder
self._theIndex = 0 # The current iterator index
self._treeChanged = False # True if tree structure has changed self._treeChanged = False # True if tree structure has changed
return return
@@ -62,12 +61,11 @@ class NWTree:
def clear(self): def clear(self):
"""Clear the item tree entirely. """Clear the item tree entirely.
""" """
self._projTree = {} self._projTree = {}
self._treeOrder = [] self._treeOrder = []
self._treeRoots = {} self._treeRoots = {}
self._trashRoot = None self._trashRoot = None
self._archRoot = None self._archRoot = None
self._theIndex = 0
self._treeChanged = False self._treeChanged = False
return return
@@ -278,7 +276,7 @@ class NWTree:
""" """
for tHandle in self._treeOrder: for tHandle in self._treeOrder:
nwItem = self.__getitem__(tHandle) nwItem = self.__getitem__(tHandle)
if nwItem is not None and nwItem.isRootType(): if isinstance(nwItem, NWItem) and nwItem.isRootType():
if itemClass is None or nwItem.itemClass == itemClass: if itemClass is None or nwItem.itemClass == itemClass:
yield tHandle, nwItem yield tHandle, nwItem
return return
@@ -365,22 +363,18 @@ class NWTree:
return True return True
## ##
# Meta Methods # Special Methods
## ##
def __len__(self): def __len__(self):
"""Return the length counter. Does not check that it is correct! """The number of items in the project.
""" """
return len(self._treeOrder) return len(self._treeOrder)
def __bool__(self): def __bool__(self):
"""Returns True if the tree has any entries. """True if there are any items in the project.
""" """
return len(self._treeOrder) > 0 return bool(self._treeOrder)
##
# Item Access Methods
##
def __getitem__(self, tHandle): def __getitem__(self, tHandle):
"""Return a project item based on its handle. Returns None if """Return a project item based on its handle. Returns None if
@@ -417,25 +411,14 @@ class NWTree:
""" """
return tHandle in self._treeOrder return tHandle in self._treeOrder
##
# Iterator Methods
##
def __iter__(self): def __iter__(self):
"""Initiates the iterator. """Iterate through project items.
""" """
self._theIndex = 0 for tHandle in self._treeOrder:
return self tItem = self._projTree.get(tHandle)
if isinstance(tItem, NWItem):
def __next__(self): yield tItem
"""Returns the item from the next entry in the _treeOrder list. return
"""
if self._theIndex < len(self._treeOrder):
theItem = self.__getitem__(self._treeOrder[self._theIndex])
self._theIndex += 1
return theItem
else:
raise StopIteration
## ##
# Internal Functions # Internal Functions
-9
View File
@@ -130,15 +130,6 @@ class nwAlert(Enum):
# END Enum nwAlert # END Enum nwAlert
class nwState(Enum):
NONE = 0
BAD = 1
GOOD = 2
# END Enum nwState
class nwView(Enum): class nwView(Enum):
EDITOR = 0 EDITOR = 0
+2 -2
View File
@@ -28,7 +28,7 @@ from novelwriter.gui.outline import GuiOutlineView
from novelwriter.gui.projtree import GuiProjectView from novelwriter.gui.projtree import GuiProjectView
from novelwriter.gui.statusbar import GuiMainStatus from novelwriter.gui.statusbar import GuiMainStatus
from novelwriter.gui.theme import GuiTheme from novelwriter.gui.theme import GuiTheme
from novelwriter.gui.viewsbar import GuiViewsBar from novelwriter.gui.sidebar import GuiSideBar
__all__ = [ __all__ = [
"GuiDocEditor", "GuiDocEditor",
@@ -41,5 +41,5 @@ __all__ = [
"GuiOutlineView", "GuiOutlineView",
"GuiProjectView", "GuiProjectView",
"GuiTheme", "GuiTheme",
"GuiViewsBar", "GuiSideBar",
] ]
+34 -34
View File
@@ -51,8 +51,8 @@ from PyQt5.QtWidgets import (
) )
from novelwriter.core import NWSpellEnchant, countWords from novelwriter.core import NWSpellEnchant, countWords
from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode, nwItemClass
from novelwriter.common import transferCase from novelwriter.common import minmax, transferCase
from novelwriter.constants import nwConst, nwFiles, nwKeyWords, nwUnicode from novelwriter.constants import nwConst, nwFiles, nwKeyWords, nwUnicode
from novelwriter.gui.dochighlight import GuiDocHighlighter from novelwriter.gui.dochighlight import GuiDocHighlighter
@@ -71,6 +71,8 @@ class GuiDocEditor(QTextEdit):
docEditedStatusChanged = pyqtSignal(bool) docEditedStatusChanged = pyqtSignal(bool)
docCountsChanged = pyqtSignal(str, int, int, int) docCountsChanged = pyqtSignal(str, int, int, int)
loadDocumentTagRequest = pyqtSignal(str, Enum) loadDocumentTagRequest = pyqtSignal(str, Enum)
novelStructureChanged = pyqtSignal()
novelItemMetaChanged = pyqtSignal(str)
def __init__(self, mainGui): def __init__(self, mainGui):
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
@@ -88,7 +90,6 @@ class GuiDocEditor(QTextEdit):
self._docChanged = False # Flag for changed status of document self._docChanged = False # Flag for changed status of document
self._docHandle = None # The handle of the open file self._docHandle = None # The handle of the open file
self._docHeaders = [] # Record of headers in the file
self._spellCheck = False # Flag for spell checking enabled self._spellCheck = False # Flag for spell checking enabled
self._nonWord = "\"'" # Characters to not include in spell checking self._nonWord = "\"'" # Characters to not include in spell checking
@@ -415,7 +416,7 @@ class GuiDocEditor(QTextEdit):
self._queuePos = self._nwItem.cursorPos self._queuePos = self._nwItem.cursorPos
else: else:
self.setCursorPosition(self._nwItem.cursorPos) self.setCursorPosition(self._nwItem.cursorPos)
else: elif isinstance(tLine, int):
self.setCursorLine(tLine) self.setCursorLine(tLine)
if self.mainConf.scrollPastEnd > 0: if self.mainConf.scrollPastEnd > 0:
@@ -425,7 +426,6 @@ class GuiDocEditor(QTextEdit):
self.document().rootFrame().setFrameFormat(docFrame) self.document().rootFrame().setFrameFormat(docFrame)
self.docFooter.updateLineCount() self.docFooter.updateLineCount()
self._docHeaders = self.theProject.index.getHandleHeaders(self._docHandle)
qApp.processEvents() qApp.processEvents()
self.document().clearUndoRedoStacks() self.document().clearUndoRedoStacks()
@@ -531,14 +531,16 @@ class GuiDocEditor(QTextEdit):
self.setDocumentChanged(False) self.setDocumentChanged(False)
oldHeader = self._nwItem.mainHeading oldHeader = self._nwItem.mainHeading
oldCount = self.theProject.index.getHandleHeaderCount(tHandle)
self.theProject.index.scanText(tHandle, docText) self.theProject.index.scanText(tHandle, docText)
newHeader = self._nwItem.mainHeading newHeader = self._nwItem.mainHeading
newCount = self.theProject.index.getHandleHeaderCount(tHandle)
# ToDo: This should be a signal if self._nwItem.itemClass == nwItemClass.NOVEL:
if self._updateHeaders(): if oldCount == newCount:
self.mainGui.requestNovelTreeRefresh() self.novelItemMetaChanged.emit(tHandle)
else: else:
self.mainGui.novelView.updateWordCounts(tHandle) self.novelStructureChanged.emit()
# ToDo: This should be a signal # ToDo: This should be a signal
if oldHeader != newHeader: if oldHeader != newHeader:
@@ -652,17 +654,30 @@ class GuiDocEditor(QTextEdit):
self.docEditedStatusChanged.emit(self._docChanged) self.docEditedStatusChanged.emit(self._docChanged)
return self._docChanged return self._docChanged
def setCursorPosition(self, thePosition): def setCursorPosition(self, position):
"""Move the cursor to a given position in the document. """Move the cursor to a given position in the document.
""" """
if not isinstance(thePosition, int): if not isinstance(position, int):
return False return False
nChars = self.document().characterCount() nChars = self.document().characterCount()
if nChars > 1: if nChars > 1:
theCursor = self.textCursor() theCursor = self.textCursor()
theCursor.setPosition(min(max(thePosition, 0), nChars-1)) theCursor.setPosition(minmax(position, 0, nChars-1))
self.setTextCursor(theCursor) self.setTextCursor(theCursor)
# By default, the editor scrolls so the cursor is on the
# last line, so we must correct it. The user setting for
# auto-scroll is used to determine the scroll distance. This
# makes it compatible with the typewriter scrolling feature
# when it is enabled. By default, it's 30% of viewport.
vPos = self.verticalScrollBar().value()
cPos = self.cursorRect().topLeft().y()
mPos = int(self.mainConf.autoScrollPos*0.01 * self.viewport().height())
if cPos > mPos:
# Only scroll if the cursor is past the auto-scroll limit
self.verticalScrollBar().setValue(max(0, vPos + cPos - mPos))
self.docFooter.updateLineCount() self.docFooter.updateLineCount()
return True return True
@@ -675,18 +690,18 @@ class GuiDocEditor(QTextEdit):
self._nwItem.setCursorPos(cursPos) self._nwItem.setCursorPos(cursPos)
return return
def setCursorLine(self, theLine): def setCursorLine(self, lineNo):
"""Move the cursor to a given line in the document. """Move the cursor to a given line in the document.
""" """
if not isinstance(theLine, int): if not isinstance(lineNo, int):
return False return False
if theLine >= 0: lineIdx = lineNo - 1 # Block index is 0 offset, lineNo is 1 offset
theBlock = self.document().findBlockByLineNumber(theLine) if lineIdx >= 0:
theBlock = self.document().findBlockByLineNumber(lineIdx)
if theBlock: if theBlock:
self.setCursorPosition(theBlock.position()) self.setCursorPosition(theBlock.position())
self.docFooter.updateLineCount() logger.debug("Cursor moved to line %d", lineNo)
logger.debug("Cursor moved to line %d", theLine)
return True return True
@@ -2065,21 +2080,6 @@ class GuiDocEditor(QTextEdit):
return False return False
return True return True
def _updateHeaders(self):
"""Update the headers record and return True if anything
changed, if a check flag was provided.
"""
if self._docHandle is None:
return False
newHeaders = self.theProject.index.getHandleHeaders(self._docHandle)
newLev = [x[1] for x in newHeaders]
oldLev = [x[1] for x in self._docHeaders]
self._docHeaders = newHeaders
return newLev != oldLev
def _checkDocSize(self, theSize): def _checkDocSize(self, theSize):
"""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.
+6 -33
View File
@@ -323,43 +323,10 @@ class GuiDocViewer(QTextBrowser):
return return
##
# Properties
##
def docHandle(self):
"""Return the handle of the currently open document. Returns
None if no document is open.
"""
return self._docHandle
## ##
# Setters # Setters
## ##
def setCursorPosition(self, thePosition):
"""Move the cursor to a given position in the document.
"""
if not isinstance(thePosition, int):
return False
if thePosition >= 0:
theCursor = self.textCursor()
theCursor.setPosition(thePosition)
self.setTextCursor(theCursor)
return True
def setCursorLine(self, theLine):
"""Move the cursor to a given line in the document.
"""
if not isinstance(theLine, int):
return False
if theLine >= 0:
theBlock = self.document().findBlockByLineNumber(theLine)
if theBlock:
self.setCursorPosition(theBlock.position())
logger.debug("Cursor moved to line %d", theLine)
return True
def setScrollPosition(self, thePos): def setScrollPosition(self, thePos):
"""Set the scrollbar position. """Set the scrollbar position.
""" """
@@ -372,6 +339,12 @@ class GuiDocViewer(QTextBrowser):
# Getters # Getters
## ##
def docHandle(self):
"""Return the handle of the currently open document. Returns
None if no document is open.
"""
return self._docHandle
def getScrollPosition(self): def getScrollPosition(self):
"""Get the scrollbar position. Returns 0 if no scrollbar. """Get the scrollbar position. Returns 0 if no scrollbar.
""" """
+93 -48
View File
@@ -40,7 +40,6 @@ from PyQt5.QtWidgets import (
) )
from novelwriter.enum import nwDocMode, nwItemClass, nwOutline from novelwriter.enum import nwDocMode, nwItemClass, nwOutline
from novelwriter.common import checkInt
from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -60,7 +59,7 @@ class GuiNovelView(QWidget):
# Signals for user interaction with the novel tree # Signals for user interaction with the novel tree
selectedItemChanged = pyqtSignal(str) selectedItemChanged = pyqtSignal(str)
openDocumentRequest = pyqtSignal(str, Enum, int, str) openDocumentRequest = pyqtSignal(str, Enum, str, bool)
def __init__(self, mainGui): def __init__(self, mainGui):
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
@@ -83,7 +82,6 @@ class GuiNovelView(QWidget):
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
# Function Mappings # Function Mappings
self.updateWordCounts = self.novelTree.updateWordCounts
self.getSelectedHandle = self.novelTree.getSelectedHandle self.getSelectedHandle = self.novelTree.getSelectedHandle
self.setActiveHandle = self.novelTree.setActiveHandle self.setActiveHandle = self.novelTree.setActiveHandle
@@ -107,12 +105,6 @@ class GuiNovelView(QWidget):
self.novelTree.initSettings() self.novelTree.initSettings()
return return
def refreshTree(self):
"""Refresh the current tree.
"""
self.novelTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("novelTree"))
return
def clearProject(self): def clearProject(self):
"""Clear project-related GUI content. """Clear project-related GUI content.
""" """
@@ -164,6 +156,13 @@ class GuiNovelView(QWidget):
# Public Slots # Public Slots
## ##
@pyqtSlot()
def refreshTree(self):
"""Refresh the current tree.
"""
self.novelTree.refreshTree(rootHandle=self.theProject.data.getLastHandle("novelTree"))
return
@pyqtSlot(str) @pyqtSlot(str)
def updateRootItem(self, tHandle): def updateRootItem(self, tHandle):
"""If any root item changes, rebuild the novel root menu. """If any root item changes, rebuild the novel root menu.
@@ -171,6 +170,14 @@ class GuiNovelView(QWidget):
self.novelBar.buildNovelRootMenu() self.novelBar.buildNovelRootMenu()
return return
@pyqtSlot(str)
def updateNovelItemMeta(self, tHandle):
"""The meta data of a novel item has changed, and the tree item
needs to be refreshed.
"""
self.novelTree.refreshHandle(tHandle)
return
# END Class GuiNovelView # END Class GuiNovelView
@@ -470,7 +477,7 @@ class GuiNovelTree(QTreeWidget):
return return
def refreshTree(self, rootHandle=None, overRide=False): def refreshTree(self, rootHandle=None, overRide=False):
"""Called whenever the Novel tab is activated. """Refresh the tree if it has been changed.
""" """
logger.debug("Requesting refresh of the novel tree") logger.debug("Requesting refresh of the novel tree")
if rootHandle is None: if rootHandle is None:
@@ -495,13 +502,24 @@ class GuiNovelTree(QTreeWidget):
return return
def updateWordCounts(self, tHandle): def refreshHandle(self, tHandle):
"""Update the word count for a given handle. """Refresh the data for a given handle.
""" """
tHeaders = self.theProject.index.getHandleWordCounts(tHandle) idxData = self.theProject.index.getItemData(tHandle)
for titleKey, wCount in tHeaders: if idxData is None:
if titleKey in self._treeMap: return
self._treeMap[titleKey].setText(self.C_WORDS, f"{wCount:n}")
logger.debug("Refreshing meta data for item '%s'", tHandle)
for sTitle, tHeading in idxData.items():
sKey = f"{tHandle}:{sTitle}"
trItem = self._treeMap.get(sKey, None)
if trItem is None:
logger.debug("Heading '%s' not in novel tree", sKey)
self.refreshTree()
return
self._updateTreeItemValues(trItem, tHeading, tHandle, sTitle)
return return
def getSelectedHandle(self): def getSelectedHandle(self):
@@ -509,14 +527,11 @@ class GuiNovelTree(QTreeWidget):
selected, return the first. selected, return the first.
""" """
selItem = self.selectedItems() selItem = self.selectedItems()
tHandle = None
tLine = 0
if selItem: if selItem:
tHandle = selItem[0].data(self.C_TITLE, self.D_HANDLE) tHandle = selItem[0].data(self.C_TITLE, self.D_HANDLE)
sTitle = selItem[0].data(self.C_TITLE, self.D_TITLE) sTitle = selItem[0].data(self.C_TITLE, self.D_TITLE)
tLine = checkInt(sTitle[1:], 1) - 1 return tHandle, sTitle
return None, None
return tHandle, tLine
def setLastColType(self, colType, doRefresh=True): def setLastColType(self, colType, doRefresh=True):
"""Change the content type of the last column and rebuild. """Change the content type of the last column and rebuild.
@@ -575,11 +590,11 @@ class GuiNovelTree(QTreeWidget):
if not isinstance(selItem, QTreeWidgetItem): if not isinstance(selItem, QTreeWidgetItem):
return return
tHandle, _ = self.getSelectedHandle() tHandle, sTitle = self.getSelectedHandle()
if tHandle is None: if tHandle is None:
return return
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle or "", False)
return return
@@ -590,6 +605,22 @@ class GuiNovelTree(QTreeWidget):
self.clearSelection() self.clearSelection()
return return
def resizeEvent(self, event):
"""Elide labels in the extra column.
"""
super().resizeEvent(event)
newW = event.size().width()
oldW = event.oldSize().width()
if newW != oldW:
eliW = int(0.25 * newW)
fMetric = self.fontMetrics()
for i in range(self.topLevelItemCount()):
trItem = self.topLevelItem(i)
if isinstance(trItem, QTreeWidgetItem):
lastText = trItem.data(self.C_EXTRA, Qt.UserRole)
trItem.setText(self.C_EXTRA, fMetric.elidedText(lastText, Qt.ElideRight, eliW))
return
## ##
# Private Slots # Private Slots
## ##
@@ -621,8 +652,8 @@ class GuiNovelTree(QTreeWidget):
clicked, and send it to the main gui class for opening in the clicked, and send it to the main gui class for opening in the
document editor. document editor.
""" """
tHandle, tLine = self.getSelectedHandle() tHandle, sTitle = self.getSelectedHandle()
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, tLine, "") self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True)
return return
## ##
@@ -638,30 +669,16 @@ class GuiNovelTree(QTreeWidget):
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True) novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
for tKey, tHandle, sTitle, novIdx in novStruct: for tKey, tHandle, sTitle, novIdx in novStruct:
if novIdx.level == "H0":
iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0)
if iLevel == 0:
continue continue
hDec = self.mainTheme.getHeaderDecoration(iLevel)
newItem = QTreeWidgetItem() newItem = QTreeWidgetItem()
newItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
newItem.setText(self.C_TITLE, novIdx.title)
newItem.setData(self.C_TITLE, self.D_HANDLE, tHandle) newItem.setData(self.C_TITLE, self.D_HANDLE, tHandle)
newItem.setData(self.C_TITLE, self.D_TITLE, sTitle) newItem.setData(self.C_TITLE, self.D_TITLE, sTitle)
newItem.setData(self.C_TITLE, self.D_KEY, tKey) newItem.setData(self.C_TITLE, self.D_KEY, tKey)
newItem.setFont(self.C_TITLE, self._hFonts[iLevel])
newItem.setText(self.C_WORDS, f"{novIdx.wordCount:n}")
newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
newItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore)
# Custom column
lastText, toolTip = self._getLastColumnText(tHandle, sTitle)
newItem.setText(self.C_EXTRA, lastText)
if lastText:
newItem.setToolTip(self.C_EXTRA, toolTip)
self._updateTreeItemValues(newItem, novIdx, tHandle, sTitle)
self._treeMap[tKey] = newItem self._treeMap[tKey] = newItem
self.addTopLevelItem(newItem) self.addTopLevelItem(newItem)
@@ -672,24 +689,52 @@ class GuiNovelTree(QTreeWidget):
return return
def _updateTreeItemValues(self, trItem, idxItem, tHandle, sTitle):
"""Set the tree item values from the index entry.
"""
iLevel = nwHeaders.H_LEVEL.get(idxItem.level, 0)
hDec = self.mainTheme.getHeaderDecoration(iLevel)
trItem.setData(self.C_TITLE, Qt.DecorationRole, hDec)
trItem.setText(self.C_TITLE, idxItem.title)
trItem.setFont(self.C_TITLE, self._hFonts[iLevel])
trItem.setText(self.C_WORDS, f"{idxItem.wordCount:n}")
trItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore)
# Custom column
mW = int(0.25 * self.viewport().width())
lastText, toolTip = self._getLastColumnText(tHandle, sTitle)
elideText = self.fontMetrics().elidedText(lastText, Qt.ElideRight, mW)
trItem.setText(self.C_EXTRA, elideText)
trItem.setData(self.C_EXTRA, Qt.UserRole, lastText)
trItem.setToolTip(self.C_EXTRA, toolTip)
return
def _getLastColumnText(self, tHandle, sTitle): def _getLastColumnText(self, tHandle, sTitle):
"""Generate the text for the last column based on user settings. """Generate the text for the last column based on user settings.
""" """
if self._lastCol == NovelTreeColumn.HIDDEN: if self._lastCol == NovelTreeColumn.HIDDEN:
return "", "" return "", ""
refData = []
refName = ""
theRefs = self.theProject.index.getReferences(tHandle, sTitle) theRefs = self.theProject.index.getReferences(tHandle, sTitle)
if self._lastCol == NovelTreeColumn.POV: if self._lastCol == NovelTreeColumn.POV:
newText = ", ".join(theRefs[nwKeyWords.POV_KEY]) refData = theRefs[nwKeyWords.POV_KEY]
return newText, f"{self._povLabel}: {newText}" refName = self._povLabel
elif self._lastCol == NovelTreeColumn.FOCUS: elif self._lastCol == NovelTreeColumn.FOCUS:
newText = ", ".join(theRefs[nwKeyWords.FOCUS_KEY]) refData = theRefs[nwKeyWords.FOCUS_KEY]
return newText, f"{self._focLabel}: {newText}" refName = self._focLabel
elif self._lastCol == NovelTreeColumn.PLOT: elif self._lastCol == NovelTreeColumn.PLOT:
newText = ", ".join(theRefs[nwKeyWords.PLOT_KEY]) refData = theRefs[nwKeyWords.PLOT_KEY]
return newText, f"{self._pltLabel}: {newText}" refName = self._pltLabel
if refData:
toolText = ", ".join(refData)
return refData[0], f"{refName}: {toolText}"
return "", "" return "", ""
@@ -699,7 +744,7 @@ class GuiNovelTree(QTreeWidget):
logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle) logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle)
pIndex = self.theProject.index pIndex = self.theProject.index
novIdx = pIndex.getNovelData(tHandle, sTitle) novIdx = pIndex.getItemHeader(tHandle, sTitle)
refTags = pIndex.getReferences(tHandle, sTitle) refTags = pIndex.getReferences(tHandle, sTitle)
synopText = novIdx.synopsis synopText = novIdx.synopsis
+19 -17
View File
@@ -55,6 +55,7 @@ logger = logging.getLogger(__name__)
class GuiOutlineView(QWidget): class GuiOutlineView(QWidget):
loadDocumentTagRequest = pyqtSignal(str, Enum) loadDocumentTagRequest = pyqtSignal(str, Enum)
openDocumentRequest = pyqtSignal(str, Enum, str, bool)
def __init__(self, mainGui): def __init__(self, mainGui):
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
@@ -72,6 +73,7 @@ class GuiOutlineView(QWidget):
self.splitOutline = QSplitter(Qt.Vertical) self.splitOutline = QSplitter(Qt.Vertical)
self.splitOutline.addWidget(self.outlineTree) self.splitOutline.addWidget(self.outlineTree)
self.splitOutline.addWidget(self.outlineData) self.splitOutline.addWidget(self.outlineData)
self.splitOutline.setOpaqueResize(False)
self.splitOutline.setSizes(self.mainConf.outlinePanePos) self.splitOutline.setSizes(self.mainConf.outlinePanePos)
# Assemble # Assemble
@@ -375,15 +377,16 @@ class GuiOutlineTree(QTreeWidget):
hiddenStateChanged = pyqtSignal() hiddenStateChanged = pyqtSignal()
activeItemChanged = pyqtSignal(str, str) activeItemChanged = pyqtSignal(str, str)
def __init__(self, theOutline): def __init__(self, outlineView):
super().__init__(parent=theOutline) super().__init__(parent=outlineView)
logger.debug("Initialising GuiOutlineTree ...") logger.debug("Initialising GuiOutlineTree ...")
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.mainGui = theOutline.mainGui self.outlineView = outlineView
self.theProject = theOutline.mainGui.theProject self.mainGui = outlineView.mainGui
self.mainTheme = theOutline.mainGui.mainTheme self.theProject = outlineView.mainGui.theProject
self.mainTheme = outlineView.mainGui.mainTheme
self.setUniformRowHeights(True) self.setUniformRowHeights(True)
self.setFrameStyle(QFrame.NoFrame) self.setFrameStyle(QFrame.NoFrame)
@@ -524,13 +527,11 @@ class GuiOutlineTree(QTreeWidget):
selected, return the first. selected, return the first.
""" """
selItem = self.selectedItems() selItem = self.selectedItems()
tHandle = None
tLine = 0
if selItem: if selItem:
tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE) tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
tLine = checkInt(selItem[0].text(self._colIdx[nwOutline.LINE]), 1) - 1 sTitle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
return tHandle, sTitle
return tHandle, tLine return None, None
## ##
# Slots # Slots
@@ -542,8 +543,10 @@ class GuiOutlineTree(QTreeWidget):
clicked, and send it to the main gui class for opening in the clicked, and send it to the main gui class for opening in the
document editor. document editor.
""" """
tHandle, tLine = self.getSelectedHandle() tHandle, sTitle = self.getSelectedHandle()
self.mainGui.openDocument(tHandle, tLine=tLine - 1, doScroll=True) if tHandle is None:
return
self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True)
return return
@pyqtSlot() @pyqtSlot()
@@ -554,9 +557,8 @@ class GuiOutlineTree(QTreeWidget):
selItems = self.selectedItems() selItems = self.selectedItems()
if selItems: if selItems:
tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE) tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
sTitle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE) sTitle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
self.activeItemChanged.emit(tHandle, sTitle) self.activeItemChanged.emit(tHandle, sTitle)
return return
@pyqtSlot(int, int, int) @pyqtSlot(int, int, int)
@@ -718,7 +720,7 @@ class GuiOutlineTree(QTreeWidget):
trItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level) trItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level)
trItem.setIcon(self._colIdx[nwOutline.LABEL], self._dIcon[nwItem.mainHeading]) trItem.setIcon(self._colIdx[nwOutline.LABEL], self._dIcon[nwItem.mainHeading])
trItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName) trItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName)
trItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0")) trItem.setText(self._colIdx[nwOutline.LINE], f"{novIdx.line:n}")
trItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis) trItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis)
trItem.setText(self._colIdx[nwOutline.CCOUNT], f"{novIdx.charCount:n}") trItem.setText(self._colIdx[nwOutline.CCOUNT], f"{novIdx.charCount:n}")
trItem.setText(self._colIdx[nwOutline.WCOUNT], f"{novIdx.wordCount:n}") trItem.setText(self._colIdx[nwOutline.WCOUNT], f"{novIdx.wordCount:n}")
@@ -1048,7 +1050,7 @@ class GuiOutlineDetails(QScrollArea):
""" """
pIndex = self.theProject.index pIndex = self.theProject.index
nwItem = self.theProject.tree[tHandle] nwItem = self.theProject.tree[tHandle]
novIdx = pIndex.getNovelData(tHandle, sTitle) novIdx = pIndex.getItemHeader(tHandle, sTitle)
theRefs = pIndex.getReferences(tHandle, sTitle) theRefs = pIndex.getReferences(tHandle, sTitle)
if nwItem is None or novIdx is None: if nwItem is None or novIdx is None:
return False return False
+5 -5
View File
@@ -60,7 +60,7 @@ class GuiProjectView(QWidget):
# Signals for user interaction with the project tree # Signals for user interaction with the project tree
selectedItemChanged = pyqtSignal(str) selectedItemChanged = pyqtSignal(str)
openDocumentRequest = pyqtSignal(str, Enum, int, str) openDocumentRequest = pyqtSignal(str, Enum, str, bool)
# Requests for the main GUI # Requests for the main GUI
projectSettingsRequest = pyqtSignal(int) projectSettingsRequest = pyqtSignal(int)
@@ -1144,7 +1144,7 @@ class GuiProjectTree(QTreeWidget):
return return
if tItem.isFileType(): if tItem.isFileType():
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "") self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
else: else:
trItem.setExpanded(not trItem.isExpanded()) trItem.setExpanded(not trItem.isExpanded())
@@ -1190,11 +1190,11 @@ class GuiProjectTree(QTreeWidget):
if isFile: if isFile:
aOpenDoc = ctxMenu.addAction(self.tr("Open Document")) aOpenDoc = ctxMenu.addAction(self.tr("Open Document"))
aOpenDoc.triggered.connect( aOpenDoc.triggered.connect(
lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, -1, "") lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
) )
aViewDoc = ctxMenu.addAction(self.tr("View Document")) aViewDoc = ctxMenu.addAction(self.tr("View Document"))
aViewDoc.triggered.connect( aViewDoc.triggered.connect(
lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False)
) )
ctxMenu.addSeparator() ctxMenu.addSeparator()
@@ -1324,7 +1324,7 @@ class GuiProjectTree(QTreeWidget):
return return
if tItem.isFileType(): if tItem.isFileType():
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, -1, "") self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False)
return return
@@ -1,7 +1,7 @@
""" """
novelWriter GUI Main Window Views ToolBar novelWriter GUI Main Window SideBar
=========================================== ===========================================
GUI class for the main window "Views" toolbar GUI class for the main window side bar
File History: File History:
Created: 2022-05-10 [1.7b1] Created: 2022-05-10 [1.7b1]
@@ -36,14 +36,14 @@ from novelwriter.enum import nwView
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GuiViewsBar(QToolBar): class GuiSideBar(QToolBar):
viewChangeRequested = pyqtSignal(nwView) viewChangeRequested = pyqtSignal(nwView)
def __init__(self, mainGui): def __init__(self, mainGui):
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
logger.debug("Initialising GuiViewsBar ...") logger.debug("Initialising GuiSideBar ...")
self.mainConf = novelwriter.CONFIG self.mainConf = novelwriter.CONFIG
self.mainGui = mainGui self.mainGui = mainGui
@@ -123,7 +123,7 @@ class GuiViewsBar(QToolBar):
self.updateTheme() self.updateTheme()
logger.debug("GuiViewsBar initialisation complete") logger.debug("GuiSideBar initialisation complete")
return return
@@ -142,4 +142,4 @@ class GuiViewsBar(QToolBar):
return return
# END Class GuiViewsBar # END Class GuiSideBar
+14 -11
View File
@@ -34,7 +34,6 @@ from PyQt5.QtGui import QColor, QPainter
from PyQt5.QtWidgets import qApp, QStatusBar, QLabel, QAbstractButton from PyQt5.QtWidgets import qApp, QStatusBar, QLabel, QAbstractButton
from novelwriter.common import formatTime from novelwriter.common import formatTime
from novelwriter.enum import nwState
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -53,8 +52,8 @@ class GuiMainStatus(QStatusBar):
self.userIdle = False self.userIdle = False
colNone = QColor(*self.mainTheme.statNone) colNone = QColor(*self.mainTheme.statNone)
colTrue = QColor(*self.mainTheme.statUnsaved) colSaved = QColor(*self.mainTheme.statSaved)
colFalse = QColor(*self.mainTheme.statSaved) colUnsaved = QColor(*self.mainTheme.statUnsaved)
iPx = self.mainTheme.baseIconSize iPx = self.mainTheme.baseIconSize
@@ -72,7 +71,7 @@ class GuiMainStatus(QStatusBar):
self.addPermanentWidget(self.langText) self.addPermanentWidget(self.langText)
# The Editor Status # The Editor Status
self.docIcon = StatusLED(colNone, colTrue, colFalse, iPx, iPx, self) self.docIcon = StatusLED(colNone, colSaved, colUnsaved, iPx, iPx, self)
self.docText = QLabel(self.tr("Editor")) self.docText = QLabel(self.tr("Editor"))
self.docIcon.setContentsMargins(0, 0, 0, 0) self.docIcon.setContentsMargins(0, 0, 0, 0)
self.docText.setContentsMargins(0, 0, xM, 0) self.docText.setContentsMargins(0, 0, xM, 0)
@@ -80,7 +79,7 @@ class GuiMainStatus(QStatusBar):
self.addPermanentWidget(self.docText) self.addPermanentWidget(self.docText)
# The Project Status # The Project Status
self.projIcon = StatusLED(colNone, colTrue, colFalse, iPx, iPx, self) self.projIcon = StatusLED(colNone, colSaved, colUnsaved, iPx, iPx, self)
self.projText = QLabel(self.tr("Project")) self.projText = QLabel(self.tr("Project"))
self.projIcon.setContentsMargins(0, 0, 0, 0) self.projIcon.setContentsMargins(0, 0, 0, 0)
self.projText.setContentsMargins(0, 0, xM, 0) self.projText.setContentsMargins(0, 0, xM, 0)
@@ -122,8 +121,8 @@ class GuiMainStatus(QStatusBar):
self.setRefTime(None) self.setRefTime(None)
self.setLanguage(None, "") self.setLanguage(None, "")
self.setProjectStats(0, 0) self.setProjectStats(0, 0)
self.setProjectStatus(nwState.NONE) self.setProjectStatus(StatusLED.S_NONE)
self.setDocumentStatus(nwState.NONE) self.setDocumentStatus(StatusLED.S_NONE)
self.updateTime() self.updateTime()
return True return True
@@ -236,14 +235,14 @@ class GuiMainStatus(QStatusBar):
def doUpdateProjectStatus(self, isChanged): def doUpdateProjectStatus(self, isChanged):
"""Slot for updating the project status. """Slot for updating the project status.
""" """
self.setProjectStatus(nwState.GOOD if isChanged else nwState.BAD) self.setProjectStatus(StatusLED.S_BAD if isChanged else StatusLED.S_GOOD)
return return
@pyqtSlot(bool) @pyqtSlot(bool)
def doUpdateDocumentStatus(self, isChanged): def doUpdateDocumentStatus(self, isChanged):
"""Slot for updating the document status. """Slot for updating the document status.
""" """
self.setDocumentStatus(nwState.GOOD if isChanged else nwState.BAD) self.setDocumentStatus(StatusLED.S_BAD if isChanged else StatusLED.S_GOOD)
return return
# END Class GuiMainStatus # END Class GuiMainStatus
@@ -251,6 +250,10 @@ class GuiMainStatus(QStatusBar):
class StatusLED(QAbstractButton): class StatusLED(QAbstractButton):
S_NONE = 0
S_BAD = 1
S_GOOD = 2
def __init__(self, colNone, colGood, colBad, sW, sH, parent=None): def __init__(self, colNone, colGood, colBad, sW, sH, parent=None):
super().__init__(parent=parent) super().__init__(parent=parent)
@@ -271,9 +274,9 @@ class StatusLED(QAbstractButton):
def setState(self, theState): def setState(self, theState):
"""Set the colour state. """Set the colour state.
""" """
if theState == nwState.GOOD: if theState == self.S_GOOD:
self._theCol = self._colGood self._theCol = self._colGood
elif theState == nwState.BAD: elif theState == self.S_BAD:
self._theCol = self._colBad self._theCol = self._colBad
else: else:
self._theCol = self._colNone self._theCol = self._colNone
+48 -34
View File
@@ -41,7 +41,7 @@ from PyQt5.QtWidgets import (
from novelwriter.gui import ( from novelwriter.gui import (
GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu, GuiDocEditor, GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiMainMenu,
GuiMainStatus, GuiNovelView, GuiOutlineView, GuiProjectView, GuiTheme, GuiMainStatus, GuiNovelView, GuiOutlineView, GuiProjectView, GuiTheme,
GuiViewsBar GuiSideBar
) )
from novelwriter.dialogs import ( from novelwriter.dialogs import (
GuiAbout, GuiPreferences, GuiProjectDetails, GuiProjectLoad, GuiAbout, GuiPreferences, GuiProjectDetails, GuiProjectLoad,
@@ -118,7 +118,7 @@ class GuiMain(QMainWindow):
self.itemDetails = GuiItemDetails(self) self.itemDetails = GuiItemDetails(self)
self.outlineView = GuiOutlineView(self) self.outlineView = GuiOutlineView(self)
self.mainMenu = GuiMainMenu(self) self.mainMenu = GuiMainMenu(self)
self.viewsBar = GuiViewsBar(self) self.viewsBar = GuiSideBar(self)
# Project Tree Stack # Project Tree Stack
self.projStack = QStackedWidget() self.projStack = QStackedWidget()
@@ -140,12 +140,14 @@ class GuiMain(QMainWindow):
self.splitView.addWidget(self.docViewer) self.splitView.addWidget(self.docViewer)
self.splitView.addWidget(self.viewMeta) self.splitView.addWidget(self.viewMeta)
self.splitView.setHandleWidth(hWd) self.splitView.setHandleWidth(hWd)
self.splitView.setOpaqueResize(False)
self.splitView.setSizes(self.mainConf.viewPanePos) self.splitView.setSizes(self.mainConf.viewPanePos)
# Splitter : Document Editor / Document Viewer # Splitter : Document Editor / Document Viewer
self.splitDocs = QSplitter(Qt.Horizontal) self.splitDocs = QSplitter(Qt.Horizontal)
self.splitDocs.addWidget(self.docEditor) self.splitDocs.addWidget(self.docEditor)
self.splitDocs.addWidget(self.splitView) self.splitDocs.addWidget(self.splitView)
self.splitDocs.setOpaqueResize(False)
self.splitDocs.setHandleWidth(hWd) self.splitDocs.setHandleWidth(hWd)
# Splitter : Project Tree / Main Tabs # Splitter : Project Tree / Main Tabs
@@ -153,6 +155,7 @@ class GuiMain(QMainWindow):
self.splitMain.setContentsMargins(0, 0, 0, 0) self.splitMain.setContentsMargins(0, 0, 0, 0)
self.splitMain.addWidget(self.treePane) self.splitMain.addWidget(self.treePane)
self.splitMain.addWidget(self.splitDocs) self.splitMain.addWidget(self.splitDocs)
self.splitMain.setOpaqueResize(False)
self.splitMain.setHandleWidth(hWd) self.splitMain.setHandleWidth(hWd)
self.splitMain.setSizes(self.mainConf.mainPanePos) self.splitMain.setSizes(self.mainConf.mainPanePos)
@@ -224,10 +227,13 @@ class GuiMain(QMainWindow):
self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts) self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts)
self.docEditor.docCountsChanged.connect(self.projView.updateCounts) self.docEditor.docCountsChanged.connect(self.projView.updateCounts)
self.docEditor.loadDocumentTagRequest.connect(self._followTag) self.docEditor.loadDocumentTagRequest.connect(self._followTag)
self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta)
self.docViewer.loadDocumentTagRequest.connect(self._followTag) self.docViewer.loadDocumentTagRequest.connect(self._followTag)
self.outlineView.loadDocumentTagRequest.connect(self._followTag) self.outlineView.loadDocumentTagRequest.connect(self._followTag)
self.outlineView.openDocumentRequest.connect(self._openDocument)
# Finalise Initialisation # Finalise Initialisation
# ======================= # =======================
@@ -306,7 +312,7 @@ class GuiMain(QMainWindow):
# Work Area # Work Area
self.docEditor.clearEditor() self.docEditor.clearEditor()
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.closeDocViewer() self.closeDocViewer(byUser=False)
self.outlineView.clearProject() self.outlineView.clearProject()
# General # General
@@ -593,14 +599,21 @@ class GuiMain(QMainWindow):
logger.debug("Requested item '%s' is not a document", tHandle) logger.debug("Requested item '%s' is not a document", tHandle)
return False return False
cHandle = self.docEditor.docHandle()
if cHandle == tHandle:
self.docEditor.setCursorLine(tLine)
if changeFocus:
self.docEditor.setFocus()
return True
self.closeDocument(beforeOpen=True) self.closeDocument(beforeOpen=True)
self._changeView(nwView.EDITOR) self._changeView(nwView.EDITOR)
if self.docEditor.loadText(tHandle, tLine): if self.docEditor.loadText(tHandle, tLine):
if changeFocus:
self.docEditor.setFocus()
self.theProject.data.setLastHandle(tHandle, "editor") self.theProject.data.setLastHandle(tHandle, "editor")
self.projView.setSelectedHandle(tHandle, doScroll=doScroll) self.projView.setSelectedHandle(tHandle, doScroll=doScroll)
self.novelView.setActiveHandle(tHandle) self.novelView.setActiveHandle(tHandle)
if changeFocus:
self.docEditor.setFocus()
else: else:
return False return False
@@ -618,7 +631,7 @@ class GuiMain(QMainWindow):
fHandle = None # The first file handle we encounter fHandle = None # The first file handle we encounter
foundIt = False # We've found tHandle, pick the next we see foundIt = False # We've found tHandle, pick the next we see
for tItem in self.theProject.tree: for tItem in self.theProject.tree:
if tItem is None or not tItem.isFileType(): if not tItem.isFileType():
continue continue
if fHandle is None: if fHandle is None:
fHandle = tItem.itemHandle fHandle = tItem.itemHandle
@@ -648,7 +661,7 @@ class GuiMain(QMainWindow):
return True return True
def viewDocument(self, tHandle=None, tAnchor=None): def viewDocument(self, tHandle=None, sTitle=None):
"""Load a document for viewing in the view panel. """Load a document for viewing in the view panel.
""" """
if not self.hasProject: if not self.hasProject:
@@ -687,7 +700,8 @@ class GuiMain(QMainWindow):
self.splitDocs.setSizes(vPos) self.splitDocs.setSizes(vPos)
self.viewMeta.setVisible(self.mainConf.showRefPanel) self.viewMeta.setVisible(self.mainConf.showRefPanel)
self.docViewer.navigateTo(tAnchor) if sTitle:
self.docViewer.navigateTo(f"#{sTitle}")
return True return True
@@ -775,17 +789,23 @@ class GuiMain(QMainWindow):
return False return False
tHandle = None tHandle = None
sTitle = None
tLine = None tLine = None
if self.projView.treeHasFocus(): if self.projView.treeHasFocus():
tHandle = self.projView.getSelectedHandle() tHandle = self.projView.getSelectedHandle()
elif self.novelView.treeHasFocus(): elif self.novelView.treeHasFocus():
tHandle, tLine = self.novelView.getSelectedHandle() tHandle, sTitle = self.novelView.getSelectedHandle()
elif self.outlineView.treeHasFocus(): elif self.outlineView.treeHasFocus():
tHandle, tLine = self.outlineView.getSelectedHandle() tHandle, sTitle = self.outlineView.getSelectedHandle()
else: else:
logger.warning("No item selected") logger.warning("No item selected")
return False return False
if tHandle is not None and sTitle is not None:
hItem = self.theProject.index.getItemHeader(tHandle, sTitle)
if hItem is not None:
tLine = hItem.line
if tHandle is not None: if tHandle is not None:
self.openDocument(tHandle, tLine=tLine, changeFocus=False, doScroll=False) self.openDocument(tHandle, tLine=tLine, changeFocus=False, doScroll=False)
@@ -808,17 +828,8 @@ class GuiMain(QMainWindow):
"""Rebuild the project tree. """Rebuild the project tree.
""" """
self.projView.populateTree() self.projView.populateTree()
# self.novelView.refreshTree()
return return
def requestNovelTreeRefresh(self):
"""Update the novel tree, but only if it is visible.
"""
if self.projStack.currentIndex() == self.idxNovelView and self.hasProject:
self.novelView.refreshTree()
return True
return False
def rebuildIndex(self, beQuiet=False): def rebuildIndex(self, beQuiet=False):
"""Rebuild the entire index. """Rebuild the entire index.
""" """
@@ -833,6 +844,7 @@ class GuiMain(QMainWindow):
self.projView.saveProjectTasks() self.projView.saveProjectTasks()
self.theProject.index.rebuildIndex() self.theProject.index.rebuildIndex()
self.projView.populateTree() self.projView.populateTree()
self.novelView.refreshTree()
tEnd = time() tEnd = time()
self.setStatus( self.setStatus(
@@ -1208,14 +1220,19 @@ class GuiMain(QMainWindow):
self.theProject.data.setLastHandle(None, "editor") self.theProject.data.setLastHandle(None, "editor")
return return
def closeDocViewer(self): def closeDocViewer(self, byUser=True):
"""Close the document view panel. """Close the document view panel.
""" """
self.docViewer.clearViewer() self.docViewer.clearViewer()
self.theProject.data.setLastHandle(None, "viewer") if byUser:
# Only reset the last handle if the user called this
self.theProject.data.setLastHandle(None, "viewer")
# Hide the panel
bPos = self.splitMain.sizes() bPos = self.splitMain.sizes()
self.splitView.setVisible(False) self.splitView.setVisible(False)
self.splitDocs.setSizes([bPos[1], 0]) self.splitDocs.setSizes([bPos[1], 0])
return not self.splitView.isVisible() return not self.splitView.isVisible()
def toggleFocusMode(self): def toggleFocusMode(self):
@@ -1471,18 +1488,22 @@ class GuiMain(QMainWindow):
if tMode == nwDocMode.EDIT: if tMode == nwDocMode.EDIT:
self.openDocument(tHandle) self.openDocument(tHandle)
elif tMode == nwDocMode.VIEW: elif tMode == nwDocMode.VIEW:
self.viewDocument(tHandle=tHandle, tAnchor=f"#{sTitle}") self.viewDocument(tHandle=tHandle, sTitle=sTitle)
return return
@pyqtSlot(str, Enum, int, str) @pyqtSlot(str, Enum, str, bool)
def _openDocument(self, tHandle, tMode, tLine, tAnchor): def _openDocument(self, tHandle, tMode, sTitle, setFocus):
"""Handle an open document request from one of the tree views. """Handle an open document request from one of the tree views.
""" """
if tHandle is not None: if tHandle is not None:
if tMode == nwDocMode.EDIT: if tMode == nwDocMode.EDIT:
self.openDocument(tHandle, tLine=tLine, changeFocus=False) tLine = None
hItem = self.theProject.index.getItemHeader(tHandle, sTitle)
if hItem is not None:
tLine = hItem.line
self.openDocument(tHandle, tLine=tLine, changeFocus=setFocus)
elif tMode == nwDocMode.VIEW: elif tMode == nwDocMode.VIEW:
self.viewDocument(tHandle=tHandle, tAnchor=(tAnchor or None)) self.viewDocument(tHandle=tHandle, sTitle=sTitle)
return return
@pyqtSlot(nwView) @pyqtSlot(nwView)
@@ -1564,7 +1585,6 @@ class GuiMain(QMainWindow):
self.docEditor.closeSearch() self.docEditor.closeSearch()
elif self.isFocusMode: elif self.isFocusMode:
self.toggleFocusMode() self.toggleFocusMode()
return return
@pyqtSlot(int) @pyqtSlot(int)
@@ -1581,17 +1601,11 @@ class GuiMain(QMainWindow):
"""Activated when the project view tab is changed. """Activated when the project view tab is changed.
""" """
sHandle = None sHandle = None
if stIndex == self.idxProjView: if stIndex == self.idxProjView:
sHandle = self.projView.getSelectedHandle() sHandle = self.projView.getSelectedHandle()
elif stIndex == self.idxNovelView: elif stIndex == self.idxNovelView:
if self.hasProject: sHandle, _ = self.novelView.getSelectedHandle()
self.novelView.refreshTree()
sHandle, _ = self.novelView.getSelectedHandle()
self.itemDetails.updateViewBox(sHandle) self.itemDetails.updateViewBox(sHandle)
return return
# END Class GuiMain # END Class GuiMain
@@ -1,109 +1,109 @@
{ {
"tagsIndex": { "tagsIndex": {
"Bod": {"handle": "4c4f28287af27", "heading": "T000001", "class": "CHARACTER"}, "Bod": {"handle": "4c4f28287af27", "heading": "T0001", "class": "CHARACTER"},
"Main": {"handle": "2426c6f0ca922", "heading": "T000001", "class": "PLOT"}, "Main": {"handle": "2426c6f0ca922", "heading": "T0001", "class": "PLOT"},
"Europe": {"handle": "04468803b92e1", "heading": "T000001", "class": "WORLD"} "Europe": {"handle": "04468803b92e1", "heading": "T0001", "class": "WORLD"}
}, },
"itemIndex": { "itemIndex": {
"7a992350f3eb6": { "7a992350f3eb6": {
"headings": { "headings": {
"T000001": {"level": "H1", "title": "Lorem Ipsum", "tag": "", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""} "T0001": {"level": "H1", "title": "Lorem Ipsum", "line": 1, "tag": "", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""}
} }
}, },
"8c58a65414c23": { "8c58a65414c23": {
"headings": { "headings": {
"T000000": {"level": "H0", "title": "", "tag": "", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""} "T0000": {"level": "H0", "title": "", "line": 0, "tag": "", "cCount": 1058, "wCount": 176, "pCount": 2, "synopsis": ""}
} }
}, },
"88d59a277361b": { "88d59a277361b": {
"headings": { "headings": {
"T000001": {"level": "H2", "title": "Prologue", "tag": "", "cCount": 584, "wCount": 92, "pCount": 1, "synopsis": "Explanation from the lipsum.com website."} "T0001": {"level": "H2", "title": "Prologue", "line": 1, "tag": "", "cCount": 584, "wCount": 92, "pCount": 1, "synopsis": "Explanation from the lipsum.com website."}
} }
}, },
"db7e733775d4d": { "db7e733775d4d": {
"headings": { "headings": {
"T000001": {"level": "H1", "title": "Act One", "tag": "", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""} "T0001": {"level": "H1", "title": "Act One", "line": 1, "tag": "", "cCount": 35, "wCount": 6, "pCount": 1, "synopsis": ""}
} }
}, },
"fb609cd8319dc": { "fb609cd8319dc": {
"headings": { "headings": {
"T000001": {"level": "H2", "title": "Chapter One", "tag": "", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."} "T0001": {"level": "H2", "title": "Chapter One", "line": 1, "tag": "", "cCount": 419, "wCount": 67, "pCount": 1, "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."}
}, },
"references": { "references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} "T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
} }
}, },
"88243afbe5ed8": { "88243afbe5ed8": {
"headings": { "headings": {
"T000001": {"level": "H3", "title": "Scene One", "tag": "", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."}, "T0001": {"level": "H3", "title": "Scene One", "line": 1, "tag": "", "cCount": 1197, "wCount": 174, "pCount": 2, "synopsis": "Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."},
"T000013": {"level": "H4", "title": "Scene One, Section Two", "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""} "T0002": {"level": "H4", "title": "Scene One, Section Two", "line": 13, "tag": "", "cCount": 1561, "wCount": 230, "pCount": 2, "synopsis": ""}
}, },
"references": { "references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} "T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
} }
}, },
"f96ec11c6a3da": { "f96ec11c6a3da": {
"headings": { "headings": {
"T000001": {"level": "H3", "title": "Scene Two", "tag": "", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."}, "T0001": {"level": "H3", "title": "Scene Two", "line": 1, "tag": "", "cCount": 2034, "wCount": 299, "pCount": 3, "synopsis": "Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."},
"T000015": {"level": "H4", "title": "Scene Two, Section Two", "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""} "T0002": {"level": "H4", "title": "Scene Two, Section Two", "line": 15, "tag": "", "cCount": 2009, "wCount": 301, "pCount": 3, "synopsis": ""}
}, },
"references": { "references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} "T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
} }
}, },
"846352075de7d": { "846352075de7d": {
"headings": { "headings": {
"T000001": {"level": "H2", "title": "Why do we use it?", "tag": "", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""} "T0001": {"level": "H2", "title": "Why do we use it?", "line": 1, "tag": "", "cCount": 631, "wCount": 109, "pCount": 3, "synopsis": ""}
} }
}, },
"441420a886d82": { "441420a886d82": {
"headings": { "headings": {
"T000001": {"level": "H2", "title": "Chapter Two", "tag": "", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."} "T0001": {"level": "H2", "title": "Chapter Two", "line": 1, "tag": "", "cCount": 477, "wCount": 70, "pCount": 1, "synopsis": "Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."}
}, },
"references": { "references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} "T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
} }
}, },
"eb103bc70c90c": { "eb103bc70c90c": {
"headings": { "headings": {
"T000001": {"level": "H3", "title": "Scene Three", "tag": "", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."} "T0001": {"level": "H3", "title": "Scene Three", "line": 1, "tag": "", "cCount": 3006, "wCount": 439, "pCount": 4, "synopsis": "Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."}
}, },
"references": { "references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} "T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
} }
}, },
"f8c0562e50f1b": { "f8c0562e50f1b": {
"headings": { "headings": {
"T000001": {"level": "H3", "title": "Scene Four", "tag": "", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."} "T0001": {"level": "H3", "title": "Scene Four", "line": 1, "tag": "", "cCount": 3839, "wCount": 563, "pCount": 6, "synopsis": "Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."}
}, },
"references": { "references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} "T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
} }
}, },
"47666c91c7ccf": { "47666c91c7ccf": {
"headings": { "headings": {
"T000001": {"level": "H3", "title": "Scene Five", "tag": "", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."} "T0001": {"level": "H3", "title": "Scene Five", "line": 1, "tag": "", "cCount": 3644, "wCount": 543, "pCount": 5, "synopsis": "Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."}
}, },
"references": { "references": {
"T000001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"} "T0001": {"Bod": "@pov", "Main": "@plot", "Europe": "@location"}
} }
}, },
"4c4f28287af27": { "4c4f28287af27": {
"headings": { "headings": {
"T000001": {"level": "H1", "title": "Nobody Owens", "tag": "Bod", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""} "T0001": {"level": "H1", "title": "Nobody Owens", "line": 1, "tag": "Bod", "cCount": 1864, "wCount": 284, "pCount": 3, "synopsis": ""}
}, },
"references": { "references": {
"T000001": {"Main": "@plot"} "T0001": {"Main": "@plot"}
} }
}, },
"2426c6f0ca922": { "2426c6f0ca922": {
"headings": { "headings": {
"T000001": {"level": "H1", "title": "Main Plot", "tag": "Main", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""} "T0001": {"level": "H1", "title": "Main Plot", "line": 1, "tag": "Main", "cCount": 1369, "wCount": 195, "pCount": 2, "synopsis": ""}
} }
}, },
"04468803b92e1": { "04468803b92e1": {
"headings": { "headings": {
"T000001": {"level": "H1", "title": "Ancient Europe", "tag": "Europe", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""} "T0001": {"level": "H1", "title": "Ancient Europe", "line": 1, "tag": "Europe", "cCount": 1770, "wCount": 259, "pCount": 3, "synopsis": ""}
} }
} }
} }
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" timeStamp="2022-11-07 12:58:26"> <novelWriterXML appVersion="2.0-rc2" hexVersion="0x020000c2" fileVersion="1.5" timeStamp="2022-11-15 15:14:31">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="4" autoCount="2" editTime="3"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="3">
<name>New Project</name> <name>New Project</name>
<title>New Novel</title> <title>New Novel</title>
<author>Jane Doe</author> <author>Jane Doe</author>
@@ -11,9 +11,9 @@
<spellChecking auto="yes">None</spellChecking> <spellChecking auto="yes">None</spellChecking>
<lastHandle> <lastHandle>
<entry key="editor">000000000000f</entry> <entry key="editor">000000000000f</entry>
<entry key="viewer">None</entry> <entry key="viewer">000000000000f</entry>
<entry key="novelTree">0000000000008</entry> <entry key="novelTree">0000000000008</entry>
<entry key="outline">0000000000008</entry> <entry key="outline">None</entry>
</lastHandle> </lastHandle>
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
@@ -30,13 +30,13 @@
<entry key="s000003" count="0" red="50" green="200" blue="0">Finished</entry> <entry key="s000003" count="0" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <importance>
<entry key="i000004" count="7" red="100" green="100" blue="100">New</entry> <entry key="i000004" count="6" red="100" green="100" blue="100">New</entry>
<entry key="i000005" count="0" red="200" green="50" blue="0">Minor</entry> <entry key="i000005" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i000006" count="0" red="200" green="150" blue="0">Major</entry> <entry key="i000006" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry> <entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </settings>
<content items="12" novelWords="136" notesWords="27"> <content items="11" novelWords="136" notesWords="27">
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL"> <item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes"/> <meta expanded="yes"/>
<name status="s000000" import="i000004">Novel</name> <name status="s000000" import="i000004">Novel</name>
@@ -81,9 +81,5 @@
<meta expanded="no" heading="H1" charCount="51" wordCount="9" paraCount="1" cursorPos="68"/> <meta expanded="no" heading="H1" charCount="51" wordCount="9" paraCount="1" cursorPos="68"/>
<name status="s000000" import="i000004" active="yes">New Note</name> <name status="s000000" import="i000004" active="yes">New Note</name>
</item> </item>
<item handle="0000000000014" parent="None" root="0000000000014" order="4" type="ROOT" class="TRASH">
<meta expanded="no"/>
<name status="s000000" import="i000004">Trash</name>
</item>
</content> </content>
</novelWriterXML> </novelWriterXML>
+5 -5
View File
@@ -208,12 +208,12 @@ def testBaseCommon_IsHandle():
def testBaseCommon_IsTitleTag(): def testBaseCommon_IsTitleTag():
"""Test the isItemClass function. """Test the isItemClass function.
""" """
assert isTitleTag("T123456") is True assert isTitleTag("T1234") is True
assert isTitleTag("t123456") is False assert isTitleTag("t1234") is False
assert isTitleTag("S123456") is False assert isTitleTag("S1234") is False
assert isTitleTag("T12345A") is False assert isTitleTag("T123A") is False
assert isTitleTag("T1234567") is False assert isTitleTag("T12345") is False
assert isTitleTag("None") is False assert isTitleTag("None") is False
assert isTitleTag(None) is False assert isTitleTag(None) is False
+213 -212
View File
@@ -29,7 +29,7 @@ from tools import C, buildTestProject, cmpFiles, writeFile
from novelwriter.enum import nwItemClass, nwItemLayout from novelwriter.enum import nwItemClass, nwItemLayout
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.index import NWIndex, countWords, TagsIndex from novelwriter.core.index import IndexItem, NWIndex, countWords, TagsIndex
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
@@ -231,10 +231,10 @@ def testCoreIndex_CheckThese(mockGUI, fncPath, mockRnd):
"@invalid: John\n" # Checks for issue #688 "@invalid: John\n" # Checks for issue #688
)) ))
assert theIndex._tagsIndex.tagHandle("Jane") == cHandle assert theIndex._tagsIndex.tagHandle("Jane") == cHandle
assert theIndex._tagsIndex.tagHeading("Jane") == "T000001" assert theIndex._tagsIndex.tagHeading("Jane") == "T0001"
assert theIndex._tagsIndex.tagClass("Jane") == "CHARACTER" assert theIndex._tagsIndex.tagClass("Jane") == "CHARACTER"
assert theIndex.getNovelData(nHandle, "T000001").title == "Hello World!" assert theIndex.getItemHeader(nHandle, "T0001").title == "Hello World!"
assert theIndex.getReferences(nHandle, "T000001") == { assert theIndex.getReferences(nHandle, "T0001") == {
"@char": [], "@char": [],
"@custom": [], "@custom": [],
"@entity": [], "@entity": [],
@@ -345,9 +345,9 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
"Well, not really.\n" "Well, not really.\n"
)) ))
assert theIndex._tagsIndex.tagHandle("Jane") == cHandle assert theIndex._tagsIndex.tagHandle("Jane") == cHandle
assert theIndex._tagsIndex.tagHeading("Jane") == "T000001" assert theIndex._tagsIndex.tagHeading("Jane") == "T0001"
assert theIndex._tagsIndex.tagClass("Jane") == "CHARACTER" assert theIndex._tagsIndex.tagClass("Jane") == "CHARACTER"
assert theIndex.getNovelData(nHandle, "T000001").title == "Hello World!" assert theIndex.getItemHeader(nHandle, "T0001").title == "Hello World!"
# Title Indexing # Title Indexing
# ============== # ==============
@@ -369,40 +369,45 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
"##### Title Five\n\n" # Not interpreted as a title, the hashes are counted as a word "##### Title Five\n\n" # Not interpreted as a title, the hashes are counted as a word
"Paragraph Five.\n\n" "Paragraph Five.\n\n"
)) ))
assert theIndex._itemIndex[nHandle]["T000001"].references == {} assert theIndex._itemIndex[nHandle]["T0001"].references == {}
assert theIndex._itemIndex[nHandle]["T000007"].references == {} assert theIndex._itemIndex[nHandle]["T0002"].references == {}
assert theIndex._itemIndex[nHandle]["T000013"].references == {} assert theIndex._itemIndex[nHandle]["T0003"].references == {}
assert theIndex._itemIndex[nHandle]["T000019"].references == {} assert theIndex._itemIndex[nHandle]["T0004"].references == {}
assert theIndex._itemIndex[nHandle]["T000001"].level == "H1" assert theIndex._itemIndex[nHandle]["T0001"].level == "H1"
assert theIndex._itemIndex[nHandle]["T000007"].level == "H2" assert theIndex._itemIndex[nHandle]["T0002"].level == "H2"
assert theIndex._itemIndex[nHandle]["T000013"].level == "H3" assert theIndex._itemIndex[nHandle]["T0003"].level == "H3"
assert theIndex._itemIndex[nHandle]["T000019"].level == "H4" assert theIndex._itemIndex[nHandle]["T0004"].level == "H4"
assert theIndex._itemIndex[nHandle]["T000001"].title == "Title One" assert theIndex._itemIndex[nHandle]["T0001"].line == 1
assert theIndex._itemIndex[nHandle]["T000007"].title == "Title Two" assert theIndex._itemIndex[nHandle]["T0002"].line == 7
assert theIndex._itemIndex[nHandle]["T000013"].title == "Title Three" assert theIndex._itemIndex[nHandle]["T0003"].line == 13
assert theIndex._itemIndex[nHandle]["T000019"].title == "Title Four" assert theIndex._itemIndex[nHandle]["T0004"].line == 19
assert theIndex._itemIndex[nHandle]["T000001"].charCount == 23 assert theIndex._itemIndex[nHandle]["T0001"].title == "Title One"
assert theIndex._itemIndex[nHandle]["T000007"].charCount == 23 assert theIndex._itemIndex[nHandle]["T0002"].title == "Title Two"
assert theIndex._itemIndex[nHandle]["T000013"].charCount == 27 assert theIndex._itemIndex[nHandle]["T0003"].title == "Title Three"
assert theIndex._itemIndex[nHandle]["T000019"].charCount == 56 assert theIndex._itemIndex[nHandle]["T0004"].title == "Title Four"
assert theIndex._itemIndex[nHandle]["T000001"].wordCount == 4 assert theIndex._itemIndex[nHandle]["T0001"].charCount == 23
assert theIndex._itemIndex[nHandle]["T000007"].wordCount == 4 assert theIndex._itemIndex[nHandle]["T0002"].charCount == 23
assert theIndex._itemIndex[nHandle]["T000013"].wordCount == 4 assert theIndex._itemIndex[nHandle]["T0003"].charCount == 27
assert theIndex._itemIndex[nHandle]["T000019"].wordCount == 9 assert theIndex._itemIndex[nHandle]["T0004"].charCount == 56
assert theIndex._itemIndex[nHandle]["T000001"].paraCount == 1 assert theIndex._itemIndex[nHandle]["T0001"].wordCount == 4
assert theIndex._itemIndex[nHandle]["T000007"].paraCount == 1 assert theIndex._itemIndex[nHandle]["T0002"].wordCount == 4
assert theIndex._itemIndex[nHandle]["T000013"].paraCount == 1 assert theIndex._itemIndex[nHandle]["T0003"].wordCount == 4
assert theIndex._itemIndex[nHandle]["T000019"].paraCount == 3 assert theIndex._itemIndex[nHandle]["T0004"].wordCount == 9
assert theIndex._itemIndex[nHandle]["T000001"].synopsis == "Synopsis One." assert theIndex._itemIndex[nHandle]["T0001"].paraCount == 1
assert theIndex._itemIndex[nHandle]["T000007"].synopsis == "Synopsis Two." assert theIndex._itemIndex[nHandle]["T0002"].paraCount == 1
assert theIndex._itemIndex[nHandle]["T000013"].synopsis == "Synopsis Three." assert theIndex._itemIndex[nHandle]["T0003"].paraCount == 1
assert theIndex._itemIndex[nHandle]["T000019"].synopsis == "Synopsis Four." assert theIndex._itemIndex[nHandle]["T0004"].paraCount == 3
assert theIndex._itemIndex[nHandle]["T0001"].synopsis == "Synopsis One."
assert theIndex._itemIndex[nHandle]["T0002"].synopsis == "Synopsis Two."
assert theIndex._itemIndex[nHandle]["T0003"].synopsis == "Synopsis Three."
assert theIndex._itemIndex[nHandle]["T0004"].synopsis == "Synopsis Four."
# Note File # Note File
assert theIndex.scanText(cHandle, ( assert theIndex.scanText(cHandle, (
@@ -411,13 +416,14 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
"% synopsis: Synopsis One.\n\n" "% synopsis: Synopsis One.\n\n"
"Paragraph One.\n\n" "Paragraph One.\n\n"
)) ))
assert theIndex._itemIndex[cHandle]["T000001"].references == {} assert theIndex._itemIndex[cHandle]["T0001"].references == {}
assert theIndex._itemIndex[cHandle]["T000001"].level == "H1" assert theIndex._itemIndex[cHandle]["T0001"].level == "H1"
assert theIndex._itemIndex[cHandle]["T000001"].title == "Title One" assert theIndex._itemIndex[cHandle]["T0001"].line == 1
assert theIndex._itemIndex[cHandle]["T000001"].charCount == 23 assert theIndex._itemIndex[cHandle]["T0001"].title == "Title One"
assert theIndex._itemIndex[cHandle]["T000001"].wordCount == 4 assert theIndex._itemIndex[cHandle]["T0001"].charCount == 23
assert theIndex._itemIndex[cHandle]["T000001"].paraCount == 1 assert theIndex._itemIndex[cHandle]["T0001"].wordCount == 4
assert theIndex._itemIndex[cHandle]["T000001"].synopsis == "Synopsis One." assert theIndex._itemIndex[cHandle]["T0001"].paraCount == 1
assert theIndex._itemIndex[cHandle]["T0001"].synopsis == "Synopsis One."
# Valid and Invalid References # Valid and Invalid References
assert theIndex.scanText(sHandle, ( assert theIndex.scanText(sHandle, (
@@ -428,7 +434,7 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
"% synopsis: Synopsis One.\n\n" "% synopsis: Synopsis One.\n\n"
"Paragraph One.\n\n" "Paragraph One.\n\n"
)) ))
assert theIndex._itemIndex[sHandle]["T000001"].references == { assert theIndex._itemIndex[sHandle]["T0001"].references == {
"One": {"@pov"}, "Two": {"@char"} "One": {"@pov"}, "Two": {"@char"}
} }
@@ -439,25 +445,27 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
"#! My Project\n\n" "#! My Project\n\n"
">> By Jane Doe <<\n\n" ">> By Jane Doe <<\n\n"
)) ))
assert theIndex._itemIndex[cHandle]["T000001"].references == {} assert theIndex._itemIndex[cHandle]["T0001"].references == {}
assert theIndex._itemIndex[tHandle]["T000001"].level == "H1" assert theIndex._itemIndex[tHandle]["T0001"].level == "H1"
assert theIndex._itemIndex[tHandle]["T000001"].title == "My Project" assert theIndex._itemIndex[tHandle]["T0001"].line == 1
assert theIndex._itemIndex[tHandle]["T000001"].charCount == 21 assert theIndex._itemIndex[tHandle]["T0001"].title == "My Project"
assert theIndex._itemIndex[tHandle]["T000001"].wordCount == 5 assert theIndex._itemIndex[tHandle]["T0001"].charCount == 21
assert theIndex._itemIndex[tHandle]["T000001"].paraCount == 1 assert theIndex._itemIndex[tHandle]["T0001"].wordCount == 5
assert theIndex._itemIndex[tHandle]["T000001"].synopsis == "" assert theIndex._itemIndex[tHandle]["T0001"].paraCount == 1
assert theIndex._itemIndex[tHandle]["T0001"].synopsis == ""
assert theIndex.scanText(tHandle, ( assert theIndex.scanText(tHandle, (
"##! Prologue\n\n" "##! Prologue\n\n"
"In the beginning there was time ...\n\n" "In the beginning there was time ...\n\n"
)) ))
assert theIndex._itemIndex[cHandle]["T000001"].references == {} assert theIndex._itemIndex[cHandle]["T0001"].references == {}
assert theIndex._itemIndex[tHandle]["T000001"].level == "H2" assert theIndex._itemIndex[tHandle]["T0001"].level == "H2"
assert theIndex._itemIndex[tHandle]["T000001"].title == "Prologue" assert theIndex._itemIndex[tHandle]["T0001"].line == 1
assert theIndex._itemIndex[tHandle]["T000001"].charCount == 43 assert theIndex._itemIndex[tHandle]["T0001"].title == "Prologue"
assert theIndex._itemIndex[tHandle]["T000001"].wordCount == 8 assert theIndex._itemIndex[tHandle]["T0001"].charCount == 43
assert theIndex._itemIndex[tHandle]["T000001"].paraCount == 1 assert theIndex._itemIndex[tHandle]["T0001"].wordCount == 8
assert theIndex._itemIndex[tHandle]["T000001"].synopsis == "" assert theIndex._itemIndex[tHandle]["T0001"].paraCount == 1
assert theIndex._itemIndex[tHandle]["T0001"].synopsis == ""
# Page wo/Title # Page wo/Title
# ============= # =============
@@ -466,25 +474,27 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
assert theIndex.scanText(pHandle, ( assert theIndex.scanText(pHandle, (
"This is a page with some text on it.\n\n" "This is a page with some text on it.\n\n"
)) ))
assert theIndex._itemIndex[pHandle]["T000000"].references == {} assert theIndex._itemIndex[pHandle]["T0000"].references == {}
assert theIndex._itemIndex[pHandle]["T000000"].level == "H0" assert theIndex._itemIndex[pHandle]["T0000"].level == "H0"
assert theIndex._itemIndex[pHandle]["T000000"].title == "" assert theIndex._itemIndex[pHandle]["T0000"].line == 0
assert theIndex._itemIndex[pHandle]["T000000"].charCount == 36 assert theIndex._itemIndex[pHandle]["T0000"].title == ""
assert theIndex._itemIndex[pHandle]["T000000"].wordCount == 9 assert theIndex._itemIndex[pHandle]["T0000"].charCount == 36
assert theIndex._itemIndex[pHandle]["T000000"].paraCount == 1 assert theIndex._itemIndex[pHandle]["T0000"].wordCount == 9
assert theIndex._itemIndex[pHandle]["T000000"].synopsis == "" assert theIndex._itemIndex[pHandle]["T0000"].paraCount == 1
assert theIndex._itemIndex[pHandle]["T0000"].synopsis == ""
theProject.tree[pHandle]._layout = nwItemLayout.NOTE theProject.tree[pHandle]._layout = nwItemLayout.NOTE
assert theIndex.scanText(pHandle, ( assert theIndex.scanText(pHandle, (
"This is a page with some text on it.\n\n" "This is a page with some text on it.\n\n"
)) ))
assert theIndex._itemIndex[pHandle]["T000000"].references == {} assert theIndex._itemIndex[pHandle]["T0000"].references == {}
assert theIndex._itemIndex[pHandle]["T000000"].level == "H0" assert theIndex._itemIndex[pHandle]["T0000"].level == "H0"
assert theIndex._itemIndex[pHandle]["T000000"].title == "" assert theIndex._itemIndex[pHandle]["T0000"].line == 0
assert theIndex._itemIndex[pHandle]["T000000"].charCount == 36 assert theIndex._itemIndex[pHandle]["T0000"].title == ""
assert theIndex._itemIndex[pHandle]["T000000"].wordCount == 9 assert theIndex._itemIndex[pHandle]["T0000"].charCount == 36
assert theIndex._itemIndex[pHandle]["T000000"].paraCount == 1 assert theIndex._itemIndex[pHandle]["T0000"].wordCount == 9
assert theIndex._itemIndex[pHandle]["T000000"].synopsis == "" assert theIndex._itemIndex[pHandle]["T0000"].paraCount == 1
assert theIndex._itemIndex[pHandle]["T0000"].synopsis == ""
assert theProject.closeProject() is True assert theProject.closeProject() is True
@@ -512,8 +522,8 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
nHandle = theProject.newFile("Hello", C.hNovelRoot) nHandle = theProject.newFile("Hello", C.hNovelRoot)
cHandle = theProject.newFile("Jane", C.hCharRoot) cHandle = theProject.newFile("Jane", C.hCharRoot)
assert theIndex.getNovelData("", "") is None assert theIndex.getItemHeader("", "") is None
assert theIndex.getNovelData(C.hNovelRoot, "") is None assert theIndex.getItemHeader(C.hNovelRoot, "") is None
assert theIndex.scanText(cHandle, ( assert theIndex.scanText(cHandle, (
"# Jane Smith\n" "# Jane Smith\n"
@@ -534,10 +544,10 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
theKeys.append(aKey) theKeys.append(aKey)
assert theKeys == [ assert theKeys == [
f"{C.hTitlePage}:T000001", f"{C.hTitlePage}:T0001",
f"{C.hChapterDoc}:T000001", f"{C.hChapterDoc}:T0001",
f"{C.hSceneDoc}:T000001", f"{C.hSceneDoc}:T0001",
f"{nHandle}:T000001", f"{nHandle}:T0001",
] ]
# Check that excluded files can be skipped # Check that excluded files can be skipped
@@ -548,10 +558,10 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
theKeys.append(aKey) theKeys.append(aKey)
assert theKeys == [ assert theKeys == [
f"{C.hTitlePage}:T000001", f"{C.hTitlePage}:T0001",
f"{C.hChapterDoc}:T000001", f"{C.hChapterDoc}:T0001",
f"{C.hSceneDoc}:T000001", f"{C.hSceneDoc}:T0001",
f"{nHandle}:T000001", f"{nHandle}:T0001",
] ]
theKeys = [] theKeys = []
@@ -559,9 +569,9 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
theKeys.append(aKey) theKeys.append(aKey)
assert theKeys == [ assert theKeys == [
f"{C.hTitlePage}:T000001", f"{C.hTitlePage}:T0001",
f"{C.hChapterDoc}:T000001", f"{C.hChapterDoc}:T0001",
f"{C.hSceneDoc}:T000001", f"{C.hSceneDoc}:T0001",
] ]
# The novel file should have the correct counts # The novel file should have the correct counts
@@ -570,6 +580,14 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
assert wC == 12 # Words in text and title only assert wC == 12 # Words in text and title only
assert pC == 2 # Paragraphs in text only assert pC == 2 # Paragraphs in text only
# getItemData + getHandleHeaderCount
# ==================================
theItem = theIndex.getItemData(nHandle)
assert isinstance(theItem, IndexItem)
assert theItem.headings() == ["T0001"]
assert theIndex.getHandleHeaderCount(nHandle) == 1
# getReferences # getReferences
# ============= # =============
@@ -594,13 +612,13 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
# The character file should have a record of the reference from the novel file # The character file should have a record of the reference from the novel file
theRefs = theIndex.getBackReferenceList(cHandle) theRefs = theIndex.getBackReferenceList(cHandle)
assert theRefs == {nHandle: "T000001"} assert theRefs == {nHandle: "T0001"}
# getTagSource # getTagSource
# ============ # ============
assert theIndex.getTagSource("Jane") == (cHandle, "T000001") assert theIndex.getTagSource("Jane") == (cHandle, "T0001")
assert theIndex.getTagSource("John") == (None, "T000000") assert theIndex.getTagSource("John") == (None, "T0000")
# getCounts # getCounts
# ========= # =========
@@ -632,13 +650,13 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
assert pC == 4 assert pC == 4
# First part # First part
cC, wC, pC = theIndex.getCounts(nHandle, "T000001") cC, wC, pC = theIndex.getCounts(nHandle, "T0001")
assert cC == 62 assert cC == 62
assert wC == 12 assert wC == 12
assert pC == 2 assert pC == 2
# Second part # Second part
cC, wC, pC = theIndex.getCounts(nHandle, "T000011") cC, wC, pC = theIndex.getCounts(nHandle, "T0002")
assert cC == 90 assert cC == 90
assert wC == 16 assert wC == 16
assert pC == 2 assert pC == 2
@@ -665,13 +683,13 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
assert pC == 4 assert pC == 4
# First part # First part
cC, wC, pC = theIndex.getCounts(cHandle, "T000001") cC, wC, pC = theIndex.getCounts(cHandle, "T0001")
assert cC == 62 assert cC == 62
assert wC == 12 assert wC == 12
assert pC == 2 assert pC == 2
# Second part # Second part
cC, wC, pC = theIndex.getCounts(cHandle, "T000011") cC, wC, pC = theIndex.getCounts(cHandle, "T0002")
assert cC == 90 assert cC == 90
assert wC == 16 assert wC == 16
assert pC == 2 assert pC == 2
@@ -692,36 +710,36 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
assert theIndex.scanText(tHandle, "### Scene Two\n\n") assert theIndex.scanText(tHandle, "### Scene Two\n\n")
assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [ assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [
(C.hTitlePage, "T000001"), (C.hTitlePage, "T0001"),
(C.hChapterDoc, "T000001"), (C.hChapterDoc, "T0001"),
(C.hSceneDoc, "T000001"), (C.hSceneDoc, "T0001"),
(nHandle, "T000001"), (nHandle, "T0001"),
(nHandle, "T000011"), (nHandle, "T0002"),
(hHandle, "T000001"), (hHandle, "T0001"),
(sHandle, "T000001"), (sHandle, "T0001"),
(tHandle, "T000001"), (tHandle, "T0001"),
] ]
assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=True)] == [ assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=True)] == [
(C.hTitlePage, "T000001"), (C.hTitlePage, "T0001"),
(C.hChapterDoc, "T000001"), (C.hChapterDoc, "T0001"),
(C.hSceneDoc, "T000001"), (C.hSceneDoc, "T0001"),
(hHandle, "T000001"), (hHandle, "T0001"),
(sHandle, "T000001"), (sHandle, "T0001"),
(tHandle, "T000001"), (tHandle, "T0001"),
] ]
# Add a fake handle to the tree and check that it's ignored # Add a fake handle to the tree and check that it's ignored
theProject.tree._treeOrder.append("0000000000000") theProject.tree._treeOrder.append("0000000000000")
assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [ assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [
(C.hTitlePage, "T000001"), (C.hTitlePage, "T0001"),
(C.hChapterDoc, "T000001"), (C.hChapterDoc, "T0001"),
(C.hSceneDoc, "T000001"), (C.hSceneDoc, "T0001"),
(nHandle, "T000001"), (nHandle, "T0001"),
(nHandle, "T000011"), (nHandle, "T0002"),
(hHandle, "T000001"), (hHandle, "T0001"),
(sHandle, "T000001"), (sHandle, "T0001"),
(tHandle, "T000001"), (tHandle, "T0001"),
] ]
theProject.tree._treeOrder.remove("0000000000000") theProject.tree._treeOrder.remove("0000000000000")
@@ -734,52 +752,31 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
# Table of Contents # Table of Contents
assert theIndex.getTableOfContents(C.hNovelRoot, 0, skipExcl=True) == [] assert theIndex.getTableOfContents(C.hNovelRoot, 0, skipExcl=True) == []
assert theIndex.getTableOfContents(C.hNovelRoot, 1, skipExcl=True) == [ assert theIndex.getTableOfContents(C.hNovelRoot, 1, skipExcl=True) == [
(f"{C.hTitlePage}:T000001", 1, "New Novel", 15), (f"{C.hTitlePage}:T0001", 1, "New Novel", 15),
] ]
assert theIndex.getTableOfContents(C.hNovelRoot, 2, skipExcl=True) == [ assert theIndex.getTableOfContents(C.hNovelRoot, 2, skipExcl=True) == [
(f"{C.hTitlePage}:T000001", 1, "New Novel", 5), (f"{C.hTitlePage}:T0001", 1, "New Novel", 5),
(f"{C.hChapterDoc}:T000001", 2, "New Chapter", 4), (f"{C.hChapterDoc}:T0001", 2, "New Chapter", 4),
(f"{hHandle}:T000001", 2, "Chapter One", 6), (f"{hHandle}:T0001", 2, "Chapter One", 6),
] ]
assert theIndex.getTableOfContents(C.hNovelRoot, 3, skipExcl=True) == [ assert theIndex.getTableOfContents(C.hNovelRoot, 3, skipExcl=True) == [
(f"{C.hTitlePage}:T000001", 1, "New Novel", 5), (f"{C.hTitlePage}:T0001", 1, "New Novel", 5),
(f"{C.hChapterDoc}:T000001", 2, "New Chapter", 2), (f"{C.hChapterDoc}:T0001", 2, "New Chapter", 2),
(f"{C.hSceneDoc}:T000001", 3, "New Scene", 2), (f"{C.hSceneDoc}:T0001", 3, "New Scene", 2),
(f"{hHandle}:T000001", 2, "Chapter One", 2), (f"{hHandle}:T0001", 2, "Chapter One", 2),
(f"{sHandle}:T000001", 3, "Scene One", 2), (f"{sHandle}:T0001", 3, "Scene One", 2),
(f"{tHandle}:T000001", 3, "Scene Two", 2), (f"{tHandle}:T0001", 3, "Scene Two", 2),
] ]
assert theIndex.getTableOfContents(C.hNovelRoot, 0, skipExcl=False) == [] assert theIndex.getTableOfContents(C.hNovelRoot, 0, skipExcl=False) == []
assert theIndex.getTableOfContents(C.hNovelRoot, 1, skipExcl=False) == [ assert theIndex.getTableOfContents(C.hNovelRoot, 1, skipExcl=False) == [
(f"{C.hTitlePage}:T000001", 1, "New Novel", 9), (f"{C.hTitlePage}:T0001", 1, "New Novel", 9),
(f"{nHandle}:T000001", 1, "Hello World!", 12), (f"{nHandle}:T0001", 1, "Hello World!", 12),
(f"{nHandle}:T000011", 1, "Hello World!", 22), (f"{nHandle}:T0002", 1, "Hello World!", 22),
]
# Header Word Counts
bHandle = "0000000000000"
assert theIndex.getHandleWordCounts(bHandle) == []
assert theIndex.getHandleWordCounts(hHandle) == [("%s:T000001" % hHandle, 2)]
assert theIndex.getHandleWordCounts(sHandle) == [("%s:T000001" % sHandle, 2)]
assert theIndex.getHandleWordCounts(tHandle) == [("%s:T000001" % tHandle, 2)]
assert theIndex.getHandleWordCounts(nHandle) == [
(f"{nHandle}:T000001", 12), (f"{nHandle}:T000011", 16)
] ]
assert theIndex.saveIndex() is True assert theIndex.saveIndex() is True
assert theProject.saveProject() is True assert theProject.saveProject() is True
# Header Record
bHandle = "0000000000000"
assert theIndex.getHandleHeaders(bHandle) == []
assert theIndex.getHandleHeaders(hHandle) == [("T000001", "H2", "Chapter One")]
assert theIndex.getHandleHeaders(sHandle) == [("T000001", "H3", "Scene One")]
assert theIndex.getHandleHeaders(tHandle) == [("T000001", "H3", "Scene Two")]
assert theIndex.getHandleHeaders(nHandle) == [
("T000001", "H1", "Hello World!"), ("T000011", "H1", "Hello World!")
]
assert theProject.closeProject() is True assert theProject.closeProject() is True
# END Test testCoreIndex_ExtractData # END Test testCoreIndex_ExtractData
@@ -796,25 +793,25 @@ def testCoreIndex_TagsIndex():
content = { content = {
"Tag1": { "Tag1": {
"handle": "0000000000001", "handle": "0000000000001",
"heading": "T000001", "heading": "T0001",
"class": nwItemClass.NOVEL.name, "class": nwItemClass.NOVEL.name,
}, },
"Tag2": { "Tag2": {
"handle": "0000000000002", "handle": "0000000000002",
"heading": "T000002", "heading": "T0002",
"class": nwItemClass.CHARACTER.name, "class": nwItemClass.CHARACTER.name,
}, },
"Tag3": { "Tag3": {
"handle": "0000000000003", "handle": "0000000000003",
"heading": "T000003", "heading": "T0003",
"class": nwItemClass.PLOT.name, "class": nwItemClass.PLOT.name,
}, },
} }
# Add data # Add data
tagsIndex.add("Tag1", "0000000000001", "T000001", nwItemClass.NOVEL) tagsIndex.add("Tag1", "0000000000001", "T0001", nwItemClass.NOVEL)
tagsIndex.add("Tag2", "0000000000002", "T000002", nwItemClass.CHARACTER) tagsIndex.add("Tag2", "0000000000002", "T0002", nwItemClass.CHARACTER)
tagsIndex.add("Tag3", "0000000000003", "T000003", nwItemClass.PLOT) tagsIndex.add("Tag3", "0000000000003", "T0003", nwItemClass.PLOT)
assert tagsIndex._tags == content assert tagsIndex._tags == content
# Get items # Get items
@@ -836,10 +833,10 @@ def testCoreIndex_TagsIndex():
assert tagsIndex.tagHandle("Tag4") is None assert tagsIndex.tagHandle("Tag4") is None
# Read back headings # Read back headings
assert tagsIndex.tagHeading("Tag1") == "T000001" assert tagsIndex.tagHeading("Tag1") == "T0001"
assert tagsIndex.tagHeading("Tag2") == "T000002" assert tagsIndex.tagHeading("Tag2") == "T0002"
assert tagsIndex.tagHeading("Tag3") == "T000003" assert tagsIndex.tagHeading("Tag3") == "T0003"
assert tagsIndex.tagHeading("Tag4") == "T000000" assert tagsIndex.tagHeading("Tag4") == "T0000"
# Read back classes # Read back classes
assert tagsIndex.tagClass("Tag1") == nwItemClass.NOVEL.name assert tagsIndex.tagClass("Tag1") == nwItemClass.NOVEL.name
@@ -880,7 +877,7 @@ def testCoreIndex_TagsIndex():
tagsIndex.unpackData({ tagsIndex.unpackData({
1234: { 1234: {
"handle": "0000000000001", "handle": "0000000000001",
"heading": "T000001", "heading": "T0001",
"class": "NOVEL", "class": "NOVEL",
} }
}) })
@@ -889,7 +886,7 @@ def testCoreIndex_TagsIndex():
with pytest.raises(KeyError): with pytest.raises(KeyError):
tagsIndex.unpackData({ tagsIndex.unpackData({
"Tag1": { "Tag1": {
"heading": "T000001", "heading": "T0001",
"class": "NOVEL", "class": "NOVEL",
} }
}) })
@@ -908,7 +905,7 @@ def testCoreIndex_TagsIndex():
tagsIndex.unpackData({ tagsIndex.unpackData({
"Tag1": { "Tag1": {
"handle": "0000000000001", "handle": "0000000000001",
"heading": "T000001", "heading": "T0001",
} }
}) })
@@ -917,7 +914,7 @@ def testCoreIndex_TagsIndex():
tagsIndex.unpackData({ tagsIndex.unpackData({
"Tag1": { "Tag1": {
"handle": "blablabla", "handle": "blablabla",
"heading": "T000001", "heading": "T0001",
"class": "NOVEL", "class": "NOVEL",
} }
}) })
@@ -937,7 +934,7 @@ def testCoreIndex_TagsIndex():
tagsIndex.unpackData({ tagsIndex.unpackData({
"Tag1": { "Tag1": {
"handle": "0000000000001", "handle": "0000000000001",
"heading": "T000001", "heading": "T0001",
"class": "blabla", "class": "blabla",
} }
}) })
@@ -975,66 +972,70 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
assert cHandle in itemIndex assert cHandle in itemIndex
assert itemIndex[cHandle].item == theProject.tree[cHandle] assert itemIndex[cHandle].item == theProject.tree[cHandle]
assert itemIndex.allItemTags(cHandle) == [] assert itemIndex.allItemTags(cHandle) == []
assert list(itemIndex.iterItemHeaders(cHandle))[0][0] == "T000000" assert list(itemIndex.iterItemHeaders(cHandle))[0][0] == "T0000"
# Add a heading to the item, which should replace the T000000 heading # Add a heading to the item, which should replace the T000000 heading
itemIndex.addItemHeading(cHandle, "T000001", "H2", "Chapter One") assert itemIndex.addItemHeading(cHandle, 1, "H2", "Chapter One") == "T0001"
assert list(itemIndex.iterItemHeaders(cHandle))[0][0] == "T000001" assert list(itemIndex.iterItemHeaders(cHandle))[0][0] == "T0001"
# Add a heading to an invalid item
assert itemIndex.addItemHeading(C.hInvalid, 1, "H1", "Stuff") == "T0000"
# Set the remainig data values # Set the remainig data values
itemIndex.setHeadingCounts(cHandle, "T000001", 60, 10, 2) itemIndex.setHeadingCounts(cHandle, "T0001", 60, 10, 2)
itemIndex.setHeadingSynopsis(cHandle, "T000001", "In the beginning ...") itemIndex.setHeadingSynopsis(cHandle, "T0001", "In the beginning ...")
itemIndex.setHeadingTag(cHandle, "T000001", "One") itemIndex.setHeadingTag(cHandle, "T0001", "One")
itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane"], "@pov") itemIndex.addHeadingReferences(cHandle, "T0001", ["Jane"], "@pov")
itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane"], "@focus") itemIndex.addHeadingReferences(cHandle, "T0001", ["Jane"], "@focus")
itemIndex.addHeadingReferences(cHandle, "T000001", ["Jane", "John"], "@char") itemIndex.addHeadingReferences(cHandle, "T0001", ["Jane", "John"], "@char")
idxData = itemIndex.packData() idxData = itemIndex.packData()
assert idxData[cHandle]["headings"]["T000001"] == { assert idxData[cHandle]["headings"]["T0001"] == {
"level": "H2", "title": "Chapter One", "tag": "One", "level": "H2", "line": 1, "title": "Chapter One", "tag": "One",
"cCount": 60, "wCount": 10, "pCount": 2, "synopsis": "In the beginning ...", "cCount": 60, "wCount": 10, "pCount": 2, "synopsis": "In the beginning ...",
} }
assert "@pov" in idxData[cHandle]["references"]["T000001"]["Jane"] assert "@pov" in idxData[cHandle]["references"]["T0001"]["Jane"]
assert "@focus" in idxData[cHandle]["references"]["T000001"]["Jane"] assert "@focus" in idxData[cHandle]["references"]["T0001"]["Jane"]
assert "@char" in idxData[cHandle]["references"]["T000001"]["Jane"] assert "@char" in idxData[cHandle]["references"]["T0001"]["Jane"]
assert "@char" in idxData[cHandle]["references"]["T000001"]["John"] assert "@char" in idxData[cHandle]["references"]["T0001"]["John"]
# Add the other two files # Add the other two files
itemIndex.add(nHandle, theProject.tree[nHandle]) itemIndex.add(nHandle, theProject.tree[nHandle])
itemIndex.add(sHandle, theProject.tree[sHandle]) itemIndex.add(sHandle, theProject.tree[sHandle])
itemIndex.addItemHeading(nHandle, "T000001", "H1", "Novel") itemIndex.addItemHeading(nHandle, 1, "H1", "Novel")
itemIndex.addItemHeading(sHandle, "T000001", "H3", "Scene One") itemIndex.addItemHeading(sHandle, 1, "H3", "Scene One")
# Check Item and Heading Direct Access # Check Item and Heading Direct Access
# ==================================== # ====================================
# Check repr strings # Check repr strings
assert repr(itemIndex[nHandle]) == f"<IndexItem handle='{nHandle}'>" assert repr(itemIndex[nHandle]) == f"<IndexItem handle='{nHandle}'>"
assert repr(itemIndex[nHandle]["T000001"]) == "<IndexHeading key='T000001'>" assert repr(itemIndex[nHandle]["T0001"]) == "<IndexHeading key='T0001'>"
# Check content of a single item # Check content of a single item
assert "T000001" in itemIndex[nHandle] assert "T0001" in itemIndex[nHandle]
assert itemIndex[cHandle].allTags() == ["One"] assert itemIndex[cHandle].allTags() == ["One"]
# Check the content of a single heading # Check the content of a single heading
assert itemIndex[cHandle]["T000001"].key == "T000001" assert itemIndex[cHandle]["T0001"].key == "T0001"
assert itemIndex[cHandle]["T000001"].level == "H2" assert itemIndex[cHandle]["T0001"].level == "H2"
assert itemIndex[cHandle]["T000001"].title == "Chapter One" assert itemIndex[cHandle]["T0001"].line == 1
assert itemIndex[cHandle]["T000001"].tag == "One" assert itemIndex[cHandle]["T0001"].title == "Chapter One"
assert itemIndex[cHandle]["T000001"].charCount == 60 assert itemIndex[cHandle]["T0001"].tag == "One"
assert itemIndex[cHandle]["T000001"].wordCount == 10 assert itemIndex[cHandle]["T0001"].charCount == 60
assert itemIndex[cHandle]["T000001"].paraCount == 2 assert itemIndex[cHandle]["T0001"].wordCount == 10
assert itemIndex[cHandle]["T000001"].synopsis == "In the beginning ..." assert itemIndex[cHandle]["T0001"].paraCount == 2
assert "Jane" in itemIndex[cHandle]["T000001"].references assert itemIndex[cHandle]["T0001"].synopsis == "In the beginning ..."
assert "John" in itemIndex[cHandle]["T000001"].references assert "Jane" in itemIndex[cHandle]["T0001"].references
assert "John" in itemIndex[cHandle]["T0001"].references
# Check heading level setter # Check heading level setter
itemIndex[cHandle]["T000001"].setLevel("H3") # Change it itemIndex[cHandle]["T0001"].setLevel("H3") # Change it
assert itemIndex[cHandle]["T000001"].level == "H3" assert itemIndex[cHandle]["T0001"].level == "H3"
itemIndex[cHandle]["T000001"].setLevel("H2") # Set it back itemIndex[cHandle]["T0001"].setLevel("H2") # Set it back
assert itemIndex[cHandle]["T000001"].level == "H2" assert itemIndex[cHandle]["T0001"].level == "H2"
itemIndex[cHandle]["T000001"].setLevel("H5") # Invalid level itemIndex[cHandle]["T0001"].setLevel("H5") # Invalid level
assert itemIndex[cHandle]["T000001"].level == "H2" assert itemIndex[cHandle]["T0001"].level == "H2"
# Data Extraction # Data Extraction
# =============== # ===============
@@ -1044,9 +1045,9 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
assert allHeads[0][0] == cHandle assert allHeads[0][0] == cHandle
assert allHeads[1][0] == nHandle assert allHeads[1][0] == nHandle
assert allHeads[2][0] == sHandle assert allHeads[2][0] == sHandle
assert allHeads[0][1] == "T000001" assert allHeads[0][1] == "T0001"
assert allHeads[1][1] == "T000001" assert allHeads[1][1] == "T0001"
assert allHeads[2][1] == "T000001" assert allHeads[2][1] == "T0001"
# Ask for stuff that doesn't exist # Ask for stuff that doesn't exist
assert itemIndex.allItemTags("blablabla") == [] assert itemIndex.allItemTags("blablabla") == []
@@ -1058,7 +1059,7 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
mHandle = theProject.newRoot(nwItemClass.NOVEL) mHandle = theProject.newRoot(nwItemClass.NOVEL)
uHandle = theProject.newFile("Title Page", mHandle) uHandle = theProject.newFile("Title Page", mHandle)
itemIndex.add(uHandle, theProject.tree[uHandle]) itemIndex.add(uHandle, theProject.tree[uHandle])
itemIndex.addItemHeading(uHandle, "T000001", "H1", "Novel 2") itemIndex.addItemHeading(uHandle, "T0001", "H1", "Novel 2")
assert uHandle in itemIndex assert uHandle in itemIndex
# Structure of all novels # Structure of all novels
@@ -1134,20 +1135,20 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
# Reference without a heading should be rejected # Reference without a heading should be rejected
itemIndex.unpackData({ itemIndex.unpackData({
cHandle: { cHandle: {
"headings": {"T000001": {}}, "headings": {"T0001": {}},
"references": {"T000001": {}, "T000002": {}}, "references": {"T0001": {}, "T0002": {}},
} }
}) })
assert "T000001" in itemIndex[cHandle] assert "T0001" in itemIndex[cHandle]
assert "T000002" not in itemIndex[cHandle] assert "T0002" not in itemIndex[cHandle]
itemIndex.clear() itemIndex.clear()
# Tag keys must be strings # Tag keys must be strings
with pytest.raises(ValueError): with pytest.raises(ValueError):
itemIndex.unpackData({ itemIndex.unpackData({
cHandle: { cHandle: {
"headings": {"T000001": {}}, "headings": {"T0001": {}},
"references": {"T000001": {1234: "@pov"}}, "references": {"T0001": {1234: "@pov"}},
} }
}) })
@@ -1155,8 +1156,8 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
with pytest.raises(ValueError): with pytest.raises(ValueError):
itemIndex.unpackData({ itemIndex.unpackData({
cHandle: { cHandle: {
"headings": {"T000001": {}}, "headings": {"T0001": {}},
"references": {"T000001": {"John": []}}, "references": {"T0001": {"John": []}},
} }
}) })
@@ -1164,16 +1165,16 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
with pytest.raises(ValueError): with pytest.raises(ValueError):
itemIndex.unpackData({ itemIndex.unpackData({
cHandle: { cHandle: {
"headings": {"T000001": {}}, "headings": {"T0001": {}},
"references": {"T000001": {"John": "@pov,@char,@stuff"}}, "references": {"T0001": {"John": "@pov,@char,@stuff"}},
} }
}) })
# This should pass # This should pass
itemIndex.unpackData({ itemIndex.unpackData({
cHandle: { cHandle: {
"headings": {"T000001": {}}, "headings": {"T0001": {}},
"references": {"T000001": {"John": "@pov,@char"}}, "references": {"T0001": {"John": "@pov,@char"}},
} }
}) })
+8 -8
View File
@@ -118,10 +118,10 @@ def testGuiEditor_LoadText(qtbot, monkeypatch, caplog, nwGUI, projPath, ipsumTex
assert nwGUI.docEditor.loadText(C.hSceneDoc) is True assert nwGUI.docEditor.loadText(C.hSceneDoc) is True
assert nwGUI.docEditor._bigDoc is True assert nwGUI.docEditor._bigDoc is True
# Regular open, with line number # Regular open, with line number (1 indexed)
assert nwGUI.docEditor.loadText(C.hSceneDoc, tLine=4) is True assert nwGUI.docEditor.loadText(C.hSceneDoc, tLine=4) is True
cursPos = nwGUI.docEditor.getCursorPosition() cursPos = nwGUI.docEditor.getCursorPosition()
assert nwGUI.docEditor.document().findBlock(cursPos).blockNumber() == 4 assert nwGUI.docEditor.document().findBlock(cursPos).blockNumber() == 3
# Load empty document # Load empty document
nwGUI.docEditor.replaceText("") nwGUI.docEditor.replaceText("")
@@ -211,7 +211,7 @@ def testGuiEditor_MetaData(qtbot, nwGUI, projPath, mockRnd):
assert nwGUI.theProject.tree[C.hSceneDoc].cursorPos == 10 assert nwGUI.theProject.tree[C.hSceneDoc].cursorPos == 10
assert nwGUI.docEditor.setCursorLine(None) is False assert nwGUI.docEditor.setCursorLine(None) is False
assert nwGUI.docEditor.setCursorLine(2) is True assert nwGUI.docEditor.setCursorLine(3) is True
assert nwGUI.docEditor.getCursorPosition() == 15 assert nwGUI.docEditor.getCursorPosition() == 15
# Document Changed Signal # Document Changed Signal
@@ -510,7 +510,7 @@ def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd
theText = "### A Scene\n\n\n%s" % ipsumText[0] theText = "### A Scene\n\n\n%s" % ipsumText[0]
assert nwGUI.docEditor.replaceText(theText) is True assert nwGUI.docEditor.replaceText(theText) is True
assert nwGUI.docEditor.setCursorLine(2) assert nwGUI.docEditor.setCursorLine(3)
# Invalid Keyword # Invalid Keyword
assert nwGUI.docEditor.insertKeyWord("stuff") is False assert nwGUI.docEditor.insertKeyWord("stuff") is False
@@ -1049,10 +1049,10 @@ def testGuiEditor_BlockFormatting(qtbot, monkeypatch, nwGUI, projPath, ipsumText
assert nwGUI.docEditor.getText() == "Title\n\n" assert nwGUI.docEditor.getText() == "Title\n\n"
assert nwGUI.docEditor.getCursorPosition() == 5 assert nwGUI.docEditor.getCursorPosition() == 5
# Second Line # Third Line
# This also needs to add a new block # This also needs to add a new block
assert nwGUI.docEditor.replaceText("#### Title\n\nThe Text\n\n") is True assert nwGUI.docEditor.replaceText("#### Title\n\nThe Text\n\n") is True
assert nwGUI.docEditor.setCursorLine(2) is True assert nwGUI.docEditor.setCursorLine(3) is True
assert nwGUI.docEditor._formatBlock(nwDocAction.BLOCK_COM) is True assert nwGUI.docEditor._formatBlock(nwDocAction.BLOCK_COM) is True
assert nwGUI.docEditor.getText() == "#### Title\n\n% The Text\n\n" assert nwGUI.docEditor.getText() == "#### Title\n\n% The Text\n\n"
@@ -1086,11 +1086,11 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
assert nwGUI.openDocument(C.hSceneDoc) is True assert nwGUI.openDocument(C.hSceneDoc) is True
# Empty Block # Empty Block
assert nwGUI.docEditor.setCursorLine(1) is True assert nwGUI.docEditor.setCursorLine(2) is True
assert nwGUI.docEditor._followTag() is False assert nwGUI.docEditor._followTag() is False
# Not On Tag # Not On Tag
assert nwGUI.docEditor.setCursorLine(0) is True assert nwGUI.docEditor.setCursorLine(1) is True
assert nwGUI.docEditor._followTag() is False assert nwGUI.docEditor._followTag() is False
# On Tag Keyword # On Tag Keyword
+6 -11
View File
@@ -57,17 +57,10 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
nwGUI.docViewer.docHeader._refreshDocument() nwGUI.docViewer.docHeader._refreshDocument()
assert nwGUI.docViewer.toPlainText() == origText assert nwGUI.docViewer.toPlainText() == origText
# Cursor line
assert nwGUI.docViewer.setCursorLine("not a number") is False
assert nwGUI.docViewer.setCursorLine(3) is True
theCursor = nwGUI.docViewer.textCursor()
assert theCursor.position() == 40
# Cursor position
assert nwGUI.docViewer.setCursorPosition("not a number") is False
assert nwGUI.docViewer.setCursorPosition(100) is True
# Select word # Select word
theCursor = nwGUI.docViewer.textCursor()
theCursor.setPosition(100)
nwGUI.docViewer.setTextCursor(theCursor)
nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor) nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor)
qClip = qApp.clipboard() qClip = qApp.clipboard()
@@ -113,7 +106,9 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
nwGUI.mainMenu.aViewDoc.activate(QAction.Trigger) nwGUI.mainMenu.aViewDoc.activate(QAction.Trigger)
# Select "Bod" link # Select "Bod" link
assert nwGUI.docViewer.setCursorPosition(27) is True theCursor = nwGUI.docViewer.textCursor()
theCursor.setPosition(27)
nwGUI.docViewer.setTextCursor(theCursor)
nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor) nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor)
theRect = nwGUI.docViewer.cursorRect() theRect = nwGUI.docViewer.cursorRect()
# qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center(), delay=100) # qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center(), delay=100)
-12
View File
@@ -57,7 +57,6 @@ def testGuiMain_ProjectBlocker(nwGUI):
assert nwGUI.importDocument() is False assert nwGUI.importDocument() is False
assert nwGUI.openSelectedItem() is False assert nwGUI.openSelectedItem() is False
assert nwGUI.editItemLabel() is False assert nwGUI.editItemLabel() is False
assert nwGUI.requestNovelTreeRefresh() is False
assert nwGUI.rebuildIndex() is False assert nwGUI.rebuildIndex() is False
assert nwGUI.showProjectSettingsDialog() is False assert nwGUI.showProjectSettingsDialog() is False
assert nwGUI.showProjectDetailsDialog() is False assert nwGUI.showProjectDetailsDialog() is False
@@ -516,17 +515,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
assert nwGUI.saveProject() assert nwGUI.saveProject()
assert nwGUI.closeDocViewer() assert nwGUI.closeDocViewer()
# Check a Quick Create and Delete
assert nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None)
newHandle = nwGUI.projView.getSelectedHandle()
assert newHandle == "0000000000013"
assert nwGUI.theProject.tree[newHandle] is not None
assert nwGUI.projView.requestDeleteItem()
assert nwGUI.projView.setSelectedHandle(newHandle)
assert nwGUI.projView.requestDeleteItem()
assert nwGUI.theProject.tree["0000000000014"] is not None # Trash
assert nwGUI.saveProject()
# Check the files # Check the files
projFile = projPath / "nwProject.nwx" projFile = projPath / "nwProject.nwx"
testFile = tstPaths.outDir / "guiEditor_Main_Final_nwProject.nwx" testFile = tstPaths.outDir / "guiEditor_Main_Final_nwProject.nwx"
+16 -12
View File
@@ -92,7 +92,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
assert not topItem.isSelected() assert not topItem.isSelected()
topItem.setSelected(True) topItem.setSelected(True)
assert novelTree.selectedItems()[0] == topItem assert novelTree.selectedItems()[0] == topItem
assert novelView.getSelectedHandle() == (C.hTitlePage, 0) assert novelView.getSelectedHandle() == (C.hTitlePage, "T0001")
# Refresh using the slot for the butoom # Refresh using the slot for the butoom
novelBar._refreshNovelTree() novelBar._refreshNovelTree()
@@ -142,31 +142,35 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
novelBar.setLastColType(NovelTreeColumn.HIDDEN) novelBar.setLastColType(NovelTreeColumn.HIDDEN)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is True assert novelTree.isColumnHidden(novelTree.C_EXTRA) is True
assert novelTree.lastColType == NovelTreeColumn.HIDDEN assert novelTree.lastColType == NovelTreeColumn.HIDDEN
assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == ("", "") assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == ("", "")
novelBar.setLastColType(NovelTreeColumn.POV) novelBar.setLastColType(NovelTreeColumn.PLOT)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
assert novelTree.lastColType == NovelTreeColumn.POV assert novelTree.lastColType == NovelTreeColumn.PLOT
assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == ( assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == (
"Jane", "Point of View: Jane" "", ""
) )
novelBar.setLastColType(NovelTreeColumn.FOCUS) novelBar.setLastColType(NovelTreeColumn.FOCUS)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
assert novelTree.lastColType == NovelTreeColumn.FOCUS assert novelTree.lastColType == NovelTreeColumn.FOCUS
assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == ( assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == (
"Jane", "Focus: Jane" "Jane", "Focus: Jane"
) )
novelBar.setLastColType(NovelTreeColumn.PLOT) novelBar.setLastColType(NovelTreeColumn.POV)
assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False assert novelTree.isColumnHidden(novelTree.C_EXTRA) is False
assert novelTree.lastColType == NovelTreeColumn.PLOT assert novelTree.lastColType == NovelTreeColumn.POV
assert novelTree._getLastColumnText(C.hSceneDoc, "T000001") == ( assert novelTree._getLastColumnText(C.hSceneDoc, "T0001") == (
"", "Plot: " "Jane", "Point of View: Jane"
) )
novelTree._lastCol = None novelTree._lastCol = None
assert novelTree._getLastColumnText("0000000000000", "T000000") == ("", "") assert novelTree._getLastColumnText("0000000000000", "T0000") == ("", "")
# This forces the resizeEvent function to process labels
spSize = nwGUI.splitMain.sizes()
nwGUI.splitMain.setSizes([spSize[0] + 10, spSize[1] - 10])
# Item Meta # Item Meta
# ========= # =========
+4 -4
View File
@@ -225,9 +225,9 @@ def testGuiOutline_Content(qtbot, nwGUI, nwLipsum):
selItem = outlineTree.topLevelItem(4) selItem = outlineTree.topLevelItem(4)
outlineTree.setCurrentItem(selItem) outlineTree.setCurrentItem(selItem)
tHandle, tLine = outlineTree.getSelectedHandle() tHandle, sTitle = outlineTree.getSelectedHandle()
assert tHandle == "88243afbe5ed8" assert tHandle == "88243afbe5ed8"
assert tLine == 0 assert sTitle == "T0001"
assert outlineData.titleLabel.text() == "<b>Scene</b>" assert outlineData.titleLabel.text() == "<b>Scene</b>"
assert outlineData.titleValue.text() == "Scene One" assert outlineData.titleValue.text() == "Scene One"
@@ -243,9 +243,9 @@ def testGuiOutline_Content(qtbot, nwGUI, nwLipsum):
selItem = outlineTree.topLevelItem(5) selItem = outlineTree.topLevelItem(5)
outlineTree.setCurrentItem(selItem) outlineTree.setCurrentItem(selItem)
tHandle, tLine = outlineTree.getSelectedHandle() tHandle, sTitle = outlineTree.getSelectedHandle()
assert tHandle == "88243afbe5ed8" assert tHandle == "88243afbe5ed8"
assert tLine == 12 assert sTitle == "T0002"
assert outlineData.titleLabel.text() == "<b>Section</b>" assert outlineData.titleLabel.text() == "<b>Section</b>"
assert outlineData.titleValue.text() == "Scene One, Section Two" assert outlineData.titleValue.text() == "Scene One, Section Two"
+7 -7
View File
@@ -24,7 +24,7 @@ import pytest
from tools import C, buildTestProject from tools import C, buildTestProject
from novelwriter.enum import nwState from novelwriter.gui.statusbar import StatusLED
@pytest.mark.gui @pytest.mark.gui
@@ -44,19 +44,19 @@ def testGuiStatusBar_Main(qtbot, nwGUI, projPath, mockRnd):
assert nwGUI.mainStatus.refTime == refTime assert nwGUI.mainStatus.refTime == refTime
# Project Status # Project Status
nwGUI.mainStatus.setProjectStatus(nwState.NONE) nwGUI.mainStatus.setProjectStatus(StatusLED.S_NONE)
assert nwGUI.mainStatus.projIcon._theCol == nwGUI.mainStatus.projIcon._colNone assert nwGUI.mainStatus.projIcon._theCol == nwGUI.mainStatus.projIcon._colNone
nwGUI.mainStatus.setProjectStatus(nwState.BAD) nwGUI.mainStatus.setProjectStatus(StatusLED.S_BAD)
assert nwGUI.mainStatus.projIcon._theCol == nwGUI.mainStatus.projIcon._colBad assert nwGUI.mainStatus.projIcon._theCol == nwGUI.mainStatus.projIcon._colBad
nwGUI.mainStatus.setProjectStatus(nwState.GOOD) nwGUI.mainStatus.setProjectStatus(StatusLED.S_GOOD)
assert nwGUI.mainStatus.projIcon._theCol == nwGUI.mainStatus.projIcon._colGood assert nwGUI.mainStatus.projIcon._theCol == nwGUI.mainStatus.projIcon._colGood
# Document Status # Document Status
nwGUI.mainStatus.setDocumentStatus(nwState.NONE) nwGUI.mainStatus.setDocumentStatus(StatusLED.S_NONE)
assert nwGUI.mainStatus.docIcon._theCol == nwGUI.mainStatus.docIcon._colNone assert nwGUI.mainStatus.docIcon._theCol == nwGUI.mainStatus.docIcon._colNone
nwGUI.mainStatus.setDocumentStatus(nwState.BAD) nwGUI.mainStatus.setDocumentStatus(StatusLED.S_BAD)
assert nwGUI.mainStatus.docIcon._theCol == nwGUI.mainStatus.docIcon._colBad assert nwGUI.mainStatus.docIcon._theCol == nwGUI.mainStatus.docIcon._colBad
nwGUI.mainStatus.setDocumentStatus(nwState.GOOD) nwGUI.mainStatus.setDocumentStatus(StatusLED.S_GOOD)
assert nwGUI.mainStatus.docIcon._theCol == nwGUI.mainStatus.docIcon._colGood assert nwGUI.mainStatus.docIcon._theCol == nwGUI.mainStatus.docIcon._colGood
# Idle Status # Idle Status