Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
290cbc9a25 | ||
|
|
6565438bdf | ||
|
|
cd3c32b2ff | ||
|
|
8f15f22e85 | ||
|
|
08acebdd65 | ||
|
|
43ebcec4e9 | ||
|
|
aba339f8e3 | ||
|
|
25c92d8b2e | ||
|
|
fde7975a0b | ||
|
|
f38e770402 | ||
|
|
753b248b9f | ||
|
|
b9608dcdbd | ||
|
|
01dde7c8a4 | ||
|
|
a67d3ed599 | ||
|
|
2abacc1b2f | ||
|
|
aff81c9a78 | ||
|
|
4a98f58a20 | ||
|
|
3ae87b6cf4 | ||
|
|
15b94da2e6 | ||
|
|
a7b7666f0c | ||
|
|
45f813d98a | ||
|
|
070f2a3985 | ||
|
|
0d5899e844 | ||
|
|
dc12d518c0 | ||
|
|
9186289c1b | ||
|
|
68ddba1768 | ||
|
|
3419839295 | ||
|
|
e4b36a8646 | ||
|
|
558af9f329 | ||
|
|
0f67e9b971 | ||
|
|
9aeffdd333 | ||
|
|
1dc84c3aca | ||
|
|
598eaa8c3c | ||
|
|
4a4775c8db | ||
|
|
bbdf1aa8e1 | ||
|
|
1434c2bdf3 | ||
|
|
3aa07e9b33 | ||
|
|
e8de021abc | ||
|
|
39eae448b0 | ||
|
|
d9a8f860f0 | ||
|
|
b40e1bac4a | ||
|
|
d4ac76aa3a | ||
|
|
1275d6ef16 | ||
|
|
2ac8f371ec | ||
|
|
86911ace70 | ||
|
|
436aba4fd1 | ||
|
|
4980468a55 | ||
|
|
785a69d34b | ||
|
|
2fe001b336 | ||
|
|
50f4d8e073 | ||
|
|
70b3b24cd3 | ||
|
|
323013bbaf | ||
|
|
79116f8977 | ||
|
|
fe12e99735 | ||
|
|
77c6caacee | ||
|
|
6638c5187e | ||
|
|
897583ce8d | ||
|
|
bfd44ecae9 | ||
|
|
ba15efc715 | ||
|
|
6f04d91bf9 | ||
|
|
c0d52ce2f1 | ||
|
|
691717ce96 | ||
|
|
16401f4c1f |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,10 @@
|
||||
# Setup
|
||||
MANIFEST
|
||||
dist/
|
||||
deploy/
|
||||
|
||||
# Documentation
|
||||
docs/build/
|
||||
|
||||
# Python Temp
|
||||
__pycache__
|
||||
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,5 +1,32 @@
|
||||
# novelWriter ChangeLog
|
||||
|
||||
## Version 0.3.2 [2019-10-27]
|
||||
|
||||
**Documentation**
|
||||
|
||||
* The documentation has been rewritten and added to the Read the Docs website. Pressing `F1` or `Help > Documentation` in the menu, opens the novelWriter documentation page. PRs #68 and #69.
|
||||
|
||||
**User Interface**
|
||||
|
||||
* Filters have been added to the Timeline View windows so unused tags can be hidden, and it's possible to select only certain classes of tags to display. PRs #61 and #62
|
||||
* The dialog boxes for Timeline View and Session Log now remember the filter choices from previous instance for the same project. PR #62
|
||||
* When having a document open in the editor, text can be imported into it from a plain text file. No formatting conversion of the imported text is performed. That is up to the user. However, this allows for importing novelWriter documents from other projects or from a previous export, partially addressing request in issue #63. PR #65
|
||||
* The Export feature now includes exporting to LaTeX, which allows building PDFs with pdflatex or other tools. PR #73
|
||||
* Export of a novelWriter flavour markdown file is also supported. This file can be imported back in as-is, and almost completes an export-edit-import cycle. A split document into multiple files feature will be added soon. PR #73
|
||||
|
||||
## Version 0.3.1 [2019-10-20]
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
* The backup request dialog should pop up on any change to the project during the last session, not just on unsaved changes. PR #58
|
||||
* The regex that searches for words for the spell check highlighter was not including unicode characters, so it would underline parts of words using unicode characters even if the word was spelled correctly. PR #58
|
||||
* When having unsaved changes in an open document, while changing editor configuration options, the document would be reloaded from disk when the changes were applied. This means the unsaved changes were lost. The document is now saved before the editor is re-initialised. PR #58
|
||||
|
||||
**User Interface**
|
||||
|
||||
* Added a GUI to display the session log. The log has been around for a while, and records when a project is opened, when it's closed, and how many words were added or removed during the session. This information is now available in a small dialog under `Project > Session Log` in the main menu. PR #59
|
||||
* The export project feature now also exports the project to Markdown and HTML5 format. PR #57
|
||||
|
||||
## Version 0.3 [2019-10-19]
|
||||
|
||||
**User Interface**
|
||||
|
||||
106
README.md
106
README.md
@@ -2,15 +2,16 @@
|
||||
|
||||
[](https://travis-ci.com/vkbo/novelWriter)
|
||||
[](https://codecov.io/gh/vkbo/novelWriter)
|
||||
[](https://novelwriter.readthedocs.io/en/latest/?badge=latest)
|
||||
|
||||
novelWriter is a markdown-like text editor designed fro writing novels and larger projects of many smaller plain text documents.
|
||||
|
||||
It is under initial development, but is currently usable.
|
||||
The application is written with Python3 and PyQt5, and developed on Linux.
|
||||
It should work fine on other operating systems as well.
|
||||
It is under initial development, and not all planned features are included, but it is perfectly usable in the current state.
|
||||
|
||||
**Note:** This application is designed specifically for my own needs as I work on my projects.
|
||||
I will only add features as I need them. I am trying to make the application as general as possible within that limitation.
|
||||
The application is written in Python3 using Qt5 via PyQt5.
|
||||
It is developed on Linux, but it should in principle work fine on other operating systems as well as long as dependencies are met.
|
||||
|
||||
The documentation is available here: [novelwriter.readthedocs.io](https://novelwriter.readthedocs.io/).
|
||||
|
||||
The application can be started from the source folder with the command:
|
||||
```
|
||||
@@ -19,46 +20,13 @@ The application can be started from the source folder with the command:
|
||||
|
||||
It also takes a few parameters for debugging and such, which can be listed with the switch `--help`.
|
||||
|
||||
There are no launcher icons yet. Consult your operating system documentation for how to make those.
|
||||
I will add those later.
|
||||
|
||||
## Features
|
||||
|
||||
The file format for the written text is a format similar to markdown, but with a few extensions and a few omissions.
|
||||
Project meta data is stored as XML.
|
||||
The editor has syntax highlighting for the features it supports, and includes a set of different syntax highlighting themes.
|
||||
The GUI also has an optional dark theme in addition to the default system theme.
|
||||
|
||||
Themes can easily be added to the `nw/themes` folder. Have a look in the existing folders for examples.
|
||||
|
||||
Open documents and the project file itself is saved regularly on a timer if they have been altered.
|
||||
The status of this is indicated by two indicators in the right corner of the status bar. Unsaved changes are in yellow, and saved is indicated by green. The indicators are marked with `P` for project and `D` for document.
|
||||
Latest character count, word count, and paragraph count is shown next to these indicators in the status bar.
|
||||
The counts are updated regularly, but not as-you-type.
|
||||
|
||||
The tree view also shows the word count per file, and total counts for the folders.
|
||||
Next to the count is a status flag. When an item is selected, the panel below the tree view will show more details.
|
||||
A novel file can use the status to set how far along from new to draft to finished it is, and the status flag for other files can be used to indicate the importance of for instance plot elements, characters, locations, etc.
|
||||
These flags are meant to be customisable, bit the interface hasn't been added yet.
|
||||
|
||||
Next to the colour icon is a flag that indicates the class of the file or folder, that is `N` for Novel, `C` for character, and so on. The last flag indicates file type. This is spelled out in the details pane at the bottom, and can be edited by pressing `Ctrl+E` or from the menu.
|
||||
|
||||
The main documentation is available in the application in the Help menu or by pressing `F1`.
|
||||
|
||||
The core features of novelWriter are:
|
||||
|
||||
* A plain text editor with a minimal set of markdown formatting features, plus a few additional features like tags and comments. Tags and comments are not counted towards word counts.
|
||||
* Most features have keyboard shortcuts to speed up using the editor.
|
||||
* Files are organised in a tree view with no automatic ordering. The order of the files are as you set them, and are saved in the same order.
|
||||
* Files live in a set of root folders, and cannot be moved between them. These are currently `Novel`, `Character`, `Plot`, `World`, `Timeline`, `Object` and `Custom`. There can only be one of each, except for the `Custom` one.
|
||||
* Within the root folders files can be organised in subfolders. The folders will have no impact on the layout of the compiled novel when I get around to adding the export feature. They are simply there for your convenience. The order of the text files in the final novel is dictated purely from the order of entries in the tree.
|
||||
* Each file can have a file layout, which currently doesn't do anything other than indicate the purpose of the file. It will, however, be used to control the format and structure of the parts of the novel when the export features are added. Each chapter must have a chapter type file. The text of the chapter can be put in the same file, or in following scene files. The choice is yours. The entire book can also be written in a single file of type `Book`.
|
||||
* Documents can be either opened in the editor (left pane) or in view mode (right pane). That means two documents can be opened at once, making it easier to work on one while following the other.
|
||||
* When tags and references are set in the documents, a timeline window will show the relationship between the novel sections and the supporting files like character files, locations, plot elements, etc.
|
||||
There are no launcher icons yet.
|
||||
Consult your operating system documentation for how to make those.
|
||||
I will add those later, and would appreciate any assistance from people working on Windows and MacOS as I don't use either of those operating systems.
|
||||
|
||||
## Dependencies
|
||||
|
||||
For the apt package manager, the following Python3 packages are needed:
|
||||
For the apt package manager on Debian systems, the following Python3 packages are needed:
|
||||
|
||||
* `python3-pyqt5` for the GUI
|
||||
* `python3-pyqt5.qtsvg` may need to be installed separately
|
||||
@@ -76,6 +44,60 @@ pip install -r requirements.txt
|
||||
```
|
||||
in the application folder.
|
||||
|
||||
## Key Features
|
||||
|
||||
The text documents of novelWriter uses a format similar to markdown, but with a few extensions and a few omissions.
|
||||
Project meta data is stored as XML.
|
||||
|
||||
### Colour Themes
|
||||
|
||||
The editor has syntax highlighting for the features it supports, and includes a set of different syntax highlighting themes.
|
||||
The GUI also has an optional dark theme in addition to the default system theme.
|
||||
|
||||
Note that the dark theme may not render all elements of the GUI as dark colours if you are running an early version of Qt5.
|
||||
This is not due to a bug in novelWriter, but due to the fact that the the styling options in the Qt API in some versions were incomplete.
|
||||
|
||||
New themes can easily be added to the `nw/themes` folder.
|
||||
Have a look in the existing folders for examples of how to define the colours.
|
||||
|
||||
### Auto-Saving and Document Stats
|
||||
|
||||
Open documents and the project file itself is saved regularly on a timer if they have been altered.
|
||||
The status of this is indicated by two indicators in the right corner of the status bar.
|
||||
Unsaved changes are in yellow, and saved is indicated by green.
|
||||
Latest character count, word count, and paragraph count is shown next to these indicators in the status bar.
|
||||
The counts are updated regularly, but not as-you-type.
|
||||
|
||||
### Easy Organising of Project Files
|
||||
|
||||
The structure of the project is shown on the left hand side of the main GUI.
|
||||
Project files are organised into root folders, indicating what class of file they are.
|
||||
The most important root folder is the Novel folder, which contains all of the files that makes up the finished novel.
|
||||
Each root folder can have subfolders.
|
||||
Folders have no impact on the project structure, they are purely tools for organising the files in whatever way the user needs.
|
||||
|
||||
The editor supports four levels of headings, which determines what level the following text belongs to.
|
||||
Headings of level one signify a book or partition title.
|
||||
Headings of level two signify the start of a new chapter.
|
||||
Headings of level three signify the start of a new scene.
|
||||
Headings of level four can be used internally in each scene to separate sections.
|
||||
|
||||
Each novel file can be assigned a layout format, which shows up as a flag next to the item in the tree view.
|
||||
These are mostly to help the user see what they contain, but some of them have impact on the format of the exported document.
|
||||
See the documentation for further details.
|
||||
|
||||
#### Project Notes
|
||||
|
||||
Supporting note files can be added for the story plot, characters, locations, story timeline, etc.
|
||||
These have their separate root folders.
|
||||
These are optional files.
|
||||
|
||||
### Visualisation of Story Elements
|
||||
|
||||
The different notes can be assigned tags, which the novel files can refer back to using special meta keywords.
|
||||
Currently, this information can be used to display a Timeline View of the story, showing where each scene connects to the plot, and which characters, etc. occur in them.
|
||||
Further features using this meta data will be added in the future.
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
19
docs/Makefile
Normal file
19
docs/Makefile
Normal file
@@ -0,0 +1,19 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@@ -0,0 +1,35 @@
|
||||
@ECHO OFF
|
||||
|
||||
pushd %~dp0
|
||||
|
||||
REM Command file for Sphinx documentation
|
||||
|
||||
if "%SPHINXBUILD%" == "" (
|
||||
set SPHINXBUILD=sphinx-build
|
||||
)
|
||||
set SOURCEDIR=source
|
||||
set BUILDDIR=build
|
||||
|
||||
if "%1" == "" goto help
|
||||
|
||||
%SPHINXBUILD% >NUL 2>NUL
|
||||
if errorlevel 9009 (
|
||||
echo.
|
||||
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||
echo.may add the Sphinx directory to PATH.
|
||||
echo.
|
||||
echo.If you don't have Sphinx installed, grab it from
|
||||
echo.http://sphinx-doc.org/
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
goto end
|
||||
|
||||
:help
|
||||
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||
|
||||
:end
|
||||
popd
|
||||
173
docs/source/conf.py
Normal file
173
docs/source/conf.py
Normal file
@@ -0,0 +1,173 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file does only contain a selection of the most common options. For a
|
||||
# full list see the documentation:
|
||||
# http://www.sphinx-doc.org/en/master/config
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath("."))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "novelWriter"
|
||||
copyright = "2018-2019, Veronica Berglyd Olsen"
|
||||
author = "Veronica Berglyd Olsen"
|
||||
|
||||
# The short X.Y version
|
||||
version = "0.3.1"
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = "0.3.1"
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = "1.0"
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named "sphinx.ext.*") or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = [".rst", ".md"]
|
||||
source_suffix = ".txt"
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = "index"
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = []
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = None
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = "default"
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ["_static"]
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# The default sidebars (for documents that don"t match any pattern) are
|
||||
# defined by theme itself. Builtin themes are using these templates by
|
||||
# default: ``["localtoc.html", "relations.html", "sourcelink.html",
|
||||
# "searchbox.html"]``.
|
||||
#
|
||||
# html_sidebars = {}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ---------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = "novelWriterdoc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ("letterpaper" or "a4paper").
|
||||
#
|
||||
# "papersize": "letterpaper",
|
||||
|
||||
# The font size ("10pt", "11pt" or "12pt").
|
||||
#
|
||||
# "pointsize": "10pt",
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# "preamble": "",
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# "figure_align": "htbp",
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, "novelWriter.tex", "novelWriter Documentation",
|
||||
"Veronica Berglyd Olsen", "manual"),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, "novelwriter", "novelWriter Documentation",
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output ----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, "novelWriter", "novelWriter Documentation",
|
||||
author, "novelWriter", "Markdown-like editor for novels.",
|
||||
"Miscellaneous"),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Epub output -------------------------------------------------
|
||||
|
||||
# Bibliographic Dublin Core info.
|
||||
epub_title = project
|
||||
|
||||
# The unique identifier of the text. This can be a ISBN number
|
||||
# or the project homepage.
|
||||
#
|
||||
# epub_identifier = ""
|
||||
|
||||
# A unique identification for the text.
|
||||
#
|
||||
# epub_uid = ""
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ["search.html"]
|
||||
91
docs/source/export.txt
Normal file
91
docs/source/export.txt
Normal file
@@ -0,0 +1,91 @@
|
||||
##################
|
||||
Exporting Projects
|
||||
##################
|
||||
|
||||
The novelWriter project can be exported in various formats using the export tool available from :menuselection:`Project --> Export Project` or by pressing :kbd:`F5`.
|
||||
|
||||
**************
|
||||
File Selection
|
||||
**************
|
||||
|
||||
Which files are selected for export can be controlled from the "Selection" section.
|
||||
The check box for "Novel files" will select any file that isn't classified as a note.
|
||||
This is useful when exporting a document containing the novel itself.
|
||||
|
||||
It is also possible to select the "Note files" for export, with or without the "Novel files".
|
||||
Comments can also optionally be included in the export for those formats that supports this.
|
||||
|
||||
**************
|
||||
Export Formats
|
||||
**************
|
||||
|
||||
Currently, five formats are supported for exporting.
|
||||
|
||||
novelWriter Markdown
|
||||
====================
|
||||
|
||||
This is simply a concatenation of the files selected by the filters.
|
||||
The files in the project are stacked together in the order they appear in the tree view, with comments, tags, etc. included.
|
||||
This is a useful format for exporting the project for later import back into novelWriter.
|
||||
|
||||
Plain Text
|
||||
==========
|
||||
|
||||
The plain text export format writes a simple ``.txt`` file without any formatting at all.
|
||||
Word-wrapping to a fixed column width is supported.
|
||||
It does, however, respect the centering of text if the :guilabel:`Fixed width` setting is larger than ``0``.
|
||||
|
||||
Markdown
|
||||
========
|
||||
|
||||
The markdown export formats writes a file using the valid markdown formats used by novelWriter, and converts comments to preformatted blocks by indenting it with four spaces.
|
||||
Word-wrapping to a fixed column width is supported.
|
||||
|
||||
HTML5 Document
|
||||
==============
|
||||
|
||||
The html5 export format writes a single ``.htm`` file with a minimal style settings block.
|
||||
The style setting block sets a maximum page width of ``768px``.
|
||||
Comments are exported as indented text boxes with a light yellow background.
|
||||
|
||||
LaTeX Document
|
||||
==============
|
||||
|
||||
The latex export formats generats a simple ``.tex`` file using a ``report`` document format.
|
||||
All chapters and sections are exported as unnumbered, and the internal header numbering formatting of novelWriter is used instead.
|
||||
Word-wrapping to a fixed column width in the LaTeX source is supported.
|
||||
|
||||
*****************
|
||||
Header Formatting
|
||||
*****************
|
||||
|
||||
The chapter, scene and section headers for novel files can receive some special treatment using the formats under :guilabel:`Chapter Headings` and :guilabel:`Other Headings`.
|
||||
The chapters can either be of numbered or unnumbered type, the latter being suitable for prologues, epilogues, interludes, etc.
|
||||
The headers can be generated using free text in combination with replace tags.
|
||||
The tooltip will explain which tags are available for each heading type.
|
||||
|
||||
.. note::
|
||||
Header formatting only applies to novel files.
|
||||
Headings in note files will will be left as-is, but heading levels 1 through 4 are converted to the correct heading level in the respective output formats.
|
||||
|
||||
Numbered Chapters
|
||||
=================
|
||||
|
||||
Numbered chapters can be automatically assigned a number from 1 and upwards.
|
||||
The number is inserted in place of the ``%num%`` tag.
|
||||
Alternatively, the number can be translated to a word by using the ``%numword%`` tag.
|
||||
The word numbers feature is currently only supported in English, and works for the range from "One" to "Nine Hundred and Ninety-Nine".
|
||||
|
||||
Unnumbered Chapters
|
||||
===================
|
||||
|
||||
Unnumbered chapters only support the ``%title%`` tag for auto-replacement.
|
||||
|
||||
Scenes and Sections
|
||||
===================
|
||||
|
||||
Both scenes and sections support the ``%title%`` tag.
|
||||
In addition, scenes behave differently than the other headings when the format does not contain any auto-replace tags.
|
||||
If the text in the box is any static text, the heading formatter will treat it as a scene separator, and insert that text as centred text between scenes, and ignoring the first scene of a chapter if the chapter heading is not followed by any text.
|
||||
|
||||
Leaving these fields blank will disable any output of scene and section headers or separators to the exported file.
|
||||
28
docs/source/index.txt
Normal file
28
docs/source/index.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
####################################
|
||||
Welcome to novelWriter Documentation
|
||||
####################################
|
||||
|
||||
This is the documentation for novelWriter |version|.
|
||||
|
||||
********
|
||||
Contents
|
||||
********
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
introduction
|
||||
interface
|
||||
projects
|
||||
structure
|
||||
notes
|
||||
export
|
||||
technical
|
||||
|
||||
******************
|
||||
Indices and Tables
|
||||
******************
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
95
docs/source/interface.txt
Normal file
95
docs/source/interface.txt
Normal file
@@ -0,0 +1,95 @@
|
||||
##############
|
||||
User Interface
|
||||
##############
|
||||
|
||||
The user interface is kept as simple as possible to avoid distractions.
|
||||
The main window contains a tree vew pane with the entire structure of the project, and a small details panel below it to display additional information.
|
||||
|
||||
Editing a document can be done by either double-clicking on it, or hitting the return key when the item is selected.
|
||||
This will open the source editor which uses a simplified markdown format described in the section below.
|
||||
|
||||
The document can also be viewed as html with all the comments and commands stripped out. To view a document, simply press Ctrl+R or select a file and go to :menuselection:`Document --> View Document` in the menu.
|
||||
The document viewed does not need to be the same document currently being edited.
|
||||
|
||||
***************
|
||||
Markdown Format
|
||||
***************
|
||||
|
||||
the document editor uses a simplified markdown format.
|
||||
That is, it supports basic formatting like bold, italics and underline, as well as four levels of headings.
|
||||
The formats are listed below.
|
||||
|
||||
In addition to these standard markdown features, the editor also allows for comments, that is text that is ignored by the word counter and not exported or seen in the document viewer.
|
||||
The editor also has a minimal set of commands used for setting tags and references between files.
|
||||
|
||||
.. csv-table:: Formatting Syntax
|
||||
:header: "Format", "Description"
|
||||
:widths: 15, 70
|
||||
|
||||
"``# Title``", "Heading level one. The space after the # is mandatory."
|
||||
"``## Title``", "Heading level two. The space after the # is mandatory."
|
||||
"``### Title``", "Heading level three. The space after the # is mandatory."
|
||||
"``#### Title``", "Heading level four. The space after the # is mandatory."
|
||||
"``**text**``", "The text is renderred as bold text."
|
||||
"``_text_``", "The text is renderred as italics text."
|
||||
"``__text__``", "The text is renderred as underlined text."
|
||||
"``% text...``", "A comment. The text is not exported, seen in viewer, or counted towards word counts."
|
||||
"``@keyword: value``", "A keyword argument followed by a value, or a comma separated list of values."
|
||||
|
||||
******************
|
||||
Keyboard Shortcuts
|
||||
******************
|
||||
|
||||
All features are available as keyboard shortcuts.
|
||||
These are as following:
|
||||
|
||||
.. csv-table:: Keyboard Shortcuts
|
||||
:header: "Shortcut", "Description"
|
||||
:widths: 15, 70
|
||||
|
||||
":kbd:`Ctrl-.`", "Correct word under cursor."
|
||||
":kbd:`Ctrl-1`", "Switch focus to tree view pane."
|
||||
":kbd:`Ctrl-2`", "Switch focus to document editor pane."
|
||||
":kbd:`Ctrl-3`", "Switch focus to document viewer pane."
|
||||
":kbd:`Ctrl-A`", "Select all text in document."
|
||||
":kbd:`Ctrl-B`", "Format selected text, or word under cursor, as bold."
|
||||
":kbd:`Ctrl-C`", "Copy selected text to clipboard."
|
||||
":kbd:`Ctrl-D`", "Wrap selected text, or word under cursor, in double quotes."
|
||||
":kbd:`Ctrl-E`", "If in tree view, edit a document or folder settings."
|
||||
":kbd:`Ctrl-F`", "Open the search bar and search for selected word, if any is selected."
|
||||
":kbd:`Ctrl-H`", "Open the search and replace bar and search for selected word, if any is selected."
|
||||
":kbd:`Ctrl-I`", "Format selected text, or word under cursor, as italic."
|
||||
":kbd:`Ctrl-N`", "Create new document."
|
||||
":kbd:`Ctrl-O`", "Open selected document."
|
||||
":kbd:`Ctrl-Q`", "Exit novelWriter."
|
||||
":kbd:`Ctrl-R`", "If in tree view, open a document for viewing. If in editor pane, open current document for viewing."
|
||||
":kbd:`Ctrl-S`", "Save the current document in the editor."
|
||||
":kbd:`Ctrl-T`", "Show project timeline."
|
||||
":kbd:`Ctrl-U`", "Format selected text, or word under cursor, as underline."
|
||||
":kbd:`Ctrl-V`", "Paste text from clipboard to cursor position."
|
||||
":kbd:`Ctrl-W`", "Close the current document in the editor."
|
||||
":kbd:`Ctrl-X`", "Cut selected text to clipboard."
|
||||
":kbd:`Ctrl-Y`", "Redo latest undo."
|
||||
":kbd:`Ctrl-Z`", "Undo latest changes."
|
||||
":kbd:`Ctrl-F7`", "Toggle spell checking."
|
||||
":kbd:`Ctrl-Del`", "If in tree view, move a document to trash, or delete a folder."
|
||||
":kbd:`Ctrl-Shift-,`", "Change project settings."
|
||||
":kbd:`Ctrl-Shift-1`", "Replace occurrence of word in current document, and search for next occurrence."
|
||||
":kbd:`Ctrl-Shift-A`", "Select all text in current paragraph."
|
||||
":kbd:`Ctrl-Shift-D`", "Wrap selected text, or word under cursor, in single quotes."
|
||||
":kbd:`Ctrl-Shift-I`", "Import text to the current document from a text file."
|
||||
":kbd:`Ctrl-Shift-N`", "Create new folder."
|
||||
":kbd:`Ctrl-Shift-O`", "Open a project."
|
||||
":kbd:`Ctrl-Shift-R`", "Close the document view pane."
|
||||
":kbd:`Ctrl-Shift-S`", "Save the current project."
|
||||
":kbd:`Ctrl-Shift-W`", "Close the current project."
|
||||
":kbd:`Ctrl-Shift-Up`", "Move item one step up in the tree view."
|
||||
":kbd:`Ctrl-Shift-Down`", "Move item one step down in the tree view."
|
||||
":kbd:`F1`", "Open documentation."
|
||||
":kbd:`F2`", "Alternative to :kbd:`Ctrl-E`."
|
||||
":kbd:`F3`", "Find next occurrence of word in current document."
|
||||
":kbd:`F5`", "Export project dialog."
|
||||
":kbd:`F7`", "Re-run spell checker."
|
||||
":kbd:`F9`", "Re-build project indices."
|
||||
":kbd:`Shift-F3`", "Find previous occurrence of word in current document."
|
||||
":kbd:`Return`", "If in tree view, open a document for editing."
|
||||
21
docs/source/introduction.txt
Normal file
21
docs/source/introduction.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
############
|
||||
Introduction
|
||||
############
|
||||
|
||||
novelWriter is a simple multi-document plain text editor using a modified
|
||||
markdown syntax to apply simple formatting. Additional features are available
|
||||
through special meta data commands. These make it possible to generate an
|
||||
overview of the entire novel project and how the various files are
|
||||
interconnected.
|
||||
|
||||
The user interface is intended to be as minimalistic as practically possible,
|
||||
and most features are accessible through the menu and through keyboard
|
||||
shortcuts. The colour scheme of the user interface can be modified with various
|
||||
themes, and new themes are fairly straight forward to add. They are separated
|
||||
into gui and syntax themes, and can be found in the themes folder in the source
|
||||
code.
|
||||
|
||||
The tree view has various sections for the various types of supporting files
|
||||
that the User may want to add to the project. The novel itself lives under its
|
||||
own root folder, and export features are in the process of being added. These
|
||||
will include HTML, open document and PDF via LaTeX.
|
||||
21
docs/source/notes.txt
Normal file
21
docs/source/notes.txt
Normal file
@@ -0,0 +1,21 @@
|
||||
########################
|
||||
Supporting Files (Notes)
|
||||
########################
|
||||
|
||||
Supporting files, or notes, are any files stored in root folders that are not the Novel root folder.
|
||||
These files are intended for summaries and outlines of the various plot elements, characters, locations, and so on, of the novel.
|
||||
These are not required, but making at least minimal files for each such element makes it possible to use the timeline view feature to see how each element intersects with each section of the novel itself.
|
||||
|
||||
*********
|
||||
File Tags
|
||||
*********
|
||||
|
||||
Each note file 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 files and will then show up as dots in the timeline view.
|
||||
|
||||
The syntax highlighter will alert the user that the keyword is correctly used and that the tag is allowed.
|
||||
Duplicate tags should be detected as long as the index is up to date.
|
||||
|
||||
The tag is the only part of these files that the application used.
|
||||
The rest of the file is there for the writer to use in whatever way they wish.
|
||||
120
docs/source/projects.txt
Normal file
120
docs/source/projects.txt
Normal file
@@ -0,0 +1,120 @@
|
||||
##############
|
||||
Novel Projects
|
||||
##############
|
||||
|
||||
A novelWriter project requires a dedicated folder for storing its files.
|
||||
See the Technical Information section for further details.
|
||||
|
||||
A new project can be created from the Project menu by selecting :menuselection:`Project --> New Project`.
|
||||
A list of recently opened projects is also maintained and can be selected from the menu.
|
||||
|
||||
The project specific settings are available in :menuselection:`Project --> Project Settings`.
|
||||
See further details below.
|
||||
|
||||
*****************
|
||||
Project Structure
|
||||
*****************
|
||||
|
||||
Projects are structured into a set of root folders, visible in the left side tree view panel.
|
||||
|
||||
The core novel files go into a root folder of type Novel.
|
||||
Other supporting files go into root folders of types Plot, Characters, Locations, Timeline, Objects or Custom.
|
||||
These other root folder types are intended for your notes on the various elements of your story.
|
||||
Using these are of course entirely optional.
|
||||
|
||||
* **Novel:** The root folder of all text that goes into the final novel.
|
||||
This class of files have other rules and features than other files in the project.
|
||||
See the Novel Structure section for more details.
|
||||
* **Plot:** This is the root folder where main plots can be outlined.
|
||||
It is optional, but adding at least dummy files can be useful in order to generate the timeline view.
|
||||
* **Characters:** Character files go in this root folder.
|
||||
These are especially important if one wants to use the timeline view to see which character appears where and which part of the story is told from a specific character's point-of-view.
|
||||
* **Locations:** Location is for various scene locations that one wants to track in the timeline view if that applies to the story.
|
||||
* **Timeline:** If the story jumps in time within the same plot, this class of files can be used to track this.
|
||||
* **Objects:** Important objects in the story can be tracked here, and connected to the timeline as well.
|
||||
* **Custom:** The custom root folder can be used for tracking anything else not covered by the above options.
|
||||
|
||||
Deleted files will be moved into a special "Trash" root folder.
|
||||
Currently, these files cannot be permanently deleted from the project.
|
||||
|
||||
Orphaned Documents
|
||||
==================
|
||||
|
||||
In the event the editor crashes or otherwise exits without saving the project state, files that have been added to the project tree and are saved to disk will appear in a special "Orphaned Items" root folder next time the application is started.
|
||||
These orphaned files will not have any meta data associated with them, so the title and other information has to be set again, and the files moved back to the correct location in the project.
|
||||
Using Project Folders
|
||||
|
||||
Folders, aside from root folders, have no structural significance to the project. They are there purely as a way for the user to organise the files in meaningful sections and to be able to close them in the tree view. When processing the files in the novel, the folders are ignored.
|
||||
|
||||
****************
|
||||
Project Settings
|
||||
****************
|
||||
|
||||
The project settings can be accessed from the :menuselection:`Project --> Project Settings` menu entry.
|
||||
This will open a dialog box.
|
||||
|
||||
Settings Tab
|
||||
============
|
||||
|
||||
The Settings tab holds the project title and author settings.
|
||||
Working Title can be set to a different title than the Book Title.
|
||||
The difference between them is simply that the Working Title is used for the GUI (main window title) and when the export features are added can optionally be printed to the cover sheet.
|
||||
The Book Title, on the other hand, will only be printed to the title page on export.
|
||||
|
||||
The Book Authors text box takes one author per line.
|
||||
The line breaks matter in that this is converted to a list for later correct formatting.
|
||||
|
||||
Status Tab
|
||||
==========
|
||||
|
||||
Each file of type NOVEL can be given a status level, signified by a coloured icon.
|
||||
These are purely there for the user's convenience, and you are not required to use them for any other feature to work.
|
||||
The intention is to use this list to set what stage of writing you are on, although you can in principle make them whatever you want.
|
||||
|
||||
Note that status levels currently in use by a file cannot be deleted.
|
||||
|
||||
Importance Tab
|
||||
==============
|
||||
|
||||
Each file of types PLOT, CHARACTER, WORLD, TIMELINE, OBJECT or CUSTOM can be given an importance level, signified by a coloured icon like for status level.
|
||||
These are also purely there for the user's convenience, and you are not required to use them for any other feature to work.
|
||||
The intention is to use this list to set how important the character, plot element, or otherwise, is for the story.
|
||||
Again, these can in principle be used for whatever you want.
|
||||
|
||||
Note that importance levels currently in use by a file cannot be deleted.
|
||||
|
||||
Auto-Replace Tab
|
||||
================
|
||||
|
||||
A set of automatically replaced keywords can be added in this tab.
|
||||
The keywords in the left column wile be replaced by the text in the right column when documents are opened in the viewer.
|
||||
This will also be applied to exports when the feature is added.
|
||||
|
||||
Note that a keyword cannot contain any spaces.
|
||||
The angle brackets are added by default, and when used in the text are a part of the keyword to be replaced.
|
||||
This is to ensure that parts of the text isn't unintentionally replaced by the content of the list.
|
||||
|
||||
*************
|
||||
Writing Files
|
||||
*************
|
||||
|
||||
New document files can be created from the Document menu, or by pressing Ctrl+N while in the tree view pane.
|
||||
This will create a new, empty file, and open the item settings dialog where the filename and various other settings can be set.
|
||||
This dialog can also be opened again later from either the menu, :menuselection:`Project -> Edit` item, or by pressing Ctrl+E or F2 with the item selected.
|
||||
|
||||
The different classes of documents have some restrictions.
|
||||
|
||||
******
|
||||
Backup
|
||||
******
|
||||
|
||||
An automatic backup system is built into novelWriter.
|
||||
In order to use it, a backup path to where the backups are to be stored needs to be provided in :menuselection:`Tools --> Preferences`.
|
||||
Backups can be run automatically when a project is closed, which also implies it is run when the application is closed.
|
||||
Backups are date stamped zip files of the entire project folder.
|
||||
|
||||
The backup feature, when configured, can also be run manually from the Tools menu.
|
||||
|
||||
For the backup to be able to run, the Working Title must be set in Project Settings.
|
||||
This value is used to generate the file name.
|
||||
It is also possible to dissable automated backup for a given project in its project settings.
|
||||
93
docs/source/structure.txt
Normal file
93
docs/source/structure.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
#################
|
||||
Project Structure
|
||||
#################
|
||||
|
||||
This section concerns files under the Novel type root folder.
|
||||
There are some restrictions and features that only applies to these type of files.
|
||||
|
||||
**********************
|
||||
Importance of Headings
|
||||
**********************
|
||||
|
||||
Subfolders under root folders have no impact on the structure of the novel itself.
|
||||
The structure is instead dictated by the heading level.
|
||||
Four levels of headings are supported, signified by the number of hashes preceding the title.
|
||||
See the Markdown section.
|
||||
|
||||
The header levels are not only important when generating the exported novel file, but is also used by the indexer and timeline view.
|
||||
Each heading starts a new region where new references to tags can be set, and will show up as a new row in the timeline view.
|
||||
|
||||
The different header levels are interpreted as specific section types of the novel.
|
||||
|
||||
* ``# Header1``: Header level 1 signifies that the text refers to either the novel title or the name of a top level partition.
|
||||
* ``## Header2``: Header level 2 signifies a chapter level partition.
|
||||
* ``### Header3``: Header level 3 signifies a scene level partition.
|
||||
* ``#### Header4``: Header level 4 signifies a sub-scene level partition.
|
||||
|
||||
**************
|
||||
Tag References
|
||||
**************
|
||||
|
||||
Each section, indicated by a heading, can contain references to tags set in the supporting files of the project.
|
||||
See the File Tags section.
|
||||
|
||||
The references are gathered by the indexer and used to generate the timeline view of how the different parts of the novel are connected.
|
||||
|
||||
References are set as keyword and a list of corresponding tags.
|
||||
The valid keywords are listed below. The format of such a line is ``@keyword: value1, [value2] ... [valueN]``.
|
||||
Note that not all keywords allow multiple values.
|
||||
|
||||
* ``@pov``: The point-of-view character for the current section.
|
||||
The target must be a note tag in the character root folder.
|
||||
Only one value is accepted. further values are ignored.
|
||||
* ``@char``: Other characters in the current section.
|
||||
The target must be a note tag in the character root folder.
|
||||
Multiple values are allowed.
|
||||
This should not include the point-of-view character.
|
||||
* ``@plot``: The plot timelines touched by the current section.
|
||||
The target must be a note tag in the plot root folder.
|
||||
Multiple values are allowed.
|
||||
* ``@time``: The timelines touched by the current section.
|
||||
The target must be a note tag in the timeline root folder.
|
||||
Multiple values are allowed.
|
||||
* ``@location``: The location the current section takes place in.
|
||||
The target must be a note tag in the locations root folder.
|
||||
Multiple values are allowed.
|
||||
* ``@object``: Objects present in the current section.
|
||||
The target must be a note tag in the object root folder.
|
||||
Multiple values are allowed.
|
||||
* ``@custom``: Custom references in the current section.
|
||||
The target must be a note tag in the custom root folder.
|
||||
Multiple values are allowed.
|
||||
|
||||
the syntax highlighter will alert the user that only the correct keywords are used, and that the tags referenced exist.
|
||||
If the index of defined tags is out of date, press F9 to regenerate it, or select :menuselection:`Tools --> Rebuild Indices` from the menu.
|
||||
In general, the index for a file is regenerated when a file is saved, so this shouldn't normally be necessary.
|
||||
|
||||
*****************
|
||||
Novel File Layout
|
||||
*****************
|
||||
|
||||
Files that exist under the NOVEL type root folder can have a number of layouts set.
|
||||
See overview below.
|
||||
These layouts are currently not used for any internal feature, but when the export feature is added they will be important in determining how the different sections of the novel is formatted.
|
||||
|
||||
* **Title Page**: The title page layout.
|
||||
The title should be formatted as a heading level one.
|
||||
* **Book**: In principle, the entire novel can be contained in a single file.
|
||||
In that case, use the Book layout on this file.
|
||||
The internal structure is then controlled by the heading levels.
|
||||
* **Plain Page**: A plain page is just that,
|
||||
It is not included into content and the heading levels are ignored.
|
||||
* **Partition**: A partition can be used to split a the novel into parts.
|
||||
Use a level one heading for this.
|
||||
* **Chapter**: Signifies the start of a new chapter.
|
||||
If the text itself is contained in scene files, these files should only contain the title and tag references for characters, plot, etc.
|
||||
The heading for chapters should be level two.
|
||||
* **Unnumbered**: Same as Chapter, but when exporting the files and automatic chapter numbering is enabled, this file will not receive a number.
|
||||
* **Scene**: A scene file.
|
||||
This file should have a header of level three.
|
||||
Further sections can have headers of level four.
|
||||
These will not impact the overall structure, but will allow for setting new characters and plot references in parts of a scene if such granularity is needed.
|
||||
* **Note**: A generic file that is ignored when the novel is exported.
|
||||
These layout is allowed within the Novel root folder, and is the only layout allowed for a file outside the Novel root folder.
|
||||
35
docs/source/technical.txt
Normal file
35
docs/source/technical.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
#####################
|
||||
Technical Information
|
||||
#####################
|
||||
|
||||
This section contains details of how novelWriter stores and handles the project data.
|
||||
|
||||
******************
|
||||
How Data is Stored
|
||||
******************
|
||||
|
||||
Main Project File
|
||||
=================
|
||||
|
||||
The project itself requires a dedicated folder for storing its files.
|
||||
The main project file is stored as an XML file with the name ``nwProject.nwx``.
|
||||
This file contains all the meta data unique for the project.
|
||||
That includes project settings.
|
||||
|
||||
If this file is lost or corrupted, the structure of the project is lost.
|
||||
It is important to keep this file backed up.
|
||||
|
||||
The project XML file is suitable for diff tools and version control, although a timesetamp is set in the meta section on line 2 each time the file is saved.
|
||||
|
||||
Project Documents
|
||||
=================
|
||||
|
||||
The project documents are saved in folders staring with ``data_``.
|
||||
Each document has a file handle taken from the first 13 characters of a SHA256 hash of the system time.
|
||||
The documents are saved with a folder and filename derived from this hash.
|
||||
|
||||
The reason for this is to avoid issues with file naming conventions and restrictions.
|
||||
The file name set in the tree view is only saved in the project XML file.
|
||||
|
||||
Each document file contains a plain text version of the text from the editor.
|
||||
The file can in principle be edited in any text editor, and is suitable for diffing and version control if so desired.
|
||||
@@ -1,632 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>novelWriter Help</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>novelWriter Help</h1>
|
||||
|
||||
<a name="Content"></a>
|
||||
<h3>Content:</h3>
|
||||
<ul>
|
||||
<li><a href="#Intro">1. Introduction</a></li>
|
||||
<li><a href="#Proj">2. Novel Projects</a></li>
|
||||
<ul>
|
||||
<li><a href="#Proj_Structure">2.1 Project Structure</a></li>
|
||||
<li><a href="#Proj_Settings">2.2 Project Settings</a></li>
|
||||
<li><a href="#Proj_Documents">2.3 Writing Files</a></li>
|
||||
<li><a href="#Proj_Backup">2.4 Backup</a></li>
|
||||
</ul>
|
||||
<li><a href="#Novel">3. Novel Structure</a></li>
|
||||
<ul>
|
||||
<li><a href="#Novel_Headings">3.1 Importance of Headings</a></li>
|
||||
<li><a href="#Novel_Refs">3.2 Tag References</a></li>
|
||||
<li><a href="#Novel_Files">3.3 Novel File Layout</a></li>
|
||||
</ul>
|
||||
<li><a href="#Notes">4. Supporting Files (Notes)</a></li>
|
||||
<ul>
|
||||
<li><a href="#Notes_Tags">4.1 File Tags</a></li>
|
||||
</ul>
|
||||
<li><a href="#GUI">5. User Interface</a></li>
|
||||
<ul>
|
||||
<li><a href="#GUI_Markdown">5.1 Markdown Format</a></li>
|
||||
<li><a href="#GUI_Shortcuts">5.2 Keyboard Shortcuts</a></li>
|
||||
</ul>
|
||||
<li><a href="#Code">6. Technical Information</a></li>
|
||||
<ul>
|
||||
<li><a href="#Code_Project">6.1 How Data is Stored</a></li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
<a name="Intro"></a>
|
||||
<h2>1. Introduction</h2>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>novelWriter is a simple multi-document plain text editor using a modified markdown syntax to
|
||||
apply simple formatting. Additional features are available through special meta data
|
||||
commands. These make it possible to generate an overview of the entire novel project and how
|
||||
the various files are interconnected.</p>
|
||||
|
||||
<p>The user interface is intended to be as minimalistic as practically possible, and most
|
||||
features are accessible through the menu and through keyboard shortcuts. The colour scheme
|
||||
of the user interface can be modified with various themes, and new themes are fairly
|
||||
straight forward to add. They are separated into gui and syntax themes, and can be found in
|
||||
the themes folder in the source code.</p>
|
||||
|
||||
<p>The tree view has various sections for the various types of supporting files that the User
|
||||
may want to add to the project. The novel itself lives under its own root folder, and export
|
||||
features are in the process of being added. These will include HTML, open document and PDF
|
||||
via LaTeX.</p>
|
||||
|
||||
<a name="Proj"></a>
|
||||
<h2>2. Novel Projects</h2>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>A novelWriter project requires a dedicated folder for storing its files. See the
|
||||
<a href="#Code">Technical Information</a> section for further details.</p>
|
||||
<p>A new project can be created from the Project menu by selecting <mark>Project > New
|
||||
Project</mark>. A list of recently opened projects is also maintained and can be selected
|
||||
from the menu.</p>
|
||||
<p>The project specific settings are available in <mark>Project > Project Settings</mark>. See
|
||||
further details below.</p>
|
||||
|
||||
<a name="Proj_Structure"></a>
|
||||
<h3>2.1 Project Structure</h3>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>Projects are structured into a set of root folders in the left side tree view panel.</p>
|
||||
<p>The core novel files go into a root folder of type <mark>Novel</mark>. Other supporting files
|
||||
go into root folders of types <mark>Plot</mark>, <mark>Characters</mark>,
|
||||
<mark>Locations</mark>, <mark>Timeline</mark>, <mark>Objects</mark> or <mark>Custom</mark>.
|
||||
These other root folder types are intended for your notes on the various elements of your
|
||||
story. Using these are of course entirely optional.</p>
|
||||
<table>
|
||||
<tr>
|
||||
<td><mark>Novel</mark></td>
|
||||
<td>The root folder of all text that goes into the final novel. This class of files have
|
||||
other rules and features than other files in the project. See the
|
||||
<a href="#Novel">Novel Structure</a> section for more details.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Plot</mark></td>
|
||||
<td>This is the root folder where main plots can be outlined. It is optional, but adding
|
||||
at least dummy files can be useful in order to generate the timeline view.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Characters</mark></td>
|
||||
<td>Character files go in this root folder. These are especially important if one wants
|
||||
to use the timeline view to see which character appears where and which part of the
|
||||
story is told from a specific character's point-of-view.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Locations</mark></td>
|
||||
<td>Location is for various scene locations that one wants to track in the timeline
|
||||
view if that applies to the story.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Timeline</mark></td>
|
||||
<td>If the story jumps in time within the same plot, this class of files can be used to
|
||||
track this.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Objects</mark></td>
|
||||
<td>Important objects in the story can be tracked here, and connected to the timeline as
|
||||
well.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Custom</mark></td>
|
||||
<td>the custom root folder can be used for tracking anything else not covered by the
|
||||
above options.</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>Deleted files will be moved into a special <mark>TRASH</mark> root folder. Currently, these
|
||||
files cannot be permanently deleted from the project.</p>
|
||||
|
||||
<h4>Orphaned Documents</h4>
|
||||
<p>In the event the editor crashes or otherwise exits without saving the project state, files
|
||||
that have been added to the project tree and are saved to disk will appear in a special
|
||||
"Orphaned Items" root folder next time the application is started. These orphaned files will
|
||||
not have any meta data associated with them, so the title and other information has to be
|
||||
set again, and the files moved back to the correct location in the project.</p>
|
||||
|
||||
<h4>Using Project Folders</h4>
|
||||
<p>Folders, aside from root folders, have no structural significance to the project. They are
|
||||
there purely as a way for the user to organise the files in meaningful sections and to be
|
||||
able to close them in the tree view. When processing the files in the novel, the folders are
|
||||
ignored.</p>
|
||||
|
||||
<a name="Proj_Settings"></a>
|
||||
<h3>2.2 Project Settings</h3>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>The project settings can be accessed from the <mark>Project > Project Settings</mark> menu
|
||||
entry. This will open a dialog box.</p>
|
||||
|
||||
<h4>Settings Tab</h4>
|
||||
<p>The Settings tab holds the project title and author settings. <em>Working Title</em> can be
|
||||
set to a different title than the <em>Book Title</em>. The difference between them is simply
|
||||
that the Working Title is used for the GUI (main window title) and when the export features
|
||||
are added can optionally be printed to the cover sheet. The Book Title, on the other hand,
|
||||
will only be printed to the title page on export.</p>
|
||||
<p>The <em>Book Authors</em> text box takes one author per line. The line breaks matter in that
|
||||
this is converted to a list for later correct formatting.</p>
|
||||
|
||||
<h4>Status Tab</h4>
|
||||
<p>Each file of type <mark>NOVEL</mark> can be given a status level, signified by a coloured
|
||||
icon. These are purely there for the user's convenience, and you are not required to use
|
||||
them for any other feature to work. The intention is to use this list to set what stage of
|
||||
writing you are on, although you can in principle make them whatever you want.</p>
|
||||
<p>Note that status levels currently in use by a file cannot be deleted.</p>
|
||||
|
||||
<h4>Importance Tab</h4>
|
||||
<p>Each file of types <mark>PLOT</mark>, <mark>CHARACTER</mark>, <mark>WORLD</mark>,
|
||||
<mark>TIMELINE</mark>, <mark>OBJECT</mark> or <mark>CUSTOM</mark> can be given an importance
|
||||
level, signified by a coloured icon like for status level. These are also purely there for
|
||||
the user's convenience, and you are not required to use them for any other feature to work.
|
||||
The intention is to use this list to set how important the character, plot element, or
|
||||
otherwise, is for the story. Again, these can in principle be used for whatever you want.
|
||||
<p>Note that importance levels currently in use by a file cannot be deleted.</p>
|
||||
|
||||
<h4>Auto-Replace Tab</h4>
|
||||
<p>A set of automatically replaced keywords can be added in this tab. The keywords in the left
|
||||
column wile be replaced by the text in the right column when documents are opened in the
|
||||
viewer. This will also be applied to exports when the feature is added.</p>
|
||||
<p>Note that a keyword cannot contain any spaces. The angle brackets are dded by default, and
|
||||
when used in the text are a part of the keyword to be replaced. This is to ensure that parts
|
||||
of the text isn't unintentionally replaced by the content of the list.</p>
|
||||
|
||||
<a name="Proj_Documents"></a>
|
||||
<h2>2.3 Writing Files</h2>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>New document files can be created from the Document menu, or by pressing <mark>Ctrl+N</mark>
|
||||
while in the tree view pane. This will create a new, empty file, and open the item settings
|
||||
dialog where the filename and various other settings can be set. This dialog can also be
|
||||
opened again later from either the menu, <mark>Project > Edit item</mark>, or by pressing
|
||||
<mark>Ctrl+E</mark> or <mark>F2</mark> with the item selected.</p>
|
||||
<p>The different classes of documents have some restrictions.</p>
|
||||
|
||||
<a name="Proj_Backup"></a>
|
||||
<h2>2.4 Backup</h2>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>An automatic backup system is built into novelWriter. In order to use it, a backup path to
|
||||
where the backups are to be stored needs to be provided in <mark>Tools > Preferences</mark>.
|
||||
Backups can be run automatically when a project is closed, which also implies it is run when
|
||||
the application is closed. Backups are date stamped zip files of the entire project
|
||||
folder.</p>
|
||||
|
||||
<p>The backup feature, when configured, can also be run manually from the <mark>Tools</mark>
|
||||
menu.</p>
|
||||
|
||||
<p>For the backup to be able to run, the <mark>Working Title</mark> must be set in <mark>Project
|
||||
Settings</mark>. This value is used to generate the file name. It is also possible to
|
||||
dissable automated backup for a given project in its project settings.</p>
|
||||
|
||||
<a name="Novel"></a>
|
||||
<h2>3. Novel Structure</h2>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>This section concerns files under the <mark>Novel</mark> type root folder. There are some
|
||||
restrictions and features that only applies to these type of files.</p>
|
||||
|
||||
<a name="Novel_Headings"></a>
|
||||
<h3>3.1 Importance of Headings</h3>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>Subfolders under root folders have no impact on the structure of the novel itself. The
|
||||
structure is instead dictated by the heading level. Four levels of headings are supported,
|
||||
signified by the number of hashes preceding the title. See the
|
||||
<a href="#GUI_Markdown">Markdown</a> section.</p>
|
||||
<p>The header levels are not only important when generating the exported novel file, but is also
|
||||
used by the indexer and timeline view. Each heading starts a new region where new references
|
||||
to tags can be set, and will show up as a new row in the timeline view.</p>
|
||||
<p>The different header levels are interpreted as specific section types of the novel.</p>
|
||||
<table>
|
||||
<tr>
|
||||
<td><mark># Header1</mark></td>
|
||||
<td>Header level 1 signifies that the text refers to either the novel title or the
|
||||
name of a top level partition.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>## Header2</mark></td>
|
||||
<td>Header level 2 signifies a chapter level partition.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>### Header3</mark></td>
|
||||
<td>Header level 3 signifies a scene level partition.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>#### Header4</mark></td>
|
||||
<td>Header level 4 signifies a sub-scene level partition.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<a name="Novel_Refs"></a>
|
||||
<h3>3.2 Tag References</h3>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>Each section started by a heading can contain references to tags set in the supporting files
|
||||
of the project. See the <a href="#Notes_Tags">File Tags</a> section.</p>
|
||||
<p>The references are gathered by the indexer and used to generate the timeline view of how the
|
||||
different parts of the novel are connected.</p>
|
||||
<p>References are set as keyword and a list of corresponding tags. The valid keywords are listed
|
||||
below. The format of such a line is <mark>@keyword: value1, [value2] ... [valueN]</mark>.
|
||||
Note that not all keywords allow multiple values.</p>
|
||||
<table>
|
||||
<tr>
|
||||
<td><mark>@pov</mark></td>
|
||||
<td>The point-of-view character for the current section. The target must be a note tag
|
||||
in the character root folder. Only one value is accepted. further values are
|
||||
ignored.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>@char</mark></td>
|
||||
<td>Other characters in the current section. The target must be a note tag in the
|
||||
character root folder. Multiple values are allowed. This should not include the
|
||||
point-of-view character.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>@plot</mark></td>
|
||||
<td>The plot timelines touched by the current section. The target must be a note tag in
|
||||
the plot root folder. Multiple values are allowed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>@time</mark></td>
|
||||
<td>The timelines touched by the current section. The target must be a note tag in the
|
||||
timeline root folder. Multiple values are allowed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>@location</mark></td>
|
||||
<td>The location the current section takes place in. The target must be a note tag in
|
||||
the locations root folder. Multiple values are allowed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>@object</mark></td>
|
||||
<td>Objects present in the current section. The target must be a note tag in the object
|
||||
root folder. Multiple values are allowed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>@custom</mark></td>
|
||||
<td>Custom references in the current section. The target must be a note tag in the
|
||||
custom root folder. Multiple values are allowed.</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>the syntax highlighter will alert the user that only the correct keywords are used, and that
|
||||
the tags referenced exist. If the index of defined tags is out of date, press
|
||||
<mark>F9</mark> to regenerate it, or select <mark>Tools > Rebuild Indices</mark> from the
|
||||
menu. In general, the index for a file is regenerated when a file is saved, so this
|
||||
shouldn't normally be necessary.</p>
|
||||
|
||||
<a name="Novel_Files"></a>
|
||||
<h3>3.3 Novel File Layout</h3>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>Files that exist under the <mark>NOVEL</mark> type root folder can have a number of layouts
|
||||
set. See overview below. These layouts are currently not used for any internal feature, but
|
||||
when the export feature is added they will be important in determining how the different
|
||||
sections of the novel is formatted.</p>
|
||||
<table>
|
||||
<tr>
|
||||
<td><mark>Title Page</mark></td>
|
||||
<td>The title page layout. The title should be formatted as a heading level one.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Book</mark></td>
|
||||
<td>In principle, the entire novel can be contained in a single file. In that case, use
|
||||
the Book layout on this file. The internal structure is then controlled by the
|
||||
heading levels.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Plain Page</mark></td>
|
||||
<td>A plain page is just that, It is not included into content and the heading levels
|
||||
are ignored.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Partition</mark></td>
|
||||
<td>A partition can be used to split a the novel into parts. Use a level one heading for
|
||||
this.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Chapter</mark></td>
|
||||
<td>Signifies the start of a new chapter. If the text itself is contained in scene
|
||||
files, these files should only contain the title and tag references for characters,
|
||||
plot, etc. The heading for chapters should be level two.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Un-Numbered</mark></td>
|
||||
<td>Same as Chapter, but when exporting the files and automatic chapter numbering is
|
||||
enabled, this file will not receive a number.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Scene</mark></td>
|
||||
<td>A scene file. This file should have a header of level three. Further sections can
|
||||
have headers of level four. These will not impact the overall structure, but will
|
||||
allow for setting new characters and plot references in parts of a scene if such
|
||||
granularity is needed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Note</mark></td>
|
||||
<td>A generic file that is ignored when the novel is exported. These layout is allowed
|
||||
within the Novel root folder, and is the only layout allowed for a file outside the
|
||||
Novel root folder.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<a name="Notes"></a>
|
||||
<h2>4. Supporting Files (Notes)</h2>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>Supporting files, or notes, are any files stored in root folders that are not the Novel root
|
||||
folder. These files are intended for summaries and outlines of the various plot elements,
|
||||
characters, locations, and so on, of the novel. These are no required, but making at least
|
||||
minimal files for each such element makes it possible to use the timeline view feature to
|
||||
see how each element intersects with each section of the novel itself.</p>
|
||||
|
||||
<a name="Notes_Tags"></a>
|
||||
<h2>4.1 File Tags</h2>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>Each note file can have a tag associated with it, The format of a tag is
|
||||
<mark>@tag: tagname</mark>, where tagname is a unique identifier. tags can then be
|
||||
referenced in the novel files and will then show up as dots in the timeline view.</p>
|
||||
<p>The syntax highlighter will alert the user that the keyword is correctly used and that the
|
||||
tag is allowed. Duplicate tags should be detected as long as the index is up to date.</p>
|
||||
<p>The tag is the only part of these files that the application used. The rest of the file is
|
||||
there for the writer to use in whatever way they wish.</p>
|
||||
|
||||
<a name="GUI"></a>
|
||||
<h2>5. User Interface</h2>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>The user interface is kept as simple as possible to avoid distractions. The main window
|
||||
contains a tree vew pane with the entire structure of the project, and a small details panel
|
||||
below it to display additional information.</p>
|
||||
<p>Editing a document can be done by either double-clicking on it, or hitting the return key
|
||||
when the item is selected. This will open the source editor which uses a simplified markdown
|
||||
format described in the section below.</p>
|
||||
<p>The document can also be viewed as html with all the comments and commands stripped out. To
|
||||
view a document, simply press <mark>Ctrl+R</mark> or select a file and go to <mark>Document
|
||||
> View Document</mark> in the menu. The document viewed does not need to be the same
|
||||
document currently being edited.</p>
|
||||
|
||||
<a name="GUI_Markdown"></a>
|
||||
<h3>5.1 Markdown Format</h3>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>the document editor uses a simplified markdown format. That is, it supports basic formatting
|
||||
like bold, italics and underline, as well as four levels of headings. The formats are listed
|
||||
below.</p>
|
||||
<p>In addition to these standard markdown features, the editor also allows for comments, that is
|
||||
text that is ignored by the word counter and not exported or seen in the document viewer.
|
||||
The editor also has a minimal set of commands used for setting tags and references between
|
||||
files.</p>
|
||||
<table>
|
||||
<tr>
|
||||
<td><mark># Title</mark></td>
|
||||
<td>Heading level one. The space after the # is mandatory.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>## Title</mark></td>
|
||||
<td>Heading level two. The space after the # is mandatory.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>### Title</mark></td>
|
||||
<td>Heading level three. The space after the # is mandatory.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>#### Title</mark></td>
|
||||
<td>Heading level four. The space after the # is mandatory.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>**text**</mark></td>
|
||||
<td>The text is renderred as bold text.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>_text_</mark></td>
|
||||
<td>The text is renderred as italics text.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>__text__</mark></td>
|
||||
<td>The text is renderred as underlined text.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>% text...</mark></td>
|
||||
<td>A comment. The text is not exported, seen in viewer, or counted towards word counts.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>@command: value</mark></td>
|
||||
<td>A command followed by a value, or a comma separated list of values.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<a name="GUI_Shortcuts"></a>
|
||||
<h3>5.2 Keyboard Shortcuts</h3>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>All features are available as keyboard shortcuts. These are as following:</p>
|
||||
<table>
|
||||
<tr>
|
||||
<td><mark>Ctrl+Shift+O</mark></td>
|
||||
<td>Open a project.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+Shift+S</mark></td>
|
||||
<td>Save the current project.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+Shift+W</mark></td>
|
||||
<td>Close the current project.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+Shift+,</mark></td>
|
||||
<td>Change project settings.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+Shift+N</mark></td>
|
||||
<td>Create new folder.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+E, F2</mark></td>
|
||||
<td>If in tree view, edit a document or folder settings.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+Del</mark></td>
|
||||
<td>If in tree view, move a document to trash, or delete a folder.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+Q</mark></td>
|
||||
<td>Exit novelWriter.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+N</mark></td>
|
||||
<td>Create new document.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+O</mark></td>
|
||||
<td>Open selected document.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Return</mark></td>
|
||||
<td>If in tree view, open a document for editing.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+S</mark></td>
|
||||
<td>Save the current document in the editor.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+W</mark></td>
|
||||
<td>Close the current document in the editor.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+R</mark></td>
|
||||
<td>If in tree view, open a document for viewing. If in editor pane, open current document for viewing.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+Shift+R</mark></td>
|
||||
<td>Close the document view pane.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+Z</mark></td>
|
||||
<td>Undo latest changes.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+Y</mark></td>
|
||||
<td>Redo latest undo.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+C</mark></td>
|
||||
<td>Copy selected text to clipboard.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+X</mark></td>
|
||||
<td>Cut selected text to clipboard.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+V</mark></td>
|
||||
<td>Paste text from clipboard to cursor position.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+A</mark></td>
|
||||
<td>Select all text in document.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+Shift+A</mark></td>
|
||||
<td>Select all text in current paragraph.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+1</mark></td>
|
||||
<td>Switch focus to tree view pane.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+2</mark></td>
|
||||
<td>Switch focus to document editor pane.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+3</mark></td>
|
||||
<td>Switch focus to document viewer pane.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+T</mark></td>
|
||||
<td>Show project timeline.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+B</mark></td>
|
||||
<td>Format selected text, or word under cursor, as bold.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+I</mark></td>
|
||||
<td>Format selected text, or word under cursor, as italic.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+U</mark></td>
|
||||
<td>Format selected text, or word under cursor, as underline.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+D</mark></td>
|
||||
<td>Wrap selected text, or word under cursor, in double quotes.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+Shift+D</mark></td>
|
||||
<td>Wrap selected text, or word under cursor, in single quotes.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+Shift+Up</mark></td>
|
||||
<td>Move item one step up in the tree view.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+Shift+Down</mark></td>
|
||||
<td>Move item one step down in the tree view.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+F7</mark></td>
|
||||
<td>Toggle spell checking.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>F7</mark></td>
|
||||
<td>Re-run spell checker.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>Ctrl+.</mark></td>
|
||||
<td>Correct word under cursor.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>F9</mark></td>
|
||||
<td>Re-build project indices.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><mark>F1</mark></td>
|
||||
<td>Open documentation.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<a name="Code"></a>
|
||||
<h2>6. Technical Information</h2>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<p>This section contains details of how novelWriter stores and handles the project data.</p>
|
||||
|
||||
<a name="Code_Project"></a>
|
||||
<h3>6.1 How Data is Stored</h3>
|
||||
<p><a href="#Content">Back to top</a></p>
|
||||
|
||||
<h4>Main Project File</h4>
|
||||
<p>The project itself requires a dedicated folder for storing its files. The main project file
|
||||
is stored as an XML file with the name <mark>nwProject.nwx</mark>. This file contains all
|
||||
the meta data unique for the project. That includes project settings.</p>
|
||||
<p>If this file is lost or corrupted, the structure of the project is lost. It is important to
|
||||
keep this file backed up.</p>
|
||||
<p>The project XML file is suitable for diff tools and version control, although a timesetamp is
|
||||
set in the meta section on line 2 each time the file is saved.</p>
|
||||
|
||||
<h4>Project Documents</h4>
|
||||
<p>The project documents are saved in folders staring with <mark>data_</mark>. Each document has
|
||||
a file handle taken from the first 13 characters of a SHA256 hash of the system time. the
|
||||
documents are saved with a folder and filename derived from this hash.</p>
|
||||
<p>The reason for this is to avoid issues with file naming conventions and restrictions. The
|
||||
file name set in the tree view is only saved in the project XML file.</p>
|
||||
<p>Each document file contains a plain text version of the text from the editor. The file can in
|
||||
principle be edited in any text editor, and is suitable for diffing and version control if
|
||||
so desired.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,6 +3,10 @@
|
||||
|
||||
import sys
|
||||
|
||||
if sys.hexversion < 0x030500F0:
|
||||
print("ERROR: At least Python 3.5 is required")
|
||||
exit(1)
|
||||
|
||||
try:
|
||||
import PyQt5.QtWidgets
|
||||
import PyQt5.QtGui
|
||||
|
||||
@@ -22,12 +22,13 @@ __package__ = "novelWriter"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__copyright__ = "Copyright 2018–2019, Veronica Berglyd Olsen"
|
||||
__license__ = "GPLv3"
|
||||
__version__ = "0.3"
|
||||
__date__ = "2019-10-19"
|
||||
__version__ = "0.3.2"
|
||||
__date__ = "2019-10-27"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
__status__ = "Development"
|
||||
__url__ = "https://github.com/vkbo/novelWriter"
|
||||
__docurl__ = "https://novelwriter.readthedocs.io"
|
||||
__credits__ = [
|
||||
"Veronica Berglyd Olsen (developer)",
|
||||
"Marian Lückhof (contributor, tester)"
|
||||
|
||||
54
nw/config.py
54
nw/config.py
@@ -12,6 +12,7 @@
|
||||
|
||||
import logging
|
||||
import configparser
|
||||
import sys
|
||||
import nw
|
||||
|
||||
from os import path, mkdir, getcwd
|
||||
@@ -47,6 +48,7 @@ class Config:
|
||||
self.confPath = None
|
||||
self.confFile = None
|
||||
self.homePath = None
|
||||
self.lastPath = None
|
||||
self.appPath = None
|
||||
self.appRoot = None
|
||||
self.appIcon = None
|
||||
@@ -65,9 +67,6 @@ class Config:
|
||||
self.mainPanePos = [300, 800]
|
||||
self.docPanePos = [400, 400]
|
||||
|
||||
## Dialogs
|
||||
self.dlgTimeLine = [600, 400]
|
||||
|
||||
## Project
|
||||
self.autoSaveProj = 60
|
||||
self.autoSaveDoc = 30
|
||||
@@ -88,8 +87,8 @@ class Config:
|
||||
self.doReplaceDots = True
|
||||
self.wordCountTimer = 5.0
|
||||
|
||||
self.fmtSingleQuotes = ["‘","’"]
|
||||
self.fmtDoubleQuotes = ["“","”"]
|
||||
self.fmtSingleQuotes = ["\u2018","\u2019"]
|
||||
self.fmtDoubleQuotes = ["\u201c","\u201d"]
|
||||
|
||||
self.spellLanguage = "en_GB"
|
||||
|
||||
@@ -116,6 +115,30 @@ class Config:
|
||||
self.verPyQtPatch = verQt[2]
|
||||
self.verPyQtValue = verQt[3]
|
||||
|
||||
# Check Python Version
|
||||
self.verPyString = sys.version.split()[0]
|
||||
self.verPyMajor = sys.version_info[0]
|
||||
self.verPyMinor = sys.version_info[1]
|
||||
self.verPyPatch = sys.version_info[2]
|
||||
self.verPyHexVal = sys.hexversion
|
||||
|
||||
# Check OS Type
|
||||
self.osType = sys.platform
|
||||
self.osLinux = False
|
||||
self.osWindows = False
|
||||
self.osDarwin = False
|
||||
self.osUnknown = False
|
||||
if self.osType.startswith("linux"):
|
||||
self.osLinux = True
|
||||
elif self.osType.startswith("darwin"):
|
||||
self.osDarwin = True
|
||||
elif self.osType.startswith("win32"):
|
||||
self.osWindows = True
|
||||
elif self.osType.startswith("cygwin"):
|
||||
self.osWindows = True
|
||||
else:
|
||||
self.osUnknown = True
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -132,6 +155,7 @@ class Config:
|
||||
|
||||
self.confFile = self.appHandle+".conf"
|
||||
self.homePath = path.expanduser("~")
|
||||
self.lastPath = self.homePath
|
||||
self.appPath = path.dirname(__file__)
|
||||
self.appRoot = path.join(self.appPath,path.pardir)
|
||||
self.helpPath = path.join(self.appRoot,"help","en_GB")
|
||||
@@ -182,7 +206,6 @@ class Config:
|
||||
self.treeColWidth = self._parseLine(cnfParse, cnfSec, "treecols", self.CNF_LIST, self.treeColWidth)
|
||||
self.mainPanePos = self._parseLine(cnfParse, cnfSec, "mainpane", self.CNF_LIST, self.mainPanePos)
|
||||
self.docPanePos = self._parseLine(cnfParse, cnfSec, "docpane", self.CNF_LIST, self.docPanePos)
|
||||
self.dlgTimeLine = self._parseLine(cnfParse, cnfSec, "timeline", self.CNF_LIST, self.dlgTimeLine)
|
||||
|
||||
## Project
|
||||
cnfSec = "Project"
|
||||
@@ -216,6 +239,7 @@ class Config:
|
||||
|
||||
## Path
|
||||
cnfSec = "Path"
|
||||
self.lastPath = self._parseLine(cnfParse, cnfSec, "lastpath", self.CNF_STR, self.lastPath)
|
||||
for i in range(10):
|
||||
self.recentList[i] = self._parseLine(cnfParse, cnfSec, "recent%d" % i,self.CNF_STR, self.recentList[i])
|
||||
|
||||
@@ -245,7 +269,6 @@ class Config:
|
||||
cnfParse.set(cnfSec,"treecols", self._packList(self.treeColWidth))
|
||||
cnfParse.set(cnfSec,"mainpane", self._packList(self.mainPanePos))
|
||||
cnfParse.set(cnfSec,"docpane", self._packList(self.docPanePos))
|
||||
cnfParse.set(cnfSec,"timeline", self._packList(self.dlgTimeLine))
|
||||
|
||||
## Project
|
||||
cnfSec = "Project"
|
||||
@@ -283,6 +306,7 @@ class Config:
|
||||
## Path
|
||||
cnfSec = "Path"
|
||||
cnfParse.add_section(cnfSec)
|
||||
cnfParse.set(cnfSec,"lastpath", str(self.lastPath))
|
||||
for i in range(10):
|
||||
cnfParse.set(cnfSec,"recent%d" % i, str(self.recentList[i]))
|
||||
|
||||
@@ -317,6 +341,13 @@ class Config:
|
||||
self.confFile = path.basename(newPath)
|
||||
return True
|
||||
|
||||
def setLastPath(self, lastPath):
|
||||
if lastPath is None or lastPath == "":
|
||||
self.lastPath = ""
|
||||
else:
|
||||
self.lastPath = path.dirname(lastPath)
|
||||
return True
|
||||
|
||||
def setWinSize(self, newWidth, newHeight):
|
||||
if abs(self.winGeometry[0] - newWidth) > 5:
|
||||
self.winGeometry[0] = newWidth
|
||||
@@ -326,15 +357,6 @@ class Config:
|
||||
self.confChanged = True
|
||||
return True
|
||||
|
||||
def setTLineSize(self, newWidth, newHeight):
|
||||
if abs(self.dlgTimeLine[0] - newWidth) > 5:
|
||||
self.dlgTimeLine[0] = newWidth
|
||||
self.confChanged = True
|
||||
if abs(self.dlgTimeLine[1] - newHeight) > 5:
|
||||
self.dlgTimeLine[1] = newHeight
|
||||
self.confChanged = True
|
||||
return True
|
||||
|
||||
def setTreeColWidths(self, colWidths):
|
||||
self.treeColWidth = colWidths
|
||||
self.confChanged = True
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
|
||||
from nw.enum import nwItemClass, nwItemLayout
|
||||
|
||||
class nwConst():
|
||||
|
||||
tStampFmt = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
# END Class nwConst
|
||||
|
||||
class nwFiles():
|
||||
|
||||
APP_ICON = "novelWriter.svg"
|
||||
@@ -20,6 +26,8 @@ class nwFiles():
|
||||
SESS_INFO = "sessionInfo.log"
|
||||
INDEX_FILE = "tagsIndex.json"
|
||||
EXPORT_OPT = "exportOptions.json"
|
||||
TLINE_OPT = "timelineOptions.json"
|
||||
SLOG_OPT = "sessionLogOptions.json"
|
||||
|
||||
# END Class nwFiles
|
||||
|
||||
@@ -53,7 +61,7 @@ class nwLabels():
|
||||
nwItemLayout.BOOK : "Book",
|
||||
nwItemLayout.PAGE : "Plain Page",
|
||||
nwItemLayout.PARTITION : "Partition",
|
||||
nwItemLayout.UNNUMBERED : "Un-Numbered",
|
||||
nwItemLayout.UNNUMBERED : "Unnumbered",
|
||||
nwItemLayout.CHAPTER : "Chapter",
|
||||
nwItemLayout.SCENE : "Scene",
|
||||
nwItemLayout.NOTE : "Note",
|
||||
|
||||
77
nw/convert/file/concat.py
Normal file
77
nw/convert/file/concat.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Concatenated File
|
||||
|
||||
novelWriter – Concatenated File
|
||||
=================================
|
||||
Concatenate the standard novelWriter files to a single file
|
||||
|
||||
File History:
|
||||
Created: 2019-10-26 [0.3.1]
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from os import path
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from nw.convert.file.text import TextFile
|
||||
from nw.convert.tokenizer import Tokenizer
|
||||
from nw.enum import nwAlert, nwItemLayout
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ConcatFile(TextFile):
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
TextFile.__init__(self, theProject, theParent)
|
||||
|
||||
self.theConv = Tokenizer(self.theProject, self.theParent)
|
||||
|
||||
return
|
||||
|
||||
def addText(self, tHandle):
|
||||
|
||||
logger.verbose("Parsing content of item '%s'" % tHandle)
|
||||
|
||||
theItem = self.theProject.getItem(tHandle)
|
||||
isNone = theItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||
isNote = theItem.itemLayout == nwItemLayout.NOTE
|
||||
isNovel = not isNone and not isNote
|
||||
|
||||
if isNone:
|
||||
return False
|
||||
if isNote and not self.expNotes:
|
||||
return False
|
||||
if isNovel and not self.expNovel:
|
||||
return False
|
||||
|
||||
self.theConv.setText(tHandle)
|
||||
|
||||
theResult = self.theConv.theText
|
||||
|
||||
if theResult is not None and self.outFile is not None:
|
||||
self.outFile.write(theResult.rstrip())
|
||||
self.outFile.write("\n\n")
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _doOpenFile(self, filePath):
|
||||
try:
|
||||
self.outFile = open(filePath,mode="wt+")
|
||||
except Exception as e:
|
||||
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
|
||||
return False
|
||||
return True
|
||||
|
||||
def _doCloseFile(self):
|
||||
if self.outFile is not None:
|
||||
self.outFile.close()
|
||||
return True
|
||||
|
||||
# END Class ConcatFile
|
||||
69
nw/convert/file/html.py
Normal file
69
nw/convert/file/html.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter HTML File
|
||||
|
||||
novelWriter – HTML File
|
||||
=========================
|
||||
Writes the project to a html file
|
||||
|
||||
File History:
|
||||
Created: 2019-10-19 [0.3]
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from nw.convert.file.text import TextFile
|
||||
from nw.convert.text.tohtml import ToHtml
|
||||
from nw.enum import nwAlert
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class HtmlFile(TextFile):
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
TextFile.__init__(self, theProject, theParent)
|
||||
|
||||
self.theConv = ToHtml(self.theProject, self.theParent)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _doOpenFile(self, filePath):
|
||||
try:
|
||||
self.outFile = open(filePath,mode="wt+")
|
||||
self.outFile.write("<!DOCTYPE html>\n")
|
||||
self.outFile.write("<html>\n")
|
||||
self.outFile.write("<head>\n")
|
||||
self.outFile.write("<style>\n")
|
||||
self.outFile.write(" #page {\n")
|
||||
self.outFile.write(" margin: 40px auto;\n")
|
||||
self.outFile.write(" max-width: 769px;\n")
|
||||
self.outFile.write(" }\n")
|
||||
self.outFile.write(" .comment {\n")
|
||||
self.outFile.write(" background-color: #fbfabd;\n")
|
||||
self.outFile.write(" border: 1px solid #b4b000;\n")
|
||||
self.outFile.write(" margin: 10px 20px;\n")
|
||||
self.outFile.write(" padding: 6px;\n")
|
||||
self.outFile.write(" }\n")
|
||||
self.outFile.write("</style>\n")
|
||||
self.outFile.write("</head>\n")
|
||||
self.outFile.write("<body>\n")
|
||||
self.outFile.write("<article id='page'>\n")
|
||||
except Exception as e:
|
||||
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
|
||||
return False
|
||||
return True
|
||||
|
||||
def _doCloseFile(self):
|
||||
if self.outFile is not None:
|
||||
self.outFile.write("</article>\n")
|
||||
self.outFile.write("</body>\n")
|
||||
self.outFile.write("</html>\n")
|
||||
self.outFile.close()
|
||||
return True
|
||||
|
||||
# END Class HtmlFile
|
||||
56
nw/convert/file/latex.py
Normal file
56
nw/convert/file/latex.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter LaTeX File
|
||||
|
||||
novelWriter – LaTeX File
|
||||
==========================
|
||||
Writes the project to a LaTeX file
|
||||
|
||||
File History:
|
||||
Created: 2019-10-24 [0.3.1]
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from nw.convert.file.text import TextFile
|
||||
from nw.convert.text.tolatex import ToLaTeX
|
||||
from nw.enum import nwAlert
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class LaTeXFile(TextFile):
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
TextFile.__init__(self, theProject, theParent)
|
||||
|
||||
self.theConv = ToLaTeX(self.theProject, self.theParent)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _doOpenFile(self, filePath):
|
||||
|
||||
try:
|
||||
self.outFile = open(filePath,mode="wt+")
|
||||
self.outFile.write(r"\documentclass[12pt]{report}\n")
|
||||
self.outFile.write(r"\usepackage[utf8]{inputenc}\n")
|
||||
self.outFile.write(r"\begin{document}\n")
|
||||
except Exception as e:
|
||||
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _doCloseFile(self):
|
||||
|
||||
if self.outFile is not None:
|
||||
self.outFile.write(r"\end{document}\n")
|
||||
self.outFile.close()
|
||||
|
||||
return True
|
||||
|
||||
# END Class LaTeXFile
|
||||
48
nw/convert/file/markdown.py
Normal file
48
nw/convert/file/markdown.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Markdown File
|
||||
|
||||
novelWriter – Markdown File
|
||||
=============================
|
||||
Writes the project to a markdown file
|
||||
|
||||
File History:
|
||||
Created: 2019-10-19 [0.3]
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from nw.convert.file.text import TextFile
|
||||
from nw.convert.text.tomarkdown import ToMarkdown
|
||||
from nw.enum import nwAlert
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MarkdownFile(TextFile):
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
TextFile.__init__(self, theProject, theParent)
|
||||
|
||||
self.theConv = ToMarkdown(self.theProject, self.theParent)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
def _doOpenFile(self, filePath):
|
||||
try:
|
||||
self.outFile = open(filePath,mode="wt+")
|
||||
except Exception as e:
|
||||
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
|
||||
return False
|
||||
return True
|
||||
|
||||
def _doCloseFile(self):
|
||||
if self.outFile is not None:
|
||||
self.outFile.close()
|
||||
return True
|
||||
|
||||
# END Class MarkdownFile
|
||||
@@ -16,8 +16,8 @@ import nw
|
||||
from os import path
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from nw.convert.tokenizer import Tokenizer
|
||||
from nw.enum import nwAlert, nwItemLayout
|
||||
from nw.convert.text.totext import ToText
|
||||
from nw.enum import nwAlert, nwItemLayout
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,16 +28,14 @@ class TextFile():
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theProject = theProject
|
||||
self.theParent = theParent
|
||||
self.fileExt = "txt"
|
||||
|
||||
self.outFile = None
|
||||
self.fileName = ""
|
||||
self.theText = ""
|
||||
self.expNovel = True
|
||||
self.expNotes = False
|
||||
self.winEnding = False
|
||||
|
||||
self.theConv = Tokenizer(self.theProject, self.theParent)
|
||||
self.theConv = ToText(self.theProject, self.theParent)
|
||||
self.makeAlert = self.theParent.makeAlert
|
||||
|
||||
self.setComments(False)
|
||||
@@ -141,9 +139,6 @@ class TextFile():
|
||||
self.theConv.doHeaders()
|
||||
self.theConv.doConvert()
|
||||
|
||||
if self.winEnding:
|
||||
self.theConv.windowsEndings()
|
||||
|
||||
if self.theConv.theResult is not None and self.outFile is not None:
|
||||
self.outFile.write(self.theConv.theResult)
|
||||
|
||||
@@ -158,18 +153,15 @@ class TextFile():
|
||||
that uses a different file format that requires a different approach.
|
||||
"""
|
||||
try:
|
||||
self.outFile = open(filePath,mode="w+")
|
||||
if self.winEnding:
|
||||
self.outFile.write("\r\n\r\n")
|
||||
else:
|
||||
self.outFile.write("\n\n")
|
||||
self.outFile = open(filePath,mode="wt+")
|
||||
self.outFile.write("\n\n")
|
||||
except Exception as e:
|
||||
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
|
||||
return False
|
||||
return True
|
||||
|
||||
def _doCloseFile(self):
|
||||
"""This function closes a file, and is meant to be overloaded by the subclass for other
|
||||
"""This function closes the file, and is meant to be overloaded by the subclass for other
|
||||
file formats.
|
||||
"""
|
||||
if self.outFile is not None:
|
||||
@@ -22,16 +22,19 @@ class ToHtml(Tokenizer):
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
Tokenizer.__init__(self, theProject, theParent)
|
||||
|
||||
return
|
||||
|
||||
def doAutoReplace(self):
|
||||
Tokenizer.doAutoReplace(self)
|
||||
|
||||
repDict = {
|
||||
"<" : "<",
|
||||
">" : ">",
|
||||
"&" : "&",
|
||||
"<" : "<",
|
||||
">" : ">",
|
||||
"&" : "&",
|
||||
"\u2013" : "&endash;",
|
||||
"\u2014" : "$emdash;",
|
||||
"\u2500" : "$emdash;",
|
||||
"\u2026" : "…",
|
||||
}
|
||||
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
|
||||
self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText)
|
||||
@@ -53,24 +56,47 @@ class ToHtml(Tokenizer):
|
||||
thisPar = []
|
||||
for tType, tText, tFormat, tAlign in self.theTokens:
|
||||
|
||||
aStyle = []
|
||||
if tAlign == self.A_CENTRE:
|
||||
aStyle.append("text-align: center;")
|
||||
|
||||
if len(aStyle) > 0:
|
||||
hStyle = " style='%s'" % (" ".join(aStyle))
|
||||
else:
|
||||
hStyle = ""
|
||||
|
||||
if tType == self.T_EMPTY:
|
||||
if len(thisPar) > 0:
|
||||
self.theResult += "<p>%s</p>\n" % " ".join(thisPar)
|
||||
self.theResult += "<p%s>%s</p>\n" % (hStyle," ".join(thisPar))
|
||||
thisPar = []
|
||||
|
||||
elif tType == self.T_HEAD1:
|
||||
self.theResult += "<h1>%s</h1>\n" % tText
|
||||
self.theResult += "<h1%s>%s</h1>\n" % (hStyle,tText)
|
||||
|
||||
elif tType == self.T_HEAD2:
|
||||
self.theResult += "<h2>%s</h2>\n" % tText
|
||||
self.theResult += "<h2%s>%s</h2>\n" % (hStyle,tText)
|
||||
|
||||
elif tType == self.T_HEAD3:
|
||||
self.theResult += "<h3>%s</h3>\n" % tText
|
||||
self.theResult += "<h3%s>%s</h3>\n" % (hStyle,tText)
|
||||
|
||||
elif tType == self.T_HEAD4:
|
||||
self.theResult += "<h4>%s</h4>\n" % tText
|
||||
self.theResult += "<h4%s>%s</h4>\n" % (hStyle,tText)
|
||||
|
||||
elif tType == self.T_SEP:
|
||||
self.theResult += "<div%s>%s</div>\n" % (hStyle,tText)
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
tTemp = tText
|
||||
for xPos, xLen, xFmt in reversed(tFormat):
|
||||
tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:]
|
||||
thisPar.append(tTemp)
|
||||
|
||||
elif tType == self.T_COMMENT and self.doComments:
|
||||
self.theResult += "<div class='comment'>%s</div>\n" % tText
|
||||
|
||||
elif tType == self.T_COMMAND and self.doCommands:
|
||||
self.theResult += "<pre>%s</pre>\n" % tText
|
||||
|
||||
# print(self.theResult)
|
||||
|
||||
return
|
||||
155
nw/convert/text/tolatex.py
Normal file
155
nw/convert/text/tolatex.py
Normal file
@@ -0,0 +1,155 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter LaTeX Converter
|
||||
|
||||
novelWriter – LaTeX Converter
|
||||
===============================
|
||||
Extends the Tokenizer class to write LaTeX
|
||||
|
||||
File History:
|
||||
Created: 2019-10-24 [0.3.1]
|
||||
|
||||
"""
|
||||
|
||||
import textwrap
|
||||
import logging
|
||||
import re
|
||||
import nw
|
||||
|
||||
from nw.convert.tokenizer import Tokenizer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ToLaTeX(Tokenizer):
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
Tokenizer.__init__(self, theProject, theParent)
|
||||
return
|
||||
|
||||
def doAutoReplace(self):
|
||||
Tokenizer.doAutoReplace(self)
|
||||
|
||||
repDict = {
|
||||
"\u2013" : "--",
|
||||
"\u2014" : "---",
|
||||
"\u2500" : "---",
|
||||
"\u2026" : "...",
|
||||
}
|
||||
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
|
||||
self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText)
|
||||
|
||||
return
|
||||
|
||||
def doConvert(self):
|
||||
|
||||
texTags = {
|
||||
self.FMT_B_B : r"\textbf{",
|
||||
self.FMT_B_E : r"}",
|
||||
self.FMT_I_B : r"\textit{",
|
||||
self.FMT_I_E : r"}",
|
||||
self.FMT_U_B : r"\underline{",
|
||||
self.FMT_U_E : r"}",
|
||||
}
|
||||
|
||||
if self.wordWrap > 0:
|
||||
tWrap = textwrap.TextWrapper(
|
||||
width = self.wordWrap,
|
||||
initial_indent = "",
|
||||
subsequent_indent = "",
|
||||
expand_tabs = True,
|
||||
replace_whitespace = True,
|
||||
fix_sentence_endings = False,
|
||||
break_long_words = True,
|
||||
drop_whitespace = True,
|
||||
break_on_hyphens = True,
|
||||
tabsize = 8,
|
||||
max_lines = None
|
||||
)
|
||||
tComm = textwrap.TextWrapper(
|
||||
width = self.wordWrap-2,
|
||||
initial_indent = "",
|
||||
subsequent_indent = "",
|
||||
expand_tabs = True,
|
||||
replace_whitespace = True,
|
||||
fix_sentence_endings = False,
|
||||
break_long_words = True,
|
||||
drop_whitespace = True,
|
||||
break_on_hyphens = True,
|
||||
tabsize = 8,
|
||||
max_lines = None
|
||||
)
|
||||
|
||||
self.theResult = ""
|
||||
thisPar = []
|
||||
for tType, tText, tFormat, tAlign in self.theTokens:
|
||||
|
||||
begText = ""
|
||||
endText = "\n"
|
||||
if tAlign == self.A_CENTRE:
|
||||
begText = "\\begin{center}\n"
|
||||
endText = "\\end{center}\n\n"
|
||||
|
||||
# First check if we have a comment or plain text, as they need some
|
||||
# extra replacing before we proceed to wrapping and final formatting.
|
||||
if tType == self.T_COMMENT:
|
||||
tText = "%% %s" % tText
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
tTemp = tText
|
||||
for xPos, xLen, xFmt in reversed(tFormat):
|
||||
tTemp = tTemp[:xPos]+texTags[xFmt]+tTemp[xPos+xLen:]
|
||||
tText = tTemp
|
||||
|
||||
tLen = len(tText)
|
||||
|
||||
# The text can now be word wrapped, if we have requested this and it's needed.
|
||||
if self.wordWrap > 0 and tLen > self.wordWrap:
|
||||
if tType == self.T_COMMENT:
|
||||
aText = tComm.wrap(tText)
|
||||
tText = "\n% ".join(aText)
|
||||
else:
|
||||
tText = tWrap.fill(tText)
|
||||
|
||||
# Then the text can receive final formatting before we append it to the results.
|
||||
# We also store text lines in a buffer and merge them only when we find an empty line,
|
||||
# indicating a new paragraph.
|
||||
if tType == self.T_EMPTY:
|
||||
if len(thisPar) > 0:
|
||||
self.theResult += begText
|
||||
self.theResult += "%s\n" % tWrap.fill(" ".join(thisPar))
|
||||
self.theResult += endText
|
||||
thisPar = []
|
||||
|
||||
elif tType == self.T_HEAD1:
|
||||
self.theResult += begText
|
||||
self.theResult += "{\\Huge %s}\n" % tText
|
||||
self.theResult += endText
|
||||
|
||||
elif tType == self.T_HEAD2:
|
||||
self.theResult += "\\chapter*{%s}\n\n" % tText
|
||||
|
||||
elif tType == self.T_HEAD3:
|
||||
self.theResult += "\\section*{%s}\n\n" % tText
|
||||
|
||||
elif tType == self.T_HEAD4:
|
||||
self.theResult += "\\subsection*{%s}\n\n" % tText
|
||||
|
||||
elif tType == self.T_SEP:
|
||||
self.theResult += begText
|
||||
self.theResult += "%s\n" % tText
|
||||
self.theResult += endText
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
thisPar.append(tText)
|
||||
|
||||
elif tType == self.T_PBREAK:
|
||||
self.theResult += "\\newpage\n\n"
|
||||
|
||||
elif tType == self.T_COMMENT and self.doComments:
|
||||
self.theResult += "%s\n\n" % tText
|
||||
|
||||
elif tType == self.T_COMMAND and self.doCommands:
|
||||
self.theResult += "%% @%s\n\n" % tText
|
||||
|
||||
return
|
||||
|
||||
# END Class ToLaTeX
|
||||
112
nw/convert/text/tomarkdown.py
Normal file
112
nw/convert/text/tomarkdown.py
Normal file
@@ -0,0 +1,112 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Markdown Text Converter
|
||||
|
||||
novelWriter – Markdown Text Converter
|
||||
=======================================
|
||||
Extends the Tokenizer class to write Markdown
|
||||
|
||||
File History:
|
||||
Created: 2019-10-19 [0.3]
|
||||
|
||||
"""
|
||||
|
||||
import textwrap
|
||||
import logging
|
||||
import re
|
||||
import nw
|
||||
|
||||
from nw.convert.tokenizer import Tokenizer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ToMarkdown(Tokenizer):
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
Tokenizer.__init__(self, theProject, theParent)
|
||||
return
|
||||
|
||||
def doConvert(self):
|
||||
|
||||
mdTags = {
|
||||
self.FMT_B_B : "**",
|
||||
self.FMT_B_E : "**",
|
||||
self.FMT_I_B : "_",
|
||||
self.FMT_I_E : "_",
|
||||
self.FMT_U_B : "__",
|
||||
self.FMT_U_E : "__",
|
||||
}
|
||||
|
||||
if self.wordWrap > 0:
|
||||
tWrap = textwrap.TextWrapper(
|
||||
width = self.wordWrap,
|
||||
initial_indent = "",
|
||||
subsequent_indent = "",
|
||||
expand_tabs = True,
|
||||
replace_whitespace = True,
|
||||
fix_sentence_endings = False,
|
||||
break_long_words = True,
|
||||
drop_whitespace = True,
|
||||
break_on_hyphens = True,
|
||||
tabsize = 8,
|
||||
max_lines = None
|
||||
)
|
||||
|
||||
self.theResult = ""
|
||||
thisPar = []
|
||||
for tType, tText, tFormat, tAlign in self.theTokens:
|
||||
|
||||
# First check if we have a comment or plain text, as they need some
|
||||
# extra replacing before we proceed to wrapping and final formatting.
|
||||
if tType == self.T_COMMENT:
|
||||
tText = " %s" % tText
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
tTemp = tText
|
||||
for xPos, xLen, xFmt in reversed(tFormat):
|
||||
tTemp = tTemp[:xPos]+mdTags[xFmt]+tTemp[xPos+xLen:]
|
||||
tText = tTemp
|
||||
|
||||
tLen = len(tText)
|
||||
|
||||
# The text can now be word wrapped, if we have requested this and it's needed.
|
||||
if self.wordWrap > 0 and tLen > self.wordWrap:
|
||||
if tType == self.T_COMMENT:
|
||||
tText = textwrap.fill(tText.strip(),initial_indent=" ",subsequent_indent=" ")
|
||||
else:
|
||||
tText = tWrap.fill(tText)
|
||||
|
||||
# Then the text can receive final formatting before we append it to the results.
|
||||
# We also store text lines in a buffer and merge them only when we find an empty line,
|
||||
# indicating a new paragraph.
|
||||
if tType == self.T_EMPTY:
|
||||
if len(thisPar) > 0:
|
||||
self.theResult += "%s\n\n" % " ".join(thisPar)
|
||||
thisPar = []
|
||||
|
||||
elif tType == self.T_HEAD1:
|
||||
self.theResult += "# %s\n\n" % tText
|
||||
|
||||
elif tType == self.T_HEAD2:
|
||||
self.theResult += "## %s\n\n" % tText
|
||||
|
||||
elif tType == self.T_HEAD3:
|
||||
self.theResult += "### %s\n\n" % tText
|
||||
|
||||
elif tType == self.T_HEAD4:
|
||||
self.theResult += "#### %s\n\n" % tText
|
||||
|
||||
elif tType == self.T_SEP:
|
||||
self.theResult += "%s\n\n" % tText
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
thisPar.append(tText)
|
||||
|
||||
elif tType == self.T_COMMENT and self.doComments:
|
||||
self.theResult += "%s\n\n" % tText
|
||||
|
||||
elif tType == self.T_COMMAND and self.doCommands:
|
||||
self.theResult += "%s\n\n" % tText
|
||||
|
||||
return
|
||||
|
||||
# END Class ToMarkdown
|
||||
119
nw/convert/text/totext.py
Normal file
119
nw/convert/text/totext.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Plain Text Converter
|
||||
|
||||
novelWriter – Plain Text Converter
|
||||
====================================
|
||||
Extends the Tokenizer class to convert to plain text
|
||||
|
||||
File History:
|
||||
Created: 2019-10-26 [0.3.1]
|
||||
|
||||
"""
|
||||
|
||||
import textwrap
|
||||
import logging
|
||||
import re
|
||||
import nw
|
||||
|
||||
from nw.convert.tokenizer import Tokenizer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ToText(Tokenizer):
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
Tokenizer.__init__(self, theProject, theParent)
|
||||
return
|
||||
|
||||
def doConvert(self):
|
||||
"""Converts the tokenized text into plain text.
|
||||
"""
|
||||
|
||||
if self.wordWrap > 0:
|
||||
tWrap = textwrap.TextWrapper(
|
||||
width = self.wordWrap,
|
||||
initial_indent = "",
|
||||
subsequent_indent = "",
|
||||
expand_tabs = True,
|
||||
replace_whitespace = True,
|
||||
fix_sentence_endings = False,
|
||||
break_long_words = True,
|
||||
drop_whitespace = True,
|
||||
break_on_hyphens = True,
|
||||
tabsize = 8,
|
||||
max_lines = None
|
||||
)
|
||||
|
||||
self.theResult = ""
|
||||
thisPar = []
|
||||
for tType, tText, tFormat, tAlign in self.theTokens:
|
||||
|
||||
# First check if we have a comment or plain text, as they need some
|
||||
# extra replacing before we proceed to wrapping and final formatting.
|
||||
if tType == self.T_COMMENT:
|
||||
tText = "[%s]" % tText
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
tTemp = tText
|
||||
for xPos, xLen, xFmt in reversed(tFormat):
|
||||
tTemp = tTemp[:xPos]+tTemp[xPos+xLen:]
|
||||
tText = tTemp
|
||||
|
||||
tLen = len(tText)
|
||||
|
||||
# The text can now be word wrapped, if we have requested this and it's needed.
|
||||
if tAlign == self.A_CENTRE:
|
||||
if self.wordWrap > 0:
|
||||
if tLen > self.wordWrap:
|
||||
aText = tWrap.wrap(tText)
|
||||
for n in range(len(aText)):
|
||||
aText[n] = self._centreText(aText[n],self.wordWrap)
|
||||
tText = "\n".join(aText)
|
||||
else:
|
||||
tText = self._centreText(tText,self.wordWrap)
|
||||
else:
|
||||
if self.wordWrap > 0 and tLen > self.wordWrap:
|
||||
tText = tWrap.fill(tText)
|
||||
|
||||
# Then the text can receive final formatting before we append it to the results.
|
||||
# We also store text lines in a buffer and merge them only when we find an empty line,
|
||||
# indicating a new paragraph.
|
||||
if tType == self.T_EMPTY:
|
||||
if len(thisPar) > 0:
|
||||
self.theResult += "%s\n\n" % " ".join(thisPar)
|
||||
thisPar = []
|
||||
|
||||
elif tType == self.T_HEAD1:
|
||||
uLine = "="*min(tLen,self.wordWrap)
|
||||
if tAlign == self.A_CENTRE:
|
||||
uLine = self._centreText(uLine,self.wordWrap)
|
||||
self.theResult += "%s\n%s\n\n" % (tText,uLine)
|
||||
|
||||
elif tType == self.T_HEAD2:
|
||||
uLine = "~"*min(tLen,self.wordWrap)
|
||||
self.theResult += "%s\n%s\n\n" % (tText,uLine)
|
||||
|
||||
elif tType == self.T_HEAD3:
|
||||
uLine = "-"*min(tLen,self.wordWrap)
|
||||
self.theResult += "%s\n%s\n\n" % (tText,uLine)
|
||||
|
||||
elif tType == self.T_HEAD4:
|
||||
self.theResult += "%s\n\n" % tText
|
||||
|
||||
elif tType == self.T_SEP:
|
||||
if self.wordWrap > 0 and tLen < self.wordWrap:
|
||||
tText = self._centreText(tText,self.wordWrap)
|
||||
self.theResult += "%s\n\n" % tText
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
thisPar.append(tText)
|
||||
|
||||
elif tType == self.T_COMMENT and self.doComments:
|
||||
self.theResult += "%s\n\n" % tText
|
||||
|
||||
elif tType == self.T_COMMAND and self.doCommands:
|
||||
self.theResult += "%s\n\n" % tText
|
||||
|
||||
return
|
||||
|
||||
# END Class ToText
|
||||
@@ -26,27 +26,28 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class Tokenizer():
|
||||
|
||||
FMT_B_B = "" # Begin bold
|
||||
FMT_B_E = "" # End bold
|
||||
FMT_I_B = "" # Begin italics
|
||||
FMT_I_E = "" # End italics
|
||||
FMT_U_B = "" # Begin underline
|
||||
FMT_U_E = "" # End underline
|
||||
FMT_B_B = 1 # Begin bold
|
||||
FMT_B_E = 2 # End bold
|
||||
FMT_I_B = 3 # Begin italics
|
||||
FMT_I_E = 4 # End italics
|
||||
FMT_U_B = 5 # Begin underline
|
||||
FMT_U_E = 6 # End underline
|
||||
|
||||
T_EMPTY = 1 # Empty line (new paragraph)
|
||||
T_COMMENT = 2 # Comment line
|
||||
T_COMMAND = 3 # Command line
|
||||
T_HEAD1 = 4 # Header 1 (title)
|
||||
T_HEAD2 = 5 # Header 2 (chapter)
|
||||
T_HEAD3 = 6 # Header 3 (scene)
|
||||
T_HEAD4 = 7 # Header 4
|
||||
T_TEXT = 8 # Text line
|
||||
T_SEP = 9 # Scene separator
|
||||
T_EMPTY = 1 # Empty line (new paragraph)
|
||||
T_COMMENT = 2 # Comment line
|
||||
T_COMMAND = 3 # Command line
|
||||
T_HEAD1 = 4 # Header 1 (title)
|
||||
T_HEAD2 = 5 # Header 2 (chapter)
|
||||
T_HEAD3 = 6 # Header 3 (scene)
|
||||
T_HEAD4 = 7 # Header 4
|
||||
T_TEXT = 8 # Text line
|
||||
T_SEP = 9 # Scene separator
|
||||
T_PBREAK = 10 # Page break
|
||||
|
||||
A_LEFT = 1 # Left aligned
|
||||
A_RIGHT = 2 # Right aligned
|
||||
A_CENTRE = 3 # Centred
|
||||
A_JUSTIFY = 4 # Justified
|
||||
A_LEFT = 1 # Left aligned
|
||||
A_RIGHT = 2 # Right aligned
|
||||
A_CENTRE = 3 # Centred
|
||||
A_JUSTIFY = 4 # Justified
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
|
||||
@@ -247,7 +248,7 @@ class Tokenizer():
|
||||
if self.firstScene:
|
||||
self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT)
|
||||
else:
|
||||
self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_LEFT)
|
||||
self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_CENTRE)
|
||||
else:
|
||||
self.theTokens[n] = (tType,tTemp,None,self.A_LEFT)
|
||||
self.firstScene = False
|
||||
@@ -257,11 +258,12 @@ class Tokenizer():
|
||||
if tTemp == "":
|
||||
self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT)
|
||||
elif tTemp == self.fmtSection:
|
||||
self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_LEFT)
|
||||
self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_CENTRE)
|
||||
else:
|
||||
self.theTokens[n] = (tType,tTemp,None,self.A_LEFT)
|
||||
|
||||
# For title page and partitions, we need to centre all text
|
||||
# and for some formats, we need a page break
|
||||
if isTitle or isPart:
|
||||
for n in range(len(self.theTokens)):
|
||||
tToken = self.theTokens[n]
|
||||
@@ -270,101 +272,8 @@ class Tokenizer():
|
||||
tFormat = tToken[2]
|
||||
self.theTokens[n] = (tType,tText,tFormat,self.A_CENTRE)
|
||||
|
||||
return
|
||||
self.theTokens[n] = (self.T_PBREAK,"",None,self.A_LEFT)
|
||||
|
||||
def doConvert(self):
|
||||
"""Converts the tokenized text into plain text.
|
||||
"""
|
||||
|
||||
if self.wordWrap > 0:
|
||||
tWrap = textwrap.TextWrapper(
|
||||
width = self.wordWrap,
|
||||
initial_indent = "",
|
||||
subsequent_indent = "",
|
||||
expand_tabs = True,
|
||||
replace_whitespace = True,
|
||||
fix_sentence_endings = False,
|
||||
break_long_words = True,
|
||||
drop_whitespace = True,
|
||||
break_on_hyphens = True,
|
||||
tabsize = 8,
|
||||
max_lines = None
|
||||
)
|
||||
|
||||
self.theResult = ""
|
||||
thisPar = []
|
||||
for tType, tText, tFormat, tAlign in self.theTokens:
|
||||
|
||||
# First check if we have a comment or plain text, as they need some
|
||||
# extra replacing before we proceed to wrapping and final formatting.
|
||||
if tType == self.T_COMMENT:
|
||||
tText = "[%s]" % tText
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
tTemp = tText
|
||||
for xPos, xLen, xFmt in reversed(tFormat):
|
||||
tTemp = tTemp[:xPos]+tTemp[xPos+xLen:]
|
||||
tText = tTemp
|
||||
|
||||
tLen = len(tText)
|
||||
|
||||
# The text can now be word wrapped, if we have requested this and it's needed.
|
||||
if tAlign == self.A_CENTRE:
|
||||
if self.wordWrap > 0:
|
||||
if tLen > self.wordWrap:
|
||||
aText = tWrap.wrap(tText)
|
||||
for n in range(len(aText)):
|
||||
aText[n] = self._centreText(aText[n],self.wordWrap)
|
||||
tText = "\n".join(aText)
|
||||
else:
|
||||
tText = self._centreText(tText,self.wordWrap)
|
||||
else:
|
||||
if self.wordWrap > 0 and tLen > self.wordWrap:
|
||||
tText = tWrap.fill(tText)
|
||||
|
||||
# Then the text can receive final formatting before we append it to the results.
|
||||
# We also store text lines in a buffer and merge them only when we find an empty line,
|
||||
# indicating a new paragraph.
|
||||
if tType == self.T_EMPTY:
|
||||
if len(thisPar) > 0:
|
||||
self.theResult += "%s\n\n" % " ".join(thisPar)
|
||||
thisPar = []
|
||||
|
||||
elif tType == self.T_HEAD1:
|
||||
uLine = "="*min(tLen,self.wordWrap)
|
||||
if tAlign == self.A_CENTRE:
|
||||
uLine = self._centreText(uLine,self.wordWrap)
|
||||
self.theResult += "%s\n%s\n\n" % (tText,uLine)
|
||||
|
||||
elif tType == self.T_HEAD2:
|
||||
uLine = "~"*min(tLen,self.wordWrap)
|
||||
self.theResult += "%s\n%s\n\n" % (tText,uLine)
|
||||
|
||||
elif tType == self.T_HEAD3:
|
||||
uLine = "-"*min(tLen,self.wordWrap)
|
||||
self.theResult += "%s\n%s\n\n" % (tText,uLine)
|
||||
|
||||
elif tType == self.T_HEAD4:
|
||||
self.theResult += "%s\n\n" % tText
|
||||
|
||||
elif tType == self.T_SEP:
|
||||
if self.wordWrap > 0 and tLen < self.wordWrap:
|
||||
tText = self._centreText(tText,self.wordWrap)
|
||||
self.theResult += "%s\n\n" % tText
|
||||
|
||||
elif tType == self.T_TEXT:
|
||||
thisPar.append(tText)
|
||||
|
||||
elif tType == self.T_COMMENT and self.doComments:
|
||||
self.theResult += "%s\n\n" % tText
|
||||
|
||||
elif tType == self.T_COMMAND and self.doCommands:
|
||||
self.theResult += "%s\n\n" % tText
|
||||
|
||||
return
|
||||
|
||||
def windowsEndings(self):
|
||||
self.theResult = self.theResult.replace("\n","\r\n")
|
||||
return
|
||||
|
||||
##
|
||||
|
||||
@@ -259,7 +259,7 @@ class GuiConfigEditGeneral(QWidget):
|
||||
self.mainConf.backupOnClose = backupOnClose
|
||||
self.mainConf.askBeforeBackup = askBeforeBackup
|
||||
|
||||
self.mainConf.confChanged = True
|
||||
self.mainConf.confChanged = True
|
||||
|
||||
return validEntries, needsRestart
|
||||
|
||||
|
||||
@@ -289,6 +289,9 @@ class GuiDocEditor(QTextEdit):
|
||||
return False
|
||||
return True
|
||||
|
||||
def isEmpty(self):
|
||||
return self.qDocument.isEmpty()
|
||||
|
||||
##
|
||||
# Document Events and Maintenance
|
||||
##
|
||||
@@ -419,15 +422,15 @@ class GuiDocEditor(QTextEdit):
|
||||
|
||||
elif self.mainConf.doReplaceDash and theTwo == "--":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2)
|
||||
theCursor.insertText("–")
|
||||
theCursor.insertText("\u2013")
|
||||
|
||||
elif self.mainConf.doReplaceDash and theTwo == "–-":
|
||||
elif self.mainConf.doReplaceDash and theTwo == "\u2013-":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 2)
|
||||
theCursor.insertText("—")
|
||||
theCursor.insertText("\u2014")
|
||||
|
||||
elif self.mainConf.doReplaceDots and theThree == "...":
|
||||
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, 3)
|
||||
theCursor.insertText("…")
|
||||
theCursor.insertText("\u2026")
|
||||
|
||||
return
|
||||
|
||||
|
||||
@@ -185,6 +185,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
# Build a QRegExp for each pattern and for the spell checker
|
||||
self.rules = [(QRegularExpression(a),b) for (a,b) in self.hRules]
|
||||
self.spellRx = QRegularExpression(r"\b[^\s]+\b")
|
||||
self.spellRx.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -98,8 +98,16 @@ class GuiDocTree(QTreeWidget):
|
||||
|
||||
if itemClass is None and pHandle is not None:
|
||||
itemClass = self.theProject.getItem(pHandle).itemClass
|
||||
|
||||
if itemClass is None:
|
||||
self.makeAlert("Failed to find an appropriate item class for item %s" % pHandle, nwAlert.BUG)
|
||||
if itemType is not None:
|
||||
if itemType == nwItemType.FILE:
|
||||
self.makeAlert("Please select a location in the tree to add a document.", nwAlert.ERROR)
|
||||
return False
|
||||
elif itemType == nwItemType.FOLDER:
|
||||
self.makeAlert("Please select a location in the tree to add a folder.", nwAlert.ERROR)
|
||||
return False
|
||||
self.makeAlert("Failed to add new item.", nwAlert.BUG)
|
||||
return False
|
||||
|
||||
logger.verbose("Adding new item of type %s and class %s to handle %s" % (
|
||||
@@ -418,9 +426,10 @@ class GuiDocTree(QTreeWidget):
|
||||
return
|
||||
|
||||
def _cleanOrphanedRoot(self):
|
||||
if self.orphRoot.childCount() == 0:
|
||||
self.takeTopLevelItem(self.indexOfTopLevelItem(self.orphRoot))
|
||||
self.orphRoot = None
|
||||
if self.orphRoot is not None:
|
||||
if self.orphRoot.childCount() == 0:
|
||||
self.takeTopLevelItem(self.indexOfTopLevelItem(self.orphRoot))
|
||||
self.orphRoot = None
|
||||
return
|
||||
|
||||
def _updateItemParent(self, tHandle):
|
||||
|
||||
@@ -13,13 +13,13 @@
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from PyQt5.QtCore import Qt, QUrl
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QTextBrowser
|
||||
from PyQt5.QtGui import QTextOption, QFont, QPalette, QColor
|
||||
|
||||
from nw.convert.tokenizer import Tokenizer
|
||||
from nw.convert.tohtml import ToHtml
|
||||
from nw.enum import nwItemType
|
||||
from nw.convert.tokenizer import Tokenizer
|
||||
from nw.convert.text.tohtml import ToHtml
|
||||
from nw.enum import nwItemType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -91,10 +91,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
|
||||
def loadText(self, tHandle):
|
||||
|
||||
if tHandle == "Help":
|
||||
self.loadHelp()
|
||||
return True
|
||||
|
||||
tItem = self.theProject.getItem(tHandle)
|
||||
if tItem is None:
|
||||
logger.warning("Item not found")
|
||||
@@ -118,14 +114,6 @@ class GuiDocViewer(QTextBrowser):
|
||||
|
||||
return True
|
||||
|
||||
def loadHelp(self):
|
||||
|
||||
self.clearViewer()
|
||||
self.setSearchPaths([self.mainConf.helpPath])
|
||||
self.setSource(QUrl("index.html"))
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
|
||||
125
nw/gui/export.py
125
nw/gui/export.py
@@ -12,7 +12,6 @@
|
||||
|
||||
import logging
|
||||
import time
|
||||
import json
|
||||
import nw
|
||||
|
||||
from os import path
|
||||
@@ -24,12 +23,16 @@ from PyQt5.QtWidgets import (
|
||||
QLabel, QComboBox, QLineEdit, QPushButton, QFileDialog, QProgressBar, QSpinBox
|
||||
)
|
||||
|
||||
from nw.project.document import NWDoc
|
||||
from nw.tools.translate import numberToWord
|
||||
from nw.convert.textfile import TextFile
|
||||
from nw.common import checkString, checkBool, checkInt
|
||||
from nw.constants import nwFiles
|
||||
from nw.enum import nwItemType
|
||||
from nw.project.document import NWDoc
|
||||
from nw.tools.translate import numberToWord
|
||||
from nw.tools.optlaststate import OptLastState
|
||||
from nw.convert.file.text import TextFile
|
||||
from nw.convert.file.html import HtmlFile
|
||||
from nw.convert.file.markdown import MarkdownFile
|
||||
from nw.convert.file.latex import LaTeXFile
|
||||
from nw.convert.file.concat import ConcatFile
|
||||
from nw.constants import nwFiles
|
||||
from nw.enum import nwItemType
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -43,7 +46,8 @@ class GuiExport(QDialog):
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theParent = theParent
|
||||
self.theProject = theProject
|
||||
self.optState = ExportLastState(self.theProject)
|
||||
self.optState = ExportLastState(self.theProject,nwFiles.EXPORT_OPT)
|
||||
self.optState.loadSettings()
|
||||
|
||||
self.outerBox = QHBoxLayout()
|
||||
self.innerBox = QVBoxLayout()
|
||||
@@ -121,6 +125,14 @@ class GuiExport(QDialog):
|
||||
outFile = None
|
||||
if eFormat == GuiExportMain.FMT_TXT:
|
||||
outFile = TextFile(self.theProject, self.theParent)
|
||||
elif eFormat == GuiExportMain.FMT_MD:
|
||||
outFile = MarkdownFile(self.theProject, self.theParent)
|
||||
elif eFormat == GuiExportMain.FMT_HTML:
|
||||
outFile = HtmlFile(self.theProject, self.theParent)
|
||||
elif eFormat == GuiExportMain.FMT_TEX:
|
||||
outFile = LaTeXFile(self.theProject, self.theParent)
|
||||
elif eFormat == GuiExportMain.FMT_NWD:
|
||||
outFile = ConcatFile(self.theProject, self.theParent)
|
||||
|
||||
if outFile is None:
|
||||
return False
|
||||
@@ -138,11 +150,11 @@ class GuiExport(QDialog):
|
||||
self.exportStatus.setText("Failed to open file for writing ...")
|
||||
return False
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
nDone = 0
|
||||
for tHandle in self.theProject.treeOrder:
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
self.exportProgress.setValue(nDone)
|
||||
tItem = self.theProject.getItem(tHandle)
|
||||
|
||||
@@ -197,13 +209,15 @@ class GuiExport(QDialog):
|
||||
|
||||
class GuiExportMain(QWidget):
|
||||
|
||||
FMT_TXT = 1
|
||||
FMT_MD = 2
|
||||
FMT_HTML = 3
|
||||
FMT_EBOOK = 4
|
||||
FMT_ODT = 5
|
||||
FMT_TEX = 6
|
||||
FMT_NWD = 1 # novelWriter markdown
|
||||
FMT_TXT = 2 # Plain text file
|
||||
FMT_MD = 3 # Markdown file
|
||||
FMT_HTML = 4 # HTML file
|
||||
FMT_EBOOK = 5 # E-book friendly HTML
|
||||
FMT_ODT = 6 # Open document
|
||||
FMT_TEX = 7 # LaTeX file
|
||||
FMT_EXT = {
|
||||
FMT_NWD : ".nwd",
|
||||
FMT_TXT : ".txt",
|
||||
FMT_MD : ".md",
|
||||
FMT_HTML : ".htm",
|
||||
@@ -212,6 +226,10 @@ class GuiExportMain(QWidget):
|
||||
FMT_TEX : ".tex",
|
||||
}
|
||||
FMT_HELP = {
|
||||
FMT_NWD : (
|
||||
"Exports a document using the novelWriter markdown format. "
|
||||
"The files selected by the filters are appended as-is, including comments and other settings."
|
||||
),
|
||||
FMT_TXT : (
|
||||
"Exports a plain text file. "
|
||||
"All formatting is stripped and comments are in square brackets."
|
||||
@@ -222,7 +240,7 @@ class GuiExportMain(QWidget):
|
||||
),
|
||||
FMT_HTML : (
|
||||
"Exports a plain html5 file. "
|
||||
"Comments are converted to preformatted text blocks."
|
||||
"Comments are wrapped in blocks with a yellow background colour."
|
||||
),
|
||||
FMT_EBOOK : (
|
||||
"Exports an html5 file that can be converted to eBook with Calibre. "
|
||||
@@ -337,12 +355,13 @@ class GuiExportMain(QWidget):
|
||||
self.outputHelp.setAlignment(Qt.AlignTop)
|
||||
|
||||
self.outputFormat = QComboBox(self)
|
||||
self.outputFormat.addItem("Plain Text (.txt)", self.FMT_TXT)
|
||||
# self.outputFormat.addItem("Markdown (.md)", self.FMT_MD)
|
||||
# self.outputFormat.addItem("HTML5 (.htm)", self.FMT_HTML)
|
||||
# self.outputFormat.addItem("HTML5 for eBook (.htm)", self.FMT_EBOOK)
|
||||
# self.outputFormat.addItem("Open Document (.odt)", self.FMT_ODT)
|
||||
# self.outputFormat.addItem("LaTeX for PDF (.tex)", self.FMT_TEX)
|
||||
self.outputFormat.addItem("novelWriter Markdown (.nwd)", self.FMT_NWD)
|
||||
self.outputFormat.addItem("Plain Text (.txt)", self.FMT_TXT)
|
||||
self.outputFormat.addItem("Markdown (.md)", self.FMT_MD)
|
||||
self.outputFormat.addItem("HTML5 (.htm)", self.FMT_HTML)
|
||||
# self.outputFormat.addItem("HTML5 for eBook (.htm)", self.FMT_EBOOK)
|
||||
# self.outputFormat.addItem("Open Document (.odt)", self.FMT_ODT)
|
||||
self.outputFormat.addItem("LaTeX for PDF (.tex)", self.FMT_TEX)
|
||||
self.outputFormat.currentIndexChanged.connect(self._updateFormat)
|
||||
|
||||
optIdx = self.outputFormat.findData(self.optState.getSetting("eFormat"))
|
||||
@@ -365,7 +384,7 @@ class GuiExportMain(QWidget):
|
||||
self.fixedWidth.setMaximum(999)
|
||||
self.fixedWidth.setSingleStep(1)
|
||||
self.fixedWidth.setValue(self.optState.getSetting("fixWidth"))
|
||||
self.fixedWidth.setToolTip("0 disables the feature. Applies to .txt, .md and .tex files.")
|
||||
self.fixedWidth.setToolTip("Applies to .txt, .md and .tex files. 0 disables the feature.")
|
||||
|
||||
self.addSettingsForm.addWidget(QLabel("Fixed width"), 0, 0)
|
||||
self.addSettingsForm.addWidget(self.fixedWidth, 0, 1)
|
||||
@@ -407,11 +426,13 @@ class GuiExportMain(QWidget):
|
||||
currDir = ""
|
||||
|
||||
extFilter = [
|
||||
"novelWriter document files (*.nwd)",
|
||||
"Text files (*.txt)",
|
||||
"Markdown files (*.md)",
|
||||
"HTML files (*.htm *.html)",
|
||||
"Open document files (*.odt)",
|
||||
# "Open document files (*.odt)",
|
||||
"LaTeX files (*.tex)",
|
||||
"All files (*.*)",
|
||||
]
|
||||
|
||||
dlgOpt = QFileDialog.Options()
|
||||
@@ -437,14 +458,14 @@ class GuiExportMain(QWidget):
|
||||
|
||||
# END Class GuiExportMain
|
||||
|
||||
class ExportLastState():
|
||||
class ExportLastState(OptLastState):
|
||||
|
||||
def __init__(self, theProject):
|
||||
self.theProject = theProject
|
||||
def __init__(self, theProject, theFile):
|
||||
OptLastState.__init__(self, theProject, theFile)
|
||||
self.theState = {
|
||||
"wNovel" : True,
|
||||
"wNotes" : False,
|
||||
"eFormat" : 2,
|
||||
"eFormat" : 1,
|
||||
"fixWidth" : 80,
|
||||
"wComments" : False,
|
||||
"chFormat" : "Chapter %numword%",
|
||||
@@ -456,52 +477,6 @@ class ExportLastState():
|
||||
self.stringOpt = ("chFormat","unFormat","scFormat","seFormat","saveTo")
|
||||
self.boolOpt = ("wNovel","wNotes","wComments")
|
||||
self.intOpt = ("eFormat","fixWidth")
|
||||
self.loadSettings()
|
||||
return
|
||||
|
||||
def loadSettings(self):
|
||||
stateFile = path.join(self.theProject.projMeta, nwFiles.EXPORT_OPT)
|
||||
theState = {}
|
||||
if path.isfile(stateFile):
|
||||
logger.debug("Loading export options file")
|
||||
try:
|
||||
with open(stateFile,mode="r") as inFile:
|
||||
theJson = inFile.read()
|
||||
theState = json.loads(theJson)
|
||||
except Exception as e:
|
||||
logger.error("Failed to load export options file")
|
||||
logger.error(str(e))
|
||||
return False
|
||||
for anOpt in theState:
|
||||
self.theState[anOpt] = theState[anOpt]
|
||||
return True
|
||||
|
||||
def saveSettings(self):
|
||||
stateFile = path.join(self.theProject.projMeta, nwFiles.EXPORT_OPT)
|
||||
logger.debug("Saving export options file")
|
||||
try:
|
||||
with open(stateFile,mode="w+") as outFile:
|
||||
outFile.write(json.dumps(self.theState, indent=2))
|
||||
except Exception as e:
|
||||
logger.error("Failed to save export options file")
|
||||
logger.error(str(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
def setSetting(self, setName, setValue):
|
||||
if setName in self.theState:
|
||||
self.theState[setName] = setValue
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
def getSetting(self, setName):
|
||||
if setName in self.stringOpt:
|
||||
return checkString(self.theState[setName],self.theState[setName],False)
|
||||
elif setName in self.boolOpt:
|
||||
return checkBool(self.theState[setName],self.theState[setName],False)
|
||||
elif setName in self.intOpt:
|
||||
return checkInt(self.theState[setName],self.theState[setName],False)
|
||||
return None
|
||||
|
||||
# END Class ExportLastState
|
||||
|
||||
@@ -87,7 +87,7 @@ class GuiItemEditor(QDialog):
|
||||
if itemLayout in self.validLayouts:
|
||||
self.editLayout.addItem(nwLabels.LAYOUT_NAME[itemLayout],itemLayout)
|
||||
|
||||
self.mainForm.addRow("Name", self.editName)
|
||||
self.mainForm.addRow("Label", self.editName)
|
||||
self.mainForm.addRow("Status", self.editStatus)
|
||||
self.mainForm.addRow("Layout", self.editLayout)
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtGui import QIcon, QDesktopServices
|
||||
from PyQt5.QtWidgets import QMenuBar, QAction, QMessageBox
|
||||
|
||||
from nw.enum import nwItemType, nwItemClass, nwDocAction
|
||||
@@ -145,6 +146,10 @@ class GuiMainMenu(QMenuBar):
|
||||
msgBox.aboutQt(self.theParent,"About Qt")
|
||||
return True
|
||||
|
||||
def _openHelp(self):
|
||||
QDesktopServices.openUrl(QUrl(nw.__docurl__))
|
||||
return True
|
||||
|
||||
##
|
||||
# Menu Builders
|
||||
##
|
||||
@@ -199,6 +204,12 @@ class GuiMainMenu(QMenuBar):
|
||||
menuItem.triggered.connect(self.theParent.exportProjectDialog)
|
||||
self.projMenu.addAction(menuItem)
|
||||
|
||||
# Project > Session Log
|
||||
menuItem = QAction("Session Log", self)
|
||||
menuItem.setStatusTip("Show the session log")
|
||||
menuItem.triggered.connect(self.theParent.showSessionLogDialog)
|
||||
self.projMenu.addAction(menuItem)
|
||||
|
||||
# Project > Separator
|
||||
self.projMenu.addSeparator()
|
||||
|
||||
@@ -306,8 +317,15 @@ class GuiMainMenu(QMenuBar):
|
||||
menuItem.triggered.connect(self.theParent.closeDocViewer)
|
||||
self.docuMenu.addAction(menuItem)
|
||||
|
||||
# # Document > Separator
|
||||
# self.docuMenu.addSeparator()
|
||||
# Document > Separator
|
||||
self.docuMenu.addSeparator()
|
||||
|
||||
# Document > Close Preview
|
||||
menuItem = QAction("Import from File", self)
|
||||
menuItem.setStatusTip("Import document from a text or markdown file")
|
||||
menuItem.setShortcut("Ctrl+Shift+I")
|
||||
menuItem.triggered.connect(self.theParent.importDocument)
|
||||
self.docuMenu.addAction(menuItem)
|
||||
|
||||
# # Document > Split
|
||||
# menuItem = QAction("Split Document", self)
|
||||
@@ -591,7 +609,7 @@ class GuiMainMenu(QMenuBar):
|
||||
menuItem = QAction("Documentation", self)
|
||||
menuItem.setStatusTip("View documentation")
|
||||
menuItem.setShortcut("F1")
|
||||
menuItem.triggered.connect(lambda : self.theParent.viewDocument("Help"))
|
||||
menuItem.triggered.connect(self._openHelp)
|
||||
self.helpMenu.addAction(menuItem)
|
||||
|
||||
return
|
||||
|
||||
249
nw/gui/sessionlog.py
Normal file
249
nw/gui/sessionlog.py
Normal file
@@ -0,0 +1,249 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Session Log Viewer
|
||||
|
||||
novelWriter – GUI Session Log Viewer
|
||||
======================================
|
||||
Class holding the session log view window
|
||||
|
||||
File History:
|
||||
Created: 2019-10-20 [0.3]
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from os import path
|
||||
from datetime import datetime
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QIcon, QColor, QPixmap, QFont
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QTreeWidget, QTreeWidgetItem, QDialogButtonBox, QHeaderView,
|
||||
QGridLayout, QLabel, QGroupBox, QCheckBox
|
||||
)
|
||||
|
||||
from nw.tools.optlaststate import OptLastState
|
||||
from nw.constants import nwConst, nwFiles
|
||||
from nw.enum import nwAlert
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiSessionLogView(QDialog):
|
||||
|
||||
def __init__(self, theParent, theProject):
|
||||
QDialog.__init__(self, theParent)
|
||||
|
||||
logger.debug("Initialising SessionLogView ...")
|
||||
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theProject = theProject
|
||||
self.theParent = theParent
|
||||
self.optState = SessionLogLastState(self.theProject,nwFiles.SLOG_OPT)
|
||||
self.optState.loadSettings()
|
||||
|
||||
self.timeFilter = 0.0
|
||||
self.timeTotal = 0.0
|
||||
|
||||
self.outerBox = QGridLayout()
|
||||
self.bottomBox = QHBoxLayout()
|
||||
|
||||
self.setWindowTitle("Session Log")
|
||||
self.setMinimumWidth(420)
|
||||
self.setMinimumHeight(400)
|
||||
|
||||
widthCol0 = self.optState.validIntRange(self.optState.getSetting("widthCol0"), 30, 999, 180)
|
||||
widthCol1 = self.optState.validIntRange(self.optState.getSetting("widthCol1"), 30, 999, 80)
|
||||
widthCol2 = self.optState.validIntRange(self.optState.getSetting("widthCol2"), 30, 999, 80)
|
||||
|
||||
self.listBox = QTreeWidget()
|
||||
self.listBox.setHeaderLabels(["Session Start","Length","Words",""])
|
||||
self.listBox.setIndentation(0)
|
||||
self.listBox.setColumnWidth(0,widthCol0)
|
||||
self.listBox.setColumnWidth(1,widthCol1)
|
||||
self.listBox.setColumnWidth(2,widthCol2)
|
||||
self.listBox.setColumnWidth(3,0)
|
||||
|
||||
hHeader = self.listBox.headerItem()
|
||||
hHeader.setTextAlignment(1,Qt.AlignRight)
|
||||
hHeader.setTextAlignment(2,Qt.AlignRight)
|
||||
|
||||
self.monoFont = QFont("Monospace",10)
|
||||
|
||||
sortValid = (Qt.AscendingOrder, Qt.DescendingOrder)
|
||||
sortCol = self.optState.validIntRange(self.optState.getSetting("sortCol"), 0, 2, 0)
|
||||
sortOrder = self.optState.validIntTuple(self.optState.getSetting("sortOrder"), sortValid, Qt.DescendingOrder)
|
||||
|
||||
self.listBox.sortByColumn(sortCol, sortOrder)
|
||||
self.listBox.setSortingEnabled(True)
|
||||
|
||||
# Session Info
|
||||
self.infoBox = QGroupBox("Sum Total Time", self)
|
||||
self.infoBoxForm = QGridLayout(self)
|
||||
self.infoBox.setLayout(self.infoBoxForm)
|
||||
|
||||
self.labelTotal = QLabel(self._formatTime(0))
|
||||
self.labelTotal.setFont(self.monoFont)
|
||||
self.labelTotal.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||
|
||||
self.labelFilter = QLabel(self._formatTime(0))
|
||||
self.labelFilter.setFont(self.monoFont)
|
||||
self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
|
||||
|
||||
self.infoBoxForm.addWidget(QLabel("All:"), 0, 0)
|
||||
self.infoBoxForm.addWidget(self.labelTotal, 0, 1)
|
||||
self.infoBoxForm.addWidget(QLabel("Filtered:"), 1, 0)
|
||||
self.infoBoxForm.addWidget(self.labelFilter, 1, 1)
|
||||
|
||||
# Filter Options
|
||||
self.filterBox = QGroupBox("Filters", self)
|
||||
self.filterBoxForm = QGridLayout(self)
|
||||
self.filterBox.setLayout(self.filterBoxForm)
|
||||
|
||||
self.hideZeros = QCheckBox("Hide zero word count", self)
|
||||
self.hideZeros.setChecked(self.optState.getSetting("hideZeros"))
|
||||
self.hideZeros.stateChanged.connect(self._doHideZeros)
|
||||
|
||||
self.hideNegative = QCheckBox("Hide negative word count", self)
|
||||
self.hideNegative.setChecked(self.optState.getSetting("hideNegative"))
|
||||
self.hideNegative.stateChanged.connect(self._doHideNegative)
|
||||
|
||||
self.filterBoxForm.addWidget(self.hideZeros, 0, 0)
|
||||
self.filterBoxForm.addWidget(self.hideNegative, 1, 0)
|
||||
|
||||
# Buttons
|
||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
self.buttonBox.rejected.connect(self._doClose)
|
||||
|
||||
# Assemble
|
||||
self.outerBox.addWidget(self.listBox, 0, 0, 1, 2)
|
||||
self.outerBox.addWidget(self.infoBox, 1, 0)
|
||||
self.outerBox.addWidget(self.filterBox, 1, 1)
|
||||
self.outerBox.addWidget(self.buttonBox, 2, 0, 1, 2)
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
self.show()
|
||||
|
||||
logger.debug("SessionLogView initialisation complete")
|
||||
|
||||
self._loadSessionLog()
|
||||
|
||||
return
|
||||
|
||||
def _loadSessionLog(self):
|
||||
|
||||
logFile = path.join(self.theProject.projMeta, nwFiles.SESS_INFO)
|
||||
if not path.isfile(logFile):
|
||||
logger.warning("No session log file found for this project.")
|
||||
return False
|
||||
|
||||
self.listBox.clear()
|
||||
|
||||
self.timeFilter = 0.0
|
||||
self.timeTotal = 0.0
|
||||
|
||||
hideZeros = self.hideZeros.isChecked()
|
||||
hideNegative = self.hideNegative.isChecked()
|
||||
|
||||
logger.debug("Loading session log file")
|
||||
try:
|
||||
with open(logFile,mode="r") as inFile:
|
||||
for inLine in inFile:
|
||||
inData = inLine.split()
|
||||
if len(inData) != 8:
|
||||
continue
|
||||
dStart = datetime.strptime("%s %s" % (inData[1],inData[2]),nwConst.tStampFmt)
|
||||
dEnd = datetime.strptime("%s %s" % (inData[4],inData[5]),nwConst.tStampFmt)
|
||||
nWords = int(inData[7])
|
||||
tDiff = dEnd - dStart
|
||||
sDiff = tDiff.total_seconds()
|
||||
|
||||
self.timeTotal += sDiff
|
||||
if abs(nWords) > 0:
|
||||
self.timeFilter += sDiff
|
||||
|
||||
if hideZeros and nWords == 0:
|
||||
continue
|
||||
|
||||
if hideNegative and nWords < 0:
|
||||
continue
|
||||
|
||||
newItem = QTreeWidgetItem([str(dStart),self._formatTime(sDiff),str(nWords),""])
|
||||
|
||||
newItem.setTextAlignment(1,Qt.AlignRight)
|
||||
newItem.setTextAlignment(2,Qt.AlignRight)
|
||||
|
||||
newItem.setFont(0,self.monoFont)
|
||||
newItem.setFont(1,self.monoFont)
|
||||
newItem.setFont(2,self.monoFont)
|
||||
|
||||
self.listBox.addTopLevelItem(newItem)
|
||||
|
||||
except Exception as e:
|
||||
self.theParent.makeAlert(["Failed to read session log file.",str(e)], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
self.labelFilter.setText(self._formatTime(self.timeFilter))
|
||||
self.labelTotal.setText(self._formatTime(self.timeTotal))
|
||||
|
||||
return True
|
||||
|
||||
def _doClose(self):
|
||||
|
||||
widthCol0 = self.listBox.columnWidth(0)
|
||||
widthCol1 = self.listBox.columnWidth(1)
|
||||
widthCol2 = self.listBox.columnWidth(2)
|
||||
sortCol = self.listBox.sortColumn()
|
||||
sortOrder = self.listBox.header().sortIndicatorOrder()
|
||||
hideZeros = self.hideZeros.isChecked()
|
||||
hideNegative = self.hideNegative.isChecked()
|
||||
|
||||
self.optState.setSetting("widthCol0", widthCol0)
|
||||
self.optState.setSetting("widthCol1", widthCol1)
|
||||
self.optState.setSetting("widthCol2", widthCol2)
|
||||
self.optState.setSetting("sortCol", sortCol)
|
||||
self.optState.setSetting("sortOrder", sortOrder)
|
||||
self.optState.setSetting("hideZeros", hideZeros)
|
||||
self.optState.setSetting("hideNegative",hideNegative)
|
||||
|
||||
self.optState.saveSettings()
|
||||
self.close()
|
||||
|
||||
return
|
||||
|
||||
def _doHideZeros(self, newState):
|
||||
self._loadSessionLog()
|
||||
return
|
||||
|
||||
def _doHideNegative(self, newState):
|
||||
self._loadSessionLog()
|
||||
return
|
||||
|
||||
def _formatTime(self, tS):
|
||||
tM = int(tS/60)
|
||||
tH = int(tM/60)
|
||||
tM = tM - tH*60
|
||||
tS = tS - tM*60 - tH*3600
|
||||
return "%02d:%02d:%02d" % (tH,tM,tS)
|
||||
|
||||
# END Class GuiSessionLogView
|
||||
|
||||
class SessionLogLastState(OptLastState):
|
||||
|
||||
def __init__(self, theProject, theFile):
|
||||
OptLastState.__init__(self, theProject, theFile)
|
||||
self.theState = {
|
||||
"widthCol0" : 180,
|
||||
"widthCol1" : 80,
|
||||
"widthCol2" : 80,
|
||||
"sortCol" : 0,
|
||||
"sortOrder" : Qt.DescendingOrder,
|
||||
"hideZeros" : True,
|
||||
"hideNegative" : False,
|
||||
}
|
||||
self.stringOpt = ()
|
||||
self.boolOpt = ("hideZeros","hideNegative")
|
||||
self.intOpt = ("widthCol0","widthCol1","widthCol2","sortCol","sortOrder")
|
||||
return
|
||||
|
||||
# END Class SessionLogLastState
|
||||
@@ -13,13 +13,18 @@
|
||||
import logging
|
||||
import nw
|
||||
|
||||
from os import path
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QIcon, QColor, QPixmap
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem,
|
||||
QDialogButtonBox, QLabel, QPushButton, QHeaderView
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QTableWidget, QTableWidgetItem, QDialogButtonBox, QLabel,
|
||||
QPushButton, QHeaderView, QGridLayout, QGroupBox, QCheckBox
|
||||
)
|
||||
|
||||
from nw.tools.optlaststate import OptLastState
|
||||
from nw.constants import nwFiles
|
||||
from nw.enum import nwItemClass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GuiTimeLineView(QDialog):
|
||||
@@ -33,16 +38,27 @@ class GuiTimeLineView(QDialog):
|
||||
self.theProject = theProject
|
||||
self.theParent = theParent
|
||||
self.theIndex = theIndex
|
||||
self.optState = TimeLineLastState(self.theProject,nwFiles.TLINE_OPT)
|
||||
self.optState.loadSettings()
|
||||
|
||||
self.theMatrix = {}
|
||||
self.numRows = 0
|
||||
self.numCols = 0
|
||||
|
||||
self.outerBox = QVBoxLayout()
|
||||
self.filterBox = QVBoxLayout()
|
||||
self.centreBox = QHBoxLayout()
|
||||
self.bottomBox = QHBoxLayout()
|
||||
|
||||
self.setWindowTitle("Timeline View")
|
||||
self.setMinimumSize(*self.mainConf.dlgTimeLine)
|
||||
self.setMinimumWidth(700)
|
||||
self.setMinimumHeight(400)
|
||||
|
||||
winWidth = self.optState.validIntRange(self.optState.getSetting("winWidth"), 700, 10000, 700)
|
||||
winHeight = self.optState.validIntRange(self.optState.getSetting("winHeight"), 400, 10000, 400)
|
||||
self.resize(winWidth,winHeight)
|
||||
|
||||
# TimeLine Table
|
||||
self.mainTable = QTableWidget()
|
||||
self.mainTable.setGridStyle(Qt.NoPen)
|
||||
|
||||
@@ -54,6 +70,53 @@ class GuiTimeLineView(QDialog):
|
||||
self.vHeader.setSectionResizeMode(QHeaderView.ResizeToContents)
|
||||
self.mainTable.setVerticalHeader(self.vHeader)
|
||||
|
||||
# Option Box
|
||||
self.optFilter = QGroupBox("Include Tags", self)
|
||||
self.optFilterGrid = QGridLayout(self)
|
||||
self.optFilter.setLayout(self.optFilterGrid)
|
||||
|
||||
self.filterPlot = QCheckBox("Plot tags", self)
|
||||
self.filterPlot.setChecked(self.optState.getSetting("fPlot"))
|
||||
self.filterPlot.stateChanged.connect(self._filterChange)
|
||||
|
||||
self.filterChar = QCheckBox("Character tags", self)
|
||||
self.filterChar.setChecked(self.optState.getSetting("fChar"))
|
||||
self.filterChar.stateChanged.connect(self._filterChange)
|
||||
|
||||
self.filterWorld = QCheckBox("Location tags", self)
|
||||
self.filterWorld.setChecked(self.optState.getSetting("fWorld"))
|
||||
self.filterWorld.stateChanged.connect(self._filterChange)
|
||||
|
||||
self.filterTime = QCheckBox("Timeline tags", self)
|
||||
self.filterTime.setChecked(self.optState.getSetting("fTime"))
|
||||
self.filterTime.stateChanged.connect(self._filterChange)
|
||||
|
||||
self.filterObject = QCheckBox("Object tags", self)
|
||||
self.filterObject.setChecked(self.optState.getSetting("fObject"))
|
||||
self.filterObject.stateChanged.connect(self._filterChange)
|
||||
|
||||
self.filterCustom = QCheckBox("Custom tags", self)
|
||||
self.filterCustom.setChecked(self.optState.getSetting("fCustom"))
|
||||
self.filterCustom.stateChanged.connect(self._filterChange)
|
||||
|
||||
self.optFilterGrid.addWidget(self.filterPlot, 0, 1)
|
||||
self.optFilterGrid.addWidget(self.filterChar, 1, 1)
|
||||
self.optFilterGrid.addWidget(self.filterWorld, 2, 1)
|
||||
self.optFilterGrid.addWidget(self.filterTime, 3, 1)
|
||||
self.optFilterGrid.addWidget(self.filterObject, 4, 1)
|
||||
self.optFilterGrid.addWidget(self.filterCustom, 5, 1)
|
||||
|
||||
self.optHide = QGroupBox("Filters", self)
|
||||
self.optHideGrid = QGridLayout(self)
|
||||
self.optHide.setLayout(self.optHideGrid)
|
||||
|
||||
self.hideUnused = QCheckBox("Hide unused", self)
|
||||
self.hideUnused.setChecked(self.optState.getSetting("hUnused"))
|
||||
self.hideUnused.stateChanged.connect(self._filterChange)
|
||||
|
||||
self.optHideGrid.addWidget(self.hideUnused, 0, 1)
|
||||
|
||||
# Button Box
|
||||
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
self.buttonBox.rejected.connect(self._doClose)
|
||||
|
||||
@@ -63,14 +126,21 @@ class GuiTimeLineView(QDialog):
|
||||
self.btnRefresh = QPushButton("Refresh Table")
|
||||
self.btnRefresh.clicked.connect(self._buildNovelList)
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
self.outerBox.addWidget(self.mainTable)
|
||||
self.outerBox.addLayout(self.bottomBox)
|
||||
self.bottomBox.addWidget(self.btnRebuild)
|
||||
self.bottomBox.addWidget(self.btnRefresh)
|
||||
self.bottomBox.addStretch()
|
||||
self.bottomBox.addWidget(self.buttonBox)
|
||||
|
||||
# Assemble
|
||||
self.filterBox.addWidget(self.optFilter)
|
||||
self.filterBox.addWidget(self.optHide)
|
||||
self.filterBox.addStretch()
|
||||
self.centreBox.addWidget(self.mainTable)
|
||||
self.centreBox.addLayout(self.filterBox)
|
||||
self.outerBox.addLayout(self.centreBox)
|
||||
self.outerBox.addLayout(self.bottomBox)
|
||||
self.setLayout(self.outerBox)
|
||||
|
||||
self._buildNovelList()
|
||||
self.buttonBox.setFocus()
|
||||
|
||||
@@ -82,13 +152,28 @@ class GuiTimeLineView(QDialog):
|
||||
|
||||
def _buildNovelList(self):
|
||||
|
||||
self.theIndex.buildNovelList()
|
||||
self.numRows = len(self.theIndex.novelList)
|
||||
self.numCols = len(self.theIndex.tagIndex.keys())
|
||||
|
||||
self.mainTable.clear()
|
||||
self.theIndex.buildNovelList()
|
||||
|
||||
self.numRows = len(self.theIndex.novelList)
|
||||
self.mainTable.setRowCount(self.numRows)
|
||||
self.mainTable.setColumnCount(self.numCols)
|
||||
|
||||
theFilters = {}
|
||||
theFilters["exClass"] = []
|
||||
theFilters["hUnused"] = self.hideUnused.isChecked()
|
||||
|
||||
if not self.filterPlot.isChecked():
|
||||
theFilters["exClass"].append(nwItemClass.PLOT)
|
||||
if not self.filterChar.isChecked():
|
||||
theFilters["exClass"].append(nwItemClass.CHARACTER)
|
||||
if not self.filterWorld.isChecked():
|
||||
theFilters["exClass"].append(nwItemClass.WORLD)
|
||||
if not self.filterTime.isChecked():
|
||||
theFilters["exClass"].append(nwItemClass.TIMELINE)
|
||||
if not self.filterObject.isChecked():
|
||||
theFilters["exClass"].append(nwItemClass.OBJECT)
|
||||
if not self.filterCustom.isChecked():
|
||||
theFilters["exClass"].append(nwItemClass.CUSTOM)
|
||||
|
||||
for n in range(len(self.theIndex.novelList)):
|
||||
iDepth = self.theIndex.novelList[n][1]
|
||||
@@ -96,14 +181,17 @@ class GuiTimeLineView(QDialog):
|
||||
newItem = QTableWidgetItem("%s%s " % (" "*iDepth,iTitle))
|
||||
self.mainTable.setVerticalHeaderItem(n, newItem)
|
||||
|
||||
theMap = self.theIndex.buildTagNovelMap(self.theIndex.tagIndex.keys())
|
||||
nCol = 0
|
||||
theMap = self.theIndex.buildTagNovelMap(self.theIndex.tagIndex.keys(), theFilters)
|
||||
self.numCols = len(theMap.keys())
|
||||
self.mainTable.setColumnCount(self.numCols)
|
||||
|
||||
nCol = 0
|
||||
for theTag, theCols in theMap.items():
|
||||
newItem = QTableWidgetItem(" %s " % theTag)
|
||||
self.mainTable.setHorizontalHeaderItem(nCol, newItem)
|
||||
for n in range(len(theCols)):
|
||||
if theCols[n] == 1:
|
||||
pxNew = QPixmap(10,10)
|
||||
pxNew = QPixmap(10,10)
|
||||
pxNew.fill(QColor(0,120,0))
|
||||
lblNew = QLabel()
|
||||
lblNew.setPixmap(pxNew)
|
||||
@@ -111,7 +199,7 @@ class GuiTimeLineView(QDialog):
|
||||
lblNew.setAttribute(Qt.WA_TranslucentBackground)
|
||||
self.mainTable.setCellWidget(n, nCol, lblNew)
|
||||
elif theCols[n] == 2:
|
||||
pxNew = QPixmap(10,10)
|
||||
pxNew = QPixmap(10,10)
|
||||
pxNew.fill(QColor(0,0,120))
|
||||
lblNew = QLabel()
|
||||
lblNew.setPixmap(pxNew)
|
||||
@@ -123,8 +211,58 @@ class GuiTimeLineView(QDialog):
|
||||
return
|
||||
|
||||
def _doClose(self):
|
||||
self.mainConf.setTLineSize(self.width(), self.height())
|
||||
|
||||
logger.verbose("GuiTimeLineView close button clicked")
|
||||
|
||||
winWidth = self.width()
|
||||
winHeight = self.height()
|
||||
fPlot = self.filterPlot.isChecked()
|
||||
fChar = self.filterChar.isChecked()
|
||||
fWorld = self.filterWorld.isChecked()
|
||||
fTime = self.filterTime.isChecked()
|
||||
fObject = self.filterObject.isChecked()
|
||||
fCustom = self.filterCustom.isChecked()
|
||||
hUnused = self.hideUnused.isChecked()
|
||||
|
||||
self.optState.setSetting("winWidth", winWidth)
|
||||
self.optState.setSetting("winHeight",winHeight)
|
||||
self.optState.setSetting("fPlot", fPlot)
|
||||
self.optState.setSetting("fChar", fChar)
|
||||
self.optState.setSetting("fWorld", fWorld)
|
||||
self.optState.setSetting("fTime", fTime)
|
||||
self.optState.setSetting("fObject", fObject)
|
||||
self.optState.setSetting("fCustom", fCustom)
|
||||
self.optState.setSetting("hUnused", hUnused)
|
||||
|
||||
self.optState.saveSettings()
|
||||
self.close()
|
||||
|
||||
return
|
||||
|
||||
# END Class GuiItemEditor
|
||||
def _filterChange(self, checkState):
|
||||
self._buildNovelList()
|
||||
return
|
||||
|
||||
# END Class GuiTimeLineView
|
||||
|
||||
class TimeLineLastState(OptLastState):
|
||||
|
||||
def __init__(self, theProject, theFile):
|
||||
OptLastState.__init__(self, theProject, theFile)
|
||||
self.theState = {
|
||||
"winWidth" : 700,
|
||||
"winHeight" : 400,
|
||||
"fPlot" : True,
|
||||
"fChar" : True,
|
||||
"fWorld" : True,
|
||||
"fTime" : True,
|
||||
"fObject" : True,
|
||||
"fCustom" : True,
|
||||
"hUnused" : True,
|
||||
}
|
||||
self.stringOpt = ()
|
||||
self.boolOpt = ("fPlot","fChar","fWorld","fTime","fObject","fCustom","hUnused")
|
||||
self.intOpt = ("winWidth","winHeight")
|
||||
return
|
||||
|
||||
# END Class TimeLineLastState
|
||||
|
||||
@@ -34,6 +34,7 @@ from nw.gui.export import GuiExport
|
||||
from nw.gui.itemeditor import GuiItemEditor
|
||||
from nw.gui.statusbar import GuiMainStatus
|
||||
from nw.gui.timelineview import GuiTimeLineView
|
||||
from nw.gui.sessionlog import GuiSessionLogView
|
||||
from nw.project.project import NWProject
|
||||
from nw.project.document import NWDoc
|
||||
from nw.project.item import NWItem
|
||||
@@ -53,14 +54,16 @@ class GuiMain(QMainWindow):
|
||||
|
||||
logger.info("Starting %s" % nw.__package__)
|
||||
logger.debug("Initialising GUI ...")
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theTheme = Theme(self)
|
||||
self.theProject = NWProject(self)
|
||||
self.theIndex = NWIndex(self.theProject, self)
|
||||
self.hasProject = False
|
||||
self.mainConf = nw.CONFIG
|
||||
self.theTheme = Theme(self)
|
||||
self.theProject = NWProject(self)
|
||||
self.theIndex = NWIndex(self.theProject, self)
|
||||
self.hasProject = False
|
||||
|
||||
logger.info("Qt5 Version: %s (%d)" % (self.mainConf.verQtString, self.mainConf.verQtValue))
|
||||
logger.info("PyQt5 Version: %s (%d)" % (self.mainConf.verPyQtString, self.mainConf.verPyQtValue))
|
||||
logger.info("OS: %s" % (self.mainConf.osType))
|
||||
logger.info("Qt5 Version: %s (%d)" % (self.mainConf.verQtString, self.mainConf.verQtValue))
|
||||
logger.info("PyQt5 Version: %s (%d)" % (self.mainConf.verPyQtString, self.mainConf.verPyQtValue))
|
||||
logger.info("Python Version: %s (0x%x)" % (self.mainConf.verPyString, self.mainConf.verPyHexVal))
|
||||
|
||||
self.resize(*self.mainConf.winGeometry)
|
||||
self._setWindowTitle()
|
||||
@@ -88,6 +91,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
self.docPane = QFrame()
|
||||
self.docView = QVBoxLayout()
|
||||
self.docView.setContentsMargins(0,0,0,0)
|
||||
self.docView.addWidget(self.searchBar)
|
||||
self.docView.addWidget(self.docEditor)
|
||||
self.docPane.setLayout(self.docView)
|
||||
@@ -224,7 +228,7 @@ class GuiMain(QMainWindow):
|
||||
if self.docEditor.docChanged:
|
||||
self.saveDocument()
|
||||
|
||||
if self.theProject.projChanged:
|
||||
if self.theProject.projAltered:
|
||||
saveOK = self.saveProject()
|
||||
doBackup = False
|
||||
if self.theProject.doBackup and self.mainConf.backupOnClose:
|
||||
@@ -364,6 +368,54 @@ class GuiMain(QMainWindow):
|
||||
|
||||
return True
|
||||
|
||||
def importDocument(self):
|
||||
|
||||
lastPath = self.mainConf.lastPath
|
||||
|
||||
extFilter = [
|
||||
"Text files (*.txt)",
|
||||
"Markdown files (*.md)",
|
||||
"All files (*.*)",
|
||||
]
|
||||
dlgOpt = QFileDialog.Options()
|
||||
dlgOpt |= QFileDialog.DontUseNativeDialog
|
||||
inPath = QFileDialog.getOpenFileName(
|
||||
self,"Import File",lastPath,options=dlgOpt,filter=";;".join(extFilter)
|
||||
)
|
||||
if inPath:
|
||||
loadFile = inPath[0]
|
||||
self.mainConf.setLastPath(loadFile)
|
||||
else:
|
||||
return False
|
||||
|
||||
theText = None
|
||||
try:
|
||||
with open(loadFile,mode="rt") as inFile:
|
||||
theText = inFile.read()
|
||||
except Exception as e:
|
||||
self.makeAlert(["Could not read file. The file cannot be a binary file.",str(e)], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
if self.docEditor.theHandle is None:
|
||||
self.makeAlert(["Please open a document to import the text file into."], nwAlert.ERROR)
|
||||
return False
|
||||
|
||||
if not self.docEditor.isEmpty():
|
||||
if self.mainConf.showGUI:
|
||||
msgBox = QMessageBox()
|
||||
msgRes = msgBox.question(
|
||||
self, "Import Document",
|
||||
"Importing the file will overwrite the current content of the document. Do you want to proceed?"
|
||||
)
|
||||
if msgRes != QMessageBox.Yes:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
self.docEditor.setText(theText)
|
||||
|
||||
return True
|
||||
|
||||
##
|
||||
# Tree Item Actions
|
||||
##
|
||||
@@ -501,6 +553,7 @@ class GuiMain(QMainWindow):
|
||||
logger.debug("Applying new preferences")
|
||||
self.initMain()
|
||||
self.theTheme.updateTheme()
|
||||
self.saveDocument()
|
||||
self.docEditor.initEditor()
|
||||
self.docViewer.initViewer()
|
||||
return True
|
||||
@@ -524,6 +577,12 @@ class GuiMain(QMainWindow):
|
||||
dlgTLine.exec_()
|
||||
return True
|
||||
|
||||
def showSessionLogDialog(self):
|
||||
if self.hasProject:
|
||||
dlgTLine = GuiSessionLogView(self, self.theProject)
|
||||
dlgTLine.exec_()
|
||||
return True
|
||||
|
||||
def makeAlert(self, theMessage, theLevel=nwAlert.INFO):
|
||||
"""Alert both the user and the logger at the same time. Message can be either a string or an
|
||||
array of strings. Severity level is 0 = info, 1 = warning, and 2 = error.
|
||||
|
||||
@@ -319,18 +319,24 @@ class NWIndex():
|
||||
|
||||
return True
|
||||
|
||||
def buildTagNovelMap(self, theTags):
|
||||
def buildTagNovelMap(self, theTags, theFilters=None):
|
||||
|
||||
tagMap = {}
|
||||
tagClass = {}
|
||||
exClass = []
|
||||
|
||||
if theFilters is not None:
|
||||
if "exClass" in theFilters.keys():
|
||||
exClass = theFilters["exClass"]
|
||||
|
||||
for theTag in theTags:
|
||||
tagMap[theTag] = [0]*len(self.novelOrder)
|
||||
try:
|
||||
tagClass[theTag] = nwItemClass[self.tagIndex[theTag][2]]
|
||||
except:
|
||||
logger.error("Could not map '%s' to nwItemClass" % self.tagIndex[theTag][2])
|
||||
tagClass[theTag] = None
|
||||
if tagClass[theTag] not in exClass:
|
||||
tagMap[theTag] = [0]*len(self.novelOrder)
|
||||
|
||||
for tHandle in self.refIndex:
|
||||
for nLine, tKey, tTag, nTitle in self.refIndex[tHandle]:
|
||||
@@ -342,6 +348,13 @@ class NWIndex():
|
||||
except:
|
||||
logger.error("Could not find '%s:%d' in novelOrder" % (tHandle, nTitle))
|
||||
|
||||
if theFilters["hUnused"]:
|
||||
tagMapFiltered = {}
|
||||
for theTag in tagMap.keys():
|
||||
if sum(tagMap[theTag]) > 0:
|
||||
tagMapFiltered[theTag] = tagMap[theTag]
|
||||
return tagMapFiltered
|
||||
|
||||
return tagMap
|
||||
|
||||
# END Class NWIndex
|
||||
|
||||
@@ -23,7 +23,7 @@ from nw.project.item import NWItem
|
||||
from nw.project.status import NWStatus
|
||||
from nw.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert
|
||||
from nw.common import checkString, checkBool, checkInt
|
||||
from nw.constants import nwFiles
|
||||
from nw.constants import nwFiles, nwConst
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -34,22 +34,23 @@ class NWProject():
|
||||
# Internal
|
||||
self.theParent = theParent
|
||||
self.mainConf = self.theParent.mainConf
|
||||
self.projChanged = None
|
||||
self.projOpened = None
|
||||
self.projOpened = None # The time stamp of when the project file was opened
|
||||
self.projChanged = None # The project has unsaved changes
|
||||
self.projAltered = None # The project has been altered this session (used to trigger backup)
|
||||
|
||||
# Debug
|
||||
self.handleSeed = None
|
||||
|
||||
# Class Settings
|
||||
self.projTree = None
|
||||
self.treeOrder = None
|
||||
self.treeRoots = None
|
||||
self.trashRoot = None
|
||||
self.projPath = None
|
||||
self.projMeta = None
|
||||
self.projCache = None
|
||||
self.projDict = None
|
||||
self.projFile = None
|
||||
self.projTree = None # Holds all the items of the project
|
||||
self.treeOrder = None # The order of the tree items on the tree view
|
||||
self.treeRoots = None # The root items of the tree
|
||||
self.trashRoot = None # The handle of the trash root folder
|
||||
self.projPath = None # The full path to where the currently open project is saved
|
||||
self.projMeta = None # The full path to the project's meta data folder
|
||||
self.projCache = None # The full path to the project's cache folder
|
||||
self.projDict = None # The spell check dictionary
|
||||
self.projFile = None # The file name of the project main xml file
|
||||
|
||||
# Project Meta
|
||||
self.projName = None
|
||||
@@ -140,8 +141,9 @@ class NWProject():
|
||||
|
||||
def clearProject(self):
|
||||
|
||||
self.projChanged = None
|
||||
self.projOpened = None
|
||||
self.projChanged = None
|
||||
self.projAltered = False
|
||||
|
||||
# Project Settings
|
||||
self.projTree = {}
|
||||
@@ -267,6 +269,7 @@ class NWProject():
|
||||
self._scanProjectFolder()
|
||||
self.setProjectChanged(False)
|
||||
self.projOpened = time()
|
||||
self.projAltered = False
|
||||
|
||||
return True
|
||||
|
||||
@@ -458,6 +461,9 @@ class NWProject():
|
||||
def setProjectChanged(self, bValue):
|
||||
self.projChanged = bValue
|
||||
self.theParent.setProjectStatus(self.projChanged)
|
||||
if bValue:
|
||||
# If we've changed the project at all, this should be True
|
||||
self.projAltered = True
|
||||
return self.projChanged
|
||||
|
||||
##
|
||||
@@ -657,8 +663,8 @@ class NWProject():
|
||||
"End: {closed:s} "
|
||||
"Words: {words:8d}"
|
||||
).format(
|
||||
opened = datetime.fromtimestamp(self.projOpened).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
closed = datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
opened = datetime.fromtimestamp(self.projOpened).strftime(nwConst.tStampFmt),
|
||||
closed = datetime.now().strftime(nwConst.tStampFmt),
|
||||
words = self.getSessionWordCount(),
|
||||
), file=outFile)
|
||||
|
||||
|
||||
91
nw/tools/optlaststate.py
Normal file
91
nw/tools/optlaststate.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Options Last State
|
||||
|
||||
novelWriter – Options Last State
|
||||
==================================
|
||||
Class holding the last state of GUI options
|
||||
|
||||
File History:
|
||||
Created: 2019-10-21 [0.3.1]
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
import nw
|
||||
|
||||
from os import path
|
||||
|
||||
from nw.common import checkString, checkBool, checkInt
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class OptLastState():
|
||||
|
||||
def __init__(self, theProject, theFile):
|
||||
self.theProject = theProject
|
||||
self.theFile = theFile
|
||||
self.theState = {}
|
||||
self.stringOpt = ()
|
||||
self.boolOpt = ()
|
||||
self.intOpt = ()
|
||||
return
|
||||
|
||||
def loadSettings(self):
|
||||
stateFile = path.join(self.theProject.projMeta,self.theFile)
|
||||
theState = {}
|
||||
if path.isfile(stateFile):
|
||||
logger.debug("Loading options file")
|
||||
try:
|
||||
with open(stateFile,mode="r") as inFile:
|
||||
theJson = inFile.read()
|
||||
theState = json.loads(theJson)
|
||||
except Exception as e:
|
||||
logger.error("Failed to load options file")
|
||||
logger.error(str(e))
|
||||
return False
|
||||
for anOpt in theState:
|
||||
self.theState[anOpt] = theState[anOpt]
|
||||
return True
|
||||
|
||||
def saveSettings(self):
|
||||
stateFile = path.join(self.theProject.projMeta,self.theFile)
|
||||
logger.debug("Saving options file")
|
||||
try:
|
||||
with open(stateFile,mode="w+") as outFile:
|
||||
outFile.write(json.dumps(self.theState, indent=2))
|
||||
except Exception as e:
|
||||
logger.error("Failed to save options file")
|
||||
logger.error(str(e))
|
||||
return False
|
||||
return True
|
||||
|
||||
def setSetting(self, setName, setValue):
|
||||
if setName in self.theState:
|
||||
self.theState[setName] = setValue
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
def getSetting(self, setName):
|
||||
if setName in self.stringOpt:
|
||||
return checkString(self.theState[setName],self.theState[setName],False)
|
||||
elif setName in self.boolOpt:
|
||||
return checkBool(self.theState[setName],self.theState[setName],False)
|
||||
elif setName in self.intOpt:
|
||||
return checkInt(self.theState[setName],self.theState[setName],False)
|
||||
return None
|
||||
|
||||
def validIntRange(self, theValue, intA, intB, intDefault):
|
||||
if isinstance(theValue, int):
|
||||
if theValue >= intA and theValue <= intB:
|
||||
return theValue
|
||||
return intDefault
|
||||
|
||||
def validIntTuple(self, theValue, theTuple, intDefault):
|
||||
if isinstance(theValue, int):
|
||||
if theValue in theTuple:
|
||||
return theValue
|
||||
return intDefault
|
||||
|
||||
# END Class OptLastState
|
||||
@@ -3,7 +3,7 @@
|
||||
% Begin Meta
|
||||
@pov: Jane
|
||||
@char: John
|
||||
@location: Earth
|
||||
@location: Space
|
||||
% End Meta
|
||||
|
||||
Some text here would look good as well, and maybe some "dialogue"?
|
||||
|
||||
16
sample/sampleNovel/data_b/a8a28a246524_main.nwd
Normal file
16
sample/sampleNovel/data_b/a8a28a246524_main.nwd
Normal file
@@ -0,0 +1,16 @@
|
||||
## Interlude
|
||||
|
||||
I am the very model of a modern Major-General
|
||||
I've information vegetable, animal, and mineral
|
||||
I know the kings of England, and I quote the fights historical
|
||||
From Marathon to Waterloo, in order categorical
|
||||
|
||||
I'm very well acquainted, too, with matters mathematical
|
||||
I understand equations, both the simple and quadratical
|
||||
About binomial theorem I'm teeming with a lot o’ news
|
||||
With many cheerful facts about the square of the hypotenuse
|
||||
|
||||
With many cheerful facts about the square of the hypotenuse
|
||||
With many cheerful facts about the square of the hypotenuse
|
||||
With many cheerful facts about the square of the hypotepotenuse
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
### Another Scene
|
||||
|
||||
@pov: John
|
||||
@location: Space
|
||||
@location: Earth
|
||||
|
||||
This is the second scene in out story. We have no idea what’s going on, so we’re just going to ramble on until we have a few lines of text so that the editor has something to work with.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="0.2.3" fileVersion="1.0" timeStamp="2019-10-19 13:37:25">
|
||||
<novelWriterXML appVersion="0.3.1" fileVersion="1.0" timeStamp="2019-10-23 21:21:02">
|
||||
<project>
|
||||
<name>Sample Project</name>
|
||||
<title>Sample Project</title>
|
||||
@@ -9,9 +9,9 @@
|
||||
</project>
|
||||
<settings>
|
||||
<spellCheck>True</spellCheck>
|
||||
<lastEdited>636b6aa9b697b</lastEdited>
|
||||
<lastEdited>ba8a28a246524</lastEdited>
|
||||
<lastViewed>636b6aa9b697b</lastViewed>
|
||||
<lastWordCount>758</lastWordCount>
|
||||
<lastWordCount>859</lastWordCount>
|
||||
<autoReplace>
|
||||
<A>B</A>
|
||||
<B>E</B>
|
||||
@@ -33,7 +33,7 @@
|
||||
<entry blue="175" green="0" red="117">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content count="18">
|
||||
<content count="19">
|
||||
<item handle="7031beac91f75" order="0" parent="None">
|
||||
<name>Novel</name>
|
||||
<type>ROOT</type>
|
||||
@@ -82,7 +82,7 @@
|
||||
<charCount>713</charCount>
|
||||
<wordCount>132</wordCount>
|
||||
<paraCount>6</paraCount>
|
||||
<cursorPos>85</cursorPos>
|
||||
<cursorPos>72</cursorPos>
|
||||
</item>
|
||||
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
|
||||
<name>Another Scene</name>
|
||||
@@ -96,7 +96,19 @@
|
||||
<paraCount>2</paraCount>
|
||||
<cursorPos>448</cursorPos>
|
||||
</item>
|
||||
<item handle="96b68994dfa3d" order="3" parent="e7ded148d6e4a">
|
||||
<item handle="ba8a28a246524" order="3" parent="e7ded148d6e4a">
|
||||
<name>Interlude</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
<status>Finished</status>
|
||||
<expanded>False</expanded>
|
||||
<layout>UNNUMBERED</layout>
|
||||
<charCount>614</charCount>
|
||||
<wordCount>101</wordCount>
|
||||
<paraCount>3</paraCount>
|
||||
<cursorPos>633</cursorPos>
|
||||
</item>
|
||||
<item handle="96b68994dfa3d" order="4" parent="e7ded148d6e4a">
|
||||
<name>A Note on Ipsums</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -108,7 +120,7 @@
|
||||
<paraCount>5</paraCount>
|
||||
<cursorPos>0</cursorPos>
|
||||
</item>
|
||||
<item handle="88706ddc78b1b" order="4" parent="e7ded148d6e4a">
|
||||
<item handle="88706ddc78b1b" order="5" parent="e7ded148d6e4a">
|
||||
<name>Chapter Two</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
@@ -120,7 +132,7 @@
|
||||
<paraCount>0</paraCount>
|
||||
<cursorPos>76</cursorPos>
|
||||
</item>
|
||||
<item handle="ae7339df26ded" order="5" parent="e7ded148d6e4a">
|
||||
<item handle="ae7339df26ded" order="6" parent="e7ded148d6e4a">
|
||||
<name>We Found John!</name>
|
||||
<type>FILE</type>
|
||||
<class>NOVEL</class>
|
||||
|
||||
BIN
screenshot.png
BIN
screenshot.png
Binary file not shown.
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 211 KiB |
2
setup.py
2
setup.py
@@ -2,6 +2,6 @@
|
||||
from distutils.core import setup
|
||||
setup(
|
||||
name="novelwriter",
|
||||
version="0.3",
|
||||
version="0.3.2",
|
||||
py_modules=["nw"],
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@ geometry = 1100, 650
|
||||
treecols = 120, 30, 50
|
||||
mainpane = 300, 800
|
||||
docpane = 400, 400
|
||||
timeline = 600, 400
|
||||
|
||||
[Project]
|
||||
autosaveproject = 60
|
||||
@@ -38,6 +37,7 @@ backuponclose = False
|
||||
askbeforebackup = True
|
||||
|
||||
[Path]
|
||||
lastpath =
|
||||
recent0 =
|
||||
recent1 =
|
||||
recent2 =
|
||||
|
||||
@@ -14,6 +14,8 @@ def testConfigInit(nwTemp,nwRef):
|
||||
tmpConf = path.join(nwTemp,"novelwriter.conf")
|
||||
refConf = path.join(nwRef, "novelwriter.conf")
|
||||
assert theConf.initConfig(nwTemp)
|
||||
assert theConf.setLastPath("")
|
||||
assert theConf.saveConfig()
|
||||
assert cmpFiles(tmpConf, refConf, [2])
|
||||
assert not theConf.confChanged
|
||||
|
||||
|
||||
Reference in New Issue
Block a user