Compare commits

...

63 Commits
v0.3 ... v0.3.2

Author SHA1 Message Date
Veronica K. Berglyd Olsen
290cbc9a25 Merge pull request #74 from vkbo/release_0.3.2
Release 0.3.2
2019-10-27 17:50:55 +01:00
Veronica K. B. Olsen
6565438bdf Release 0.3.2 2019-10-27 17:48:29 +01:00
Veronica K. B. Olsen
cd3c32b2ff Always get markdown urls wrong ... 2019-10-27 17:36:07 +01:00
Veronica K. B. Olsen
8f15f22e85 Updated readme file 2019-10-27 17:34:50 +01:00
Veronica K. Berglyd Olsen
08acebdd65 Merge pull request #73 from vkbo/export_options
LaTeX Export
2019-10-27 15:47:25 +01:00
Veronica K. B. Olsen
43ebcec4e9 Updated documentation for export and the user interface 2019-10-27 15:44:40 +01:00
Veronica K. B. Olsen
aba339f8e3 Added documentation for export, and renamed all files to .txt as per recommendation 2019-10-27 14:55:11 +01:00
Veronica K. B. Olsen
25c92d8b2e Improved the html export a bit, with a max width and comments formatted as yellow notes 2019-10-27 14:18:01 +01:00
Veronica K. B. Olsen
fde7975a0b No need for hardcoding line endings for windows. Python handles this by default 2019-10-26 23:05:12 +02:00
Veronica K. B. Olsen
f38e770402 Added novelWriter flavour markdown as an export format, and cleaned up windows/unix line endings code 2019-10-26 22:58:16 +02:00
Veronica K. B. Olsen
753b248b9f Cleaned up the nw/convert folder a bit 2019-10-26 22:24:59 +02:00
Veronica K. B. Olsen
b9608dcdbd Add support for LaTeX export 2019-10-25 00:07:54 +02:00
Veronica K. B. Olsen
01dde7c8a4 Update tokenizer and html converter 2019-10-25 00:07:37 +02:00
Veronica K. B. Olsen
a67d3ed599 Set autoreplace characters with unicode code instead to make it easier to read the source code 2019-10-25 00:06:39 +02:00
Veronica K. B. Olsen
2abacc1b2f Some cleanup of the export gui 2019-10-24 22:18:41 +02:00
Veronica K. Berglyd Olsen
aff81c9a78 Merge pull request #69 from vkbo/documentation
Use online help instead of local
2019-10-24 21:55:17 +02:00
Veronica K. B. Olsen
4a98f58a20 Added more system checks and help now opens the documentation website 2019-10-24 21:13:11 +02:00
Veronica K. B. Olsen
3ae87b6cf4 Copied the rest of the documentation from the html version to the read .rst files 2019-10-24 18:44:04 +02:00
Veronica K. B. Olsen
15b94da2e6 Merge branch 'documentation' 2019-10-24 18:15:42 +02:00
Veronica K. B. Olsen
a7b7666f0c Updated readme and set docs theme to default 2019-10-24 18:15:27 +02:00
Veronica K. Berglyd Olsen
45f813d98a Merge pull request #68 from vkbo/documentation
Documentation Update - Step 1
2019-10-24 18:08:15 +02:00
Veronica K. B. Olsen
070f2a3985 Started adding documentation using sphinx 2019-10-24 18:03:39 +02:00
Veronica K. Berglyd Olsen
0d5899e844 Merge pull request #65 from vkbo/new_doc_from_file
Import Text File
2019-10-23 21:37:32 +02:00
Veronica K. B. Olsen
dc12d518c0 Fixed test 2019-10-23 21:34:25 +02:00
Veronica K. B. Olsen
9186289c1b Changed the spelling of 'unnumbered' 2019-10-23 21:22:35 +02:00
Veronica K. B. Olsen
68ddba1768 Added an unnumbered chapter to the sample project 2019-10-23 21:21:49 +02:00
Veronica K. B. Olsen
3419839295 Fix a corner case nonetype exit 2019-10-23 21:21:29 +02:00
Veronica K. B. Olsen
e4b36a8646 Make the import file explicitly mode='rt' 2019-10-23 21:13:51 +02:00
Veronica K. B. Olsen
558af9f329 Added an import file to document feature 2019-10-23 21:05:33 +02:00
Veronica K. B. Olsen
0f67e9b971 Improve the error messages when adding a new file or folder without selecting a location 2019-10-23 20:32:12 +02:00
Veronica K. Berglyd Olsen
9aeffdd333 Merge pull request #62 from vkbo/timeline_update
Minor improvements to timeline and session dialogs
2019-10-22 20:00:48 +02:00
Veronica K. Berglyd Olsen
1dc84c3aca Merge branch 'master' into timeline_update 2019-10-22 19:58:13 +02:00
Veronica K. B. Olsen
598eaa8c3c Remember settings for session log, auto-refresh timeline, and check valid settings 2019-10-22 19:56:56 +02:00
Veronica K. Berglyd Olsen
4a4775c8db Merge pull request #61 from vkbo/timeline_update
Timeline Update
2019-10-21 21:07:51 +02:00
Veronica K. B. Olsen
bbdf1aa8e1 Updated test 2019-10-21 21:04:55 +02:00
Veronica K. B. Olsen
1434c2bdf3 Added filters to timeline view GUI 2019-10-21 20:57:46 +02:00
Veronica K. B. Olsen
3aa07e9b33 Added a options state class for timeline view GUI 2019-10-21 18:59:03 +02:00
Veronica K. B. Olsen
e8de021abc Made the window options class in export a superclass for multiple windows 2019-10-21 18:50:35 +02:00
Veronica K. B. Olsen
39eae448b0 And the index ... 2019-10-20 18:50:10 +02:00
Veronica K. B. Olsen
d9a8f860f0 Mixed up the session log and timelien window 2019-10-20 18:48:53 +02:00
Veronica K. B. Olsen
b40e1bac4a Typos 2019-10-20 16:57:19 +02:00
Veronica K. Berglyd Olsen
d4ac76aa3a Merge pull request #60 from vkbo/release_0.3.1
Release 0.3.1
2019-10-20 16:50:26 +02:00
Veronica K. B. Olsen
1275d6ef16 Forgot to mention PR 57 2019-10-20 16:48:07 +02:00
Veronica K. B. Olsen
2ac8f371ec Updated version and changelog 2019-10-20 16:45:06 +02:00
Veronica K. Berglyd Olsen
86911ace70 Merge pull request #59 from vkbo/session_log
Session Log
2019-10-20 16:40:45 +02:00
Veronica K. B. Olsen
436aba4fd1 Label text changes 2019-10-20 16:37:33 +02:00
Veronica K. B. Olsen
4980468a55 Some improvements to the sesison gui 2019-10-20 16:35:51 +02:00
Veronica K. B. Olsen
785a69d34b Session log dialog window is now complete 2019-10-20 16:26:49 +02:00
Veronica K. B. Olsen
2fe001b336 Session log is now printed in the table 2019-10-20 15:52:25 +02:00
Veronica K. B. Olsen
50f4d8e073 Merge branch 'master' into session_log 2019-10-20 14:41:40 +02:00
Veronica K. Berglyd Olsen
70b3b24cd3 Merge pull request #58 from vkbo/fixes
Bugfixes
2019-10-20 14:41:16 +02:00
Veronica K. B. Olsen
323013bbaf Added session log dialog window 2019-10-20 14:40:59 +02:00
Veronica K. B. Olsen
79116f8977 Make sure the open document is saved before applying changes from the config dialog 2019-10-20 14:28:51 +02:00
Veronica K. B. Olsen
fe12e99735 Fixed a bug where unicode characters were not included in the spell check regex 2019-10-20 14:23:27 +02:00
Veronica K. B. Olsen
77c6caacee When closing a project, it should ask for backup even if the project is saved. 2019-10-20 13:56:21 +02:00
Veronica K. B. Olsen
6638c5187e Remove margins from virtical box in doc editor 2019-10-19 17:24:15 +02:00
Veronica K. B. Olsen
897583ce8d Merge branch 'master' of github.com:vkbo/novelWriter 2019-10-19 17:14:10 +02:00
Veronica K. B. Olsen
bfd44ecae9 Missing import in html and md export 2019-10-19 15:19:56 +02:00
Veronica K. Berglyd Olsen
ba15efc715 Merge pull request #57 from vkbo/more_export
Export to Markdown and Html
2019-10-19 15:15:07 +02:00
Veronica K. B. Olsen
6f04d91bf9 Merge branch 'master' into more_export 2019-10-19 15:09:12 +02:00
Veronica K. B. Olsen
c0d52ce2f1 Formatting bug 2019-10-19 15:08:42 +02:00
Veronica K. B. Olsen
691717ce96 Fixed a few bugs 2019-10-19 15:05:28 +02:00
Veronica K. B. Olsen
16401f4c1f Added export to html and markdown files 2019-10-19 14:59:32 +02:00
51 changed files with 2320 additions and 990 deletions

4
.gitignore vendored
View File

@@ -1,6 +1,10 @@
# Setup
MANIFEST
dist/
deploy/
# Documentation
docs/build/
# Python Temp
__pycache__

View File

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

@@ -2,15 +2,16 @@
[![Build Status](https://travis-ci.com/vkbo/novelWriter.svg?branch=master)](https://travis-ci.com/vkbo/novelWriter)
[![codecov](https://codecov.io/gh/vkbo/novelWriter/branch/master/graph/badge.svg)](https://codecov.io/gh/vkbo/novelWriter)
[![Documentation Status](https://readthedocs.org/projects/novelwriter/badge/?version=latest)](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
![Screenshot 1](screenshot.png)

19
docs/Makefile Normal file
View 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
View 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
View 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
View 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
View 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
View 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."

View 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
View 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
View 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
View 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
View 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.

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

@@ -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 = {
"<" : "&lt;",
">" : "&gt;",
"&" : "&amp;",
"<" : "&lt;",
">" : "&gt;",
"&" : "&amp;",
"\u2013" : "&endash;",
"\u2014" : "$emdash;",
"\u2500" : "$emdash;",
"\u2026" : "&hellip;",
}
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
View 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 KiB

After

Width:  |  Height:  |  Size: 211 KiB

View File

@@ -2,6 +2,6 @@
from distutils.core import setup
setup(
name="novelwriter",
version="0.3",
version="0.3.2",
py_modules=["nw"],
)

View File

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

View File

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