diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml
index 038480f7..bf8ffec3 100644
--- a/.github/workflows/syntax.yml
+++ b/.github/workflows/syntax.yml
@@ -25,5 +25,5 @@ jobs:
flake8 tests --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Coding Style Violations
run: |
- flake8 nw --count --max-line-length=99 --ignore E203,E221,E226,E241,E251,E261,E266,E302,E305 --show-source --statistics
- flake8 tests --count --max-line-length=99 --ignore E203,E221,E226,E241,E251,E261,E266,E302,E305 --show-source --statistics
+ flake8 nw --count --max-line-length=99 --ignore E203,E221,E226,E228,E241,E251,E261,E266,E302,E305 --show-source --statistics
+ flake8 tests --count --max-line-length=99 --ignore E203,E221,E226,E228,E241,E251,E261,E266,E302,E305 --show-source --statistics
diff --git a/.gitignore b/.gitignore
index 57dc50a9..830a409a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,6 +5,7 @@
/deploy/
*.spec
*.egg-info
+setup.iss
# Documentation
/docs/build/
diff --git a/MANIFEST.in b/MANIFEST.in
index 36a99646..b3004fc3 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,4 +1,4 @@
include LICENSE.md
-recursive-include assets *
+recursive-include setup *
recursive-include nw/assets *
recursive-include sample *.nwx *.nwd
diff --git a/README.md b/README.md
index 46273c44..9efa0b24 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
[](https://pypi.org/project/novelWriter)
[](https://pypi.org/project/novelWriter)
-
+
novelWriter is a Markdown-like text editor designed for writing novels and larger projects of many
smaller plain text documents. It uses its own flavour of Markdown that supports a meta data syntax
@@ -52,63 +52,65 @@ in principle work fine on other operating systems as well as long as dependencie
tests are run on the latest versions of Ubuntu Linux, Windows Server and macOS.
-## Installing and Running
+# Installing and Running
-You can runt novelWriter either from a downloaded copy of the source code, or by running:
+novelWriter is available on [pypi.org](https://pypi.org/project/novelWriter/), and can be installed with:
```bash
pip install novelwriter
```
-**Note:** On some systems you must use `pip3` instead for the Python 3 version.
-You can update novelWriter to the latest version by running:
+To upgrade an existing installation, use:
```bash
pip install --upgrade novelwriter
```
-The application can then be started with one of the commands, depending on your Python configuration:
+Dependencies are installed automatically, but can generally be installed with:
+```bash
+pip install -r requirements.txt
+```
+
+Below are some brief instructions on how to get started on different operating systems.
+
+
+## Linux
+
+Either download the source, or install with pip.
+
+If you run from source, install the dependencies via pip, or directly from the OS repo.
+There are very few dependencies, and they should be available in the standard repo.
+The Python packages needed are `pyqt5`, `lxml` and `pyenchant`.
+
+### Installing from Source
+
+You can also install novelWriter from source with:
+```bash
+python3 setup.py sample
+sudo python3 setup.py install
+sudo python3 setup.py launcher
+```
+
+The last line will install the application icons and set up a launcher for novelWriter.
+The method uses hardcoded paths, so it may or may not work for your Linux distro.
+If you have any issues, please submit a ticket so the script can be tuned.
+
+The script may prompt you to choose which executable to configure if it finds more than one.
+
+### Running from Source
+
+If you want to run directly from the source, the application can be started with:
```bash
./novelWriter.py
-python novelWriter.py
-python3 novelWriter.py
```
-It also takes a few parameters for debugging and such, which can be listed with the switch `--help`.
-The `--info`, `--debug` or `--verbose` flags are particularly useful for increasing logging output
-for debugging.
-
-You can also provide a path to a folder containing a novelWriter project as the last parameter.
-
-
-### Launcher and Icons
-
-In the root assets folder there are icons and scripts and a template for setting up a launcher on
-Gnome desktops. You may need to modify those scripts slightly, but as they are, they work on Debian
-and Ubuntu. For other operating systems, please consult your operating system documentation for how
-to make those. Feel free to submit more if you are able to make them.
-
-
-## Package Dependencies
-
-It is recommended that novelWriter runs with Qt 5.10 or later, and requires Python 3.6 or later.
-Minimum version of Qt is 5.2.
-
-
-### Linux
-
-Generally, dependencies can be installed via `pip` with:
+You can also create a launcher for running directly from source with:
```bash
-pip3 install -r requirements.txt
+sudo python3 setup.py launcher
```
-You can also install the packages from the distro's own package manager.
-For the apt package manager on Debian/Ubuntu systems, the following Python3 packages are needed:
-
-* `python3-pyqt5` for the GUI
-* `python3-lxml` for writing project files
-* `python3-enchant` for better spell checking (optional)
+For more install options, see [Build and Install novelWriter](setup/README.md).
-### macOS
+## macOS
These instructions assume you're using brew, and have Python and pip set up.
If not, see the [brew docs](https://docs.brew.sh/Homebrew-and-Python) for help.
@@ -126,11 +128,16 @@ It comes with a lot of default dictionaries.
brew install enchant
```
+## Windows
-### Windows
+On Windows, you may first need to install Python.
+See the [python.org](https://www.python.org/) website for download packages.
+It is recommended that you install the latest version of Python 3.8.
-On Windows, the `pip install` command is generally sufficient to install everything you need.
-That should also install the Qt libraries and the spell check dictionary dependencies.
+To install dependencies, run:
+```bash
+pip install --user -r requirements.txt
+```
**Note:** On Windows, make sure Python3 is in your PATH if you want to launch novelWriter from
command line. You can also right click the `novelWriter.py` file, create a shortcut, then right
@@ -142,8 +149,11 @@ It should look something like this:
C:\...\AppData\Local\Programs\Python\Python38\python.exe novelWriter.py
```
+You can also run the `make.py` script to generate a single executable, or an installer.
+See [Build and Install novelWriter](setup/README.md) for more details.
-### Package Versions
+
+## Package Versions
Exporting to Markdown requires PyQt/Qt 5.14. There are no known minimum for `lxml`, but the code
was originally written with 4.2. The optional spell check library must be at least 3.0.0 to work
@@ -155,8 +165,15 @@ checker, but more can be added to the `nw/assets/dict` folder. See the [README](
file in that folder for how to generate more dictionaries. Note that the difflib-based option is
both slow and limited.
+## Debugging
-## Key Features
+If you need to debug novelWriter, you must run it from command line.
+It takes a few parameters, which can be listed with the switch `--help`.
+The `--info`, `--debug` or `--verbose` flags are particularly useful for increasing logging output
+for debugging.
+
+
+# Key Features
Some features of novelWriter are listed below. Consult the documentation for more information.
diff --git a/assets/installDebian.sh b/assets/installDebian.sh
deleted file mode 100755
index 3f5ae392..00000000
--- a/assets/installDebian.sh
+++ /dev/null
@@ -1,49 +0,0 @@
-#!/bin/bash
-
-cd ..
-
-EXEC=$(pwd)/novelWriter.py
-EXEC=$(echo $EXEC | sed 's_/_\\/_g')
-
-sed "s/%%exec%%/$EXEC/g" assets/novelwriter.desktop > /usr/share/applications/novelwriter.desktop
-
-if [ ! -d /usr/share/icons/hicolor/24x24/apps ]; then
- mkdir -pv /usr/share/icons/hicolor/24x24/apps
-fi
-if [ ! -d /usr/share/icons/hicolor/48x48/apps ]; then
- mkdir -pv /usr/share/icons/hicolor/48x48/apps
-fi
-if [ ! -d /usr/share/icons/hicolor/96x96/apps ]; then
- mkdir -pv /usr/share/icons/hicolor/96x96/apps
-fi
-if [ ! -d /usr/share/icons/hicolor/256x256/apps ]; then
- mkdir -pv /usr/share/icons/hicolor/256x256/apps
-fi
-if [ ! -d /usr/share/icons/hicolor/512x512/apps ]; then
- mkdir -pv /usr/share/icons/hicolor/512x512/apps
-fi
-if [ ! -d /usr/share/icons/hicolor/scalable/apps ]; then
- mkdir -pv /usr/share/icons/hicolor/scalable/apps
-fi
-if [ ! -d /usr/share/icons/hicolor/scalable/mimetypes ]; then
- mkdir -pv /usr/share/icons/hicolor/scalable/mimetypes
-fi
-
-cp -v assets/icons/24x24/novelwriter.png /usr/share/icons/hicolor/24x24/apps/
-cp -v assets/icons/48x48/novelwriter.png /usr/share/icons/hicolor/48x48/apps/
-cp -v assets/icons/96x96/novelwriter.png /usr/share/icons/hicolor/96x96/apps/
-cp -v assets/icons/256x256/novelwriter.png /usr/share/icons/hicolor/256x256/apps/
-cp -v assets/icons/512x512/novelwriter.png /usr/share/icons/hicolor/512x512/apps/
-cp -v assets/icons/novelwriter.svg /usr/share/icons/hicolor/scalable/apps/
-cp -v assets/icons/x-novelwriter-project.svg /usr/share/icons/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg
-cp -v assets/mime/x-novelwriter-project.xml /usr/share/mime/packages/
-
-update-mime-database /usr/share/mime/
-update-icon-caches /usr/share/icons/hicolor/24x24/apps/
-update-icon-caches /usr/share/icons/hicolor/48x48/apps/
-update-icon-caches /usr/share/icons/hicolor/96x96/apps/
-update-icon-caches /usr/share/icons/hicolor/256x256/apps/
-update-icon-caches /usr/share/icons/hicolor/512x512/apps/
-update-icon-caches /usr/share/icons/hicolor/1024x1024/apps/
-update-icon-caches /usr/share/icons/hicolor/scalable/apps/
-update-icon-caches /usr/share/icons/hicolor/scalable/mimetypes/
diff --git a/assets/installUbuntu.sh b/assets/installUbuntu.sh
deleted file mode 100755
index 91e78409..00000000
--- a/assets/installUbuntu.sh
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/bin/bash
-
-cd ..
-
-EXEC=$(pwd)/novelWriter.py
-EXEC=$(echo $EXEC | sed 's_/_\\/_g')
-
-sed "s/%%exec%%/$EXEC/g" assets/novelwriter.desktop > /usr/share/applications/novelwriter.desktop
-
-if [ ! -d /usr/share/icons/hicolor/24x24/apps ]; then
- mkdir -pv /usr/share/icons/hicolor/24x24/apps
-fi
-if [ ! -d /usr/share/icons/hicolor/48x48/apps ]; then
- mkdir -pv /usr/share/icons/hicolor/48x48/apps
-fi
-if [ ! -d /usr/share/icons/hicolor/96x96/apps ]; then
- mkdir -pv /usr/share/icons/hicolor/96x96/apps
-fi
-if [ ! -d /usr/share/icons/hicolor/256x256/apps ]; then
- mkdir -pv /usr/share/icons/hicolor/256x256/apps
-fi
-if [ ! -d /usr/share/icons/hicolor/512x512/apps ]; then
- mkdir -pv /usr/share/icons/hicolor/512x512/apps
-fi
-if [ ! -d /usr/share/icons/hicolor/scalable/apps ]; then
- mkdir -pv /usr/share/icons/hicolor/scalable/apps
-fi
-if [ ! -d /usr/share/icons/hicolor/scalable/mimetypes ]; then
- mkdir -pv /usr/share/icons/hicolor/scalable/mimetypes
-fi
-
-cp -v assets/icons/24x24/novelwriter.png /usr/share/icons/hicolor/24x24/apps/
-cp -v assets/icons/48x48/novelwriter.png /usr/share/icons/hicolor/48x48/apps/
-cp -v assets/icons/96x96/novelwriter.png /usr/share/icons/hicolor/96x96/apps/
-cp -v assets/icons/256x256/novelwriter.png /usr/share/icons/hicolor/256x256/apps/
-cp -v assets/icons/512x512/novelwriter.png /usr/share/icons/hicolor/512x512/apps/
-cp -v assets/icons/novelwriter.svg /usr/share/icons/hicolor/scalable/apps/
-cp -v assets/icons/x-novelwriter-project.svg /usr/share/icons/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg
-cp -v assets/mime/x-novelwriter-project.xml /usr/share/mime/packages/
-
-update-mime-database /usr/share/mime/
-update-icon-caches /usr/share/icons/*
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 7aef4aa3..c0f62bfc 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -12,7 +12,6 @@
# 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("."))
import os
diff --git a/docs/source/int_started.rst b/docs/source/int_started.rst
index e96a0285..0d0236c8 100644
--- a/docs/source/int_started.rst
+++ b/docs/source/int_started.rst
@@ -140,7 +140,7 @@ encountered. To list all options, run:
python novelWriter.py --help
-There are also a couple of install scripts in the assets folder which will assist in setting up a
+There are also a couple of install scripts in the setup folder which will assist in setting up a
launch icon and the novelWriter project file mimetype for Gnome desktops on Linux. Currently,
there's one script for Debian and one for Ubuntu.
diff --git a/install.py b/install.py
deleted file mode 100755
index 11754a26..00000000
--- a/install.py
+++ /dev/null
@@ -1,84 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-import os
-import sys
-import getopt
-import subprocess
-
-# Defaults
-buildWindowed = True
-
-# Parse Options
-shortOpt = "hd"
-longOpt = [
- "help",
- "debug",
-]
-helpMsg = (
- "\n"
- "novelWriter Install Script\n"
- "\n"
- "Usage:\n"
- " -h, --help Print this message.\n"
- " -d, --debug Build novelWriter for debugging. To debug novelwriter after build,\n"
- " run it from command line with the debug options. Please check the\n"
- " novelWriter --help output for details.\n"
-)
-
-try:
- inOpts, inArgs = getopt.getopt(sys.argv[1:], shortOpt, longOpt)
-except getopt.GetoptError:
- print(helpMsg)
- sys.exit(2)
-
-for inOpt, inArg in inOpts:
- if inOpt in ("-h", "--help"):
- print(helpMsg)
- sys.exit(0)
- elif inOpt in ("-d", "--debug"):
- buildWindowed = False
-
-# Run pip
-packList = ["pyinstaller"]
-with open("requirements.txt", mode="r") as reqFile:
- for reqPack in reqFile:
- if len(reqPack.strip()) > 0:
- packList.append(reqPack)
-
-for packName in packList:
- print("Installing package dependency: %s" % packName)
- try:
- subprocess.call([sys.executable, "-m", "pip", "install", packName])
- except Exception as e:
- print("Failed with error:")
- print(str(e))
-
-# Run pyinstaller
-if sys.platform.startswith("win32"):
- dotDot = ";"
-else:
- dotDot = ":"
-
-instOpt = [
- "--name=novelWriter",
- "--clean",
- "--onefile",
- "--add-data=%s%s%s" % (os.path.join("nw", "assets"), dotDot, "assets"),
- "--icon=%s" % os.path.join("nw", "assets", "icons", "novelwriter.ico"),
-]
-if buildWindowed:
- instOpt.append("--windowed")
-
-instOpt.append("novelWriter.py")
-
-import PyInstaller.__main__ # noqa: E402
-PyInstaller.__main__.run(instOpt)
-
-print("")
-print("##################")
-print(" Build Finished")
-print("##################")
-print("")
-print("If everything went well, the novelWriter executable should be in the folder named 'dist'")
-print("")
diff --git a/make.py b/make.py
new file mode 100755
index 00000000..859ad8ca
--- /dev/null
+++ b/make.py
@@ -0,0 +1,333 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+This make script is intended for building distributable packages of
+novelWriter. These are either:
+
+ * A single file executable named dist/novelWriter(.exe). This is a
+ quite slow option, and the file is fairly big.
+ * A single directory named dist/novelWriter with a novelWriter(.exe),
+ and all dependecies included.
+ * The latter can be combined with a build stage of a setup.exe file if
+ on Windows. This requires Inno Setup to be installed and in path.
+
+In addition, providing the pip otion will cause the script to try to
+install all dependencies needed for runing the build, and for running
+novelWriter itself.
+"""
+
+import os
+import sys
+import shutil
+import subprocess
+
+OS_NONE = 0
+OS_LINUX = 1
+OS_WIN = 2
+OS_DARWIN = 3
+
+# =============================================================================================== #
+# Package Installer
+# =============================================================================================== #
+
+def installPackages(hostOS):
+ """Install package dependencies both for this script and for running
+ novelWriter itself.
+ """
+ print("")
+ print("Installing Dependencies")
+ print("#######################")
+ print("")
+
+ installQueue = ["pip", "pyinstaller", "-r requirements.txt"]
+ if hostOS == OS_DARWIN:
+ installQueue.append("pyobjc")
+
+ pyCmd = [sys.executable, "-m"]
+ pipCmd = ["pip", "install", "--user", "--upgrade"]
+ for stepCmd in installQueue:
+ pkgCmd = stepCmd.split(" ")
+ try:
+ subprocess.call(pyCmd + pipCmd + pkgCmd)
+ except Exception as e:
+ print("Failed with error:")
+ print(str(e))
+ sys.exit(1)
+
+ return
+
+# =============================================================================================== #
+# Run PyInstaller on Package
+# =============================================================================================== #
+
+def freezePackage(buildWindowed, oneFile, makeSetup, hostOS):
+ """Run PyInstaller to freeze the packages. This assumes all
+ dependencies are already in place.
+ """
+ import PyInstaller.__main__ # noqa: E402
+
+ print("")
+ print("Running PyInstaller")
+ print("###################")
+ print("")
+
+ if hostOS == OS_WIN:
+ dotDot = ";"
+ else:
+ dotDot = ":"
+
+ sys.modules["FixTk"] = None
+ instOpt = [
+ "--name=novelWriter",
+ "--clean",
+ "--add-data=%s%s%s" % (os.path.join("nw", "assets"), dotDot, "assets"),
+ "--icon=%s" % os.path.join("nw", "assets", "icons", "novelwriter.ico"),
+ "--exclude-module=PyQt5.QtQml",
+ "--exclude-module=PyQt5.QtBluetooth",
+ "--exclude-module=PyQt5.QtDBus",
+ "--exclude-module=PyQt5.QtMultimedia",
+ "--exclude-module=PyQt5.QtMultimediaWidgets",
+ "--exclude-module=PyQt5.QtNetwork",
+ "--exclude-module=PyQt5.QtNetworkAuth",
+ "--exclude-module=PyQt5.QtNfc",
+ "--exclude-module=PyQt5.QtQuick",
+ "--exclude-module=PyQt5.QtQuickWidgets",
+ "--exclude-module=PyQt5.QtRemoteObjects",
+ "--exclude-module=PyQt5.QtSensors",
+ "--exclude-module=PyQt5.QtSerialPort",
+ "--exclude-module=PyQt5.QtSql",
+ "--exclude-module=FixTk",
+ "--exclude-module=tcl",
+ "--exclude-module=tk",
+ "--exclude-module=_tkinter",
+ "--exclude-module=tkinter",
+ "--exclude-module=Tkinter",
+ ]
+
+ if buildWindowed:
+ instOpt.append("--windowed")
+
+ if oneFile and not makeSetup:
+ instOpt.append("--onefile")
+ else:
+ instOpt.append("--onedir")
+
+ instOpt.append("novelWriter.py")
+
+ # Make sample.zip first
+ try:
+ subprocess.call([sys.executable, "setup.py", "sample"])
+ except Exception as e:
+ print("Failed with error:")
+ print(str(e))
+ sys.exit(1)
+
+ PyInstaller.__main__.run(instOpt)
+
+ if not oneFile:
+ # These files are not needed, and take up a fair bit of space.
+ delFiles = []
+ if hostOS == OS_WIN:
+ delFiles = [
+ "Qt5DBus.dll",
+ "Qt5Network.dll",
+ "Qt5Qml.dll",
+ "Qt5QmlModels.dll",
+ "Qt5Quick.dll",
+ "Qt5Quick3D.dll",
+ "Qt5Quick3DAssetImport.dll",
+ "Qt5Quick3DRender.dll",
+ "Qt5Quick3DRuntimeRender.dll",
+ "Qt5Quick3DUtils.dll",
+ "Qt5Sql.dll"
+ ]
+ elif hostOS == OS_LINUX:
+ delFiles = [
+ "libQt5DBus.so.5",
+ "libQt5Network.so.5",
+ "libQt5Qml.so.5",
+ "libQt5QmlModels.so.5",
+ "libQt5Quick.so.5",
+ "libQt5Quick3D.so.5",
+ "libQt5Quick3DAssetImport.so.5",
+ "libQt5Quick3DRender.so.5",
+ "libQt5Quick3DRuntimeRender.so.5",
+ "libQt5Quick3DUtils.so.5",
+ "libQt5Sql.so.5"
+ ]
+ distDir = os.path.join(os.getcwd(), "dist", "novelWriter")
+ for delFile in delFiles:
+ delPath = os.path.join(distDir, delFile)
+ if os.path.isfile(delPath):
+ print("Deleting file: %s" % delPath)
+ os.unlink(delPath)
+
+ print("")
+ print("Build Finished")
+ print("")
+ print("The novelWriter executable should be in the folder named 'dist'")
+ print("")
+
+ return
+
+# =============================================================================================== #
+# Inno Setup Builder
+# =============================================================================================== #
+
+def innoSetup():
+ """Run the Inno Setup tool to build a setup.exe file for Windows.
+ """
+ print("")
+ print("Running Inno Setup")
+ print("##################")
+ print("")
+
+ # Read the iss template
+ issData = ""
+ with open(os.path.join("setup", "win_setup.iss"), mode="r") as inFile:
+ issData = inFile.read()
+
+ import nw # noqa: E402
+ issData = issData.replace(r"%%version%%", nw.__version__)
+ issData = issData.replace(r"%%dir%%", os.getcwd())
+
+ with open("setup.iss", mode="w+") as outFile:
+ outFile.write(issData)
+
+ try:
+ subprocess.call(["iscc", "setup.iss"])
+ except Exception as e:
+ print("Inno Setup failed with error:")
+ print(str(e))
+ sys.exit(1)
+
+ return
+
+# =============================================================================================== #
+# Clean Build and Dist Folders
+# =============================================================================================== #
+
+def cleanInstall():
+ """Recursively delete the 'build' and 'dist' folders.
+ """
+ print("")
+ print("Cleaning up build environment ...")
+
+ buildDir = os.path.join(os.getcwd(), "build")
+ if os.path.isdir(buildDir):
+ try:
+ shutil.rmtree(buildDir)
+ print("Deleted folder 'build'")
+ except Exception as e:
+ print("Error: Cannot delete 'build' folder.")
+ print(str(e))
+ sys.exit(1)
+ else:
+ print("Folder 'build' not found")
+
+ distDir = os.path.join(os.getcwd(), "dist")
+ if os.path.isdir(distDir):
+ try:
+ shutil.rmtree(distDir)
+ print("Deleted folder 'dist'")
+ except Exception as e:
+ print("Error: Cannot delete 'dist' folder.")
+ print(str(e))
+ sys.exit(1)
+ else:
+ print("Folder 'dist' not found")
+
+ print("")
+
+ return
+
+# =============================================================================================== #
+# Process Build Steps
+# =============================================================================================== #
+
+if __name__ == "__main__":
+ """Parse command line options and run the commands.
+ """
+ # Detect OS
+ if sys.platform.startswith("linux"):
+ hostOS = OS_LINUX
+ elif sys.platform.startswith("darwin"):
+ hostOS = OS_DARWIN
+ elif sys.platform.startswith("win32"):
+ hostOS = OS_WIN
+ elif sys.platform.startswith("cygwin"):
+ hostOS = OS_WIN
+ else:
+ hostOS = OS_NONE
+
+ # Flags and Variables
+ buildWindowed = True
+ oneFile = False
+ makeSetup = False
+ doFreeze = False
+
+ helpMsg = (
+ "\n"
+ "novelWriter Make Tool\n"
+ "=====================\n"
+ "This tool provides build commands for distibuting novelWriter as a\n"
+ "package. The available options are as follows:\n"
+ "\n"
+ "help Print the help message.\n"
+ "freeze Freeze the package and produces a folder of all\n"
+ " dependencies using pyinstaller.\n"
+ "onefile Build a standalone executable with all dependencies\n"
+ " bundled. Implies 'freeze', cannot be used with 'setup'.\n"
+ "pip Run pip to install all package dependencies for\n"
+ " novelWriter and this build tool.\n"
+ "setup Build a setup.exe installer for Windows. This option\n"
+ " automaticall disables the 'onefile' option.\n"
+ "clean This will attempt to delete the 'build' and 'dist'\n"
+ " folders in the current folder.\n"
+ )
+
+ if "help" in sys.argv or len(sys.argv) <= 1:
+ print(helpMsg)
+ sys.exit(0)
+
+ if not os.path.isfile(os.path.join(os.getcwd(), "novelWriter.py")):
+ print("Error: This script must be run in the root folder of novelWriter.")
+ sys.exit(1)
+
+ if not os.path.isdir(os.path.join(os.getcwd(), "nw")):
+ print("Error: This script must be run in the root folder of novelWriter.")
+ sys.exit(1)
+
+ if "clean" in sys.argv:
+ sys.argv.remove("clean")
+ cleanInstall()
+
+ if "pip" in sys.argv:
+ sys.argv.remove("pip")
+ installPackages(hostOS)
+
+ if "freeze" in sys.argv:
+ sys.argv.remove("freeze")
+ doFreeze = True
+
+ if "onefile" in sys.argv:
+ sys.argv.remove("onefile")
+ doFreeze = True
+ oneFile = True
+
+ if "setup" in sys.argv:
+ sys.argv.remove("setup")
+ if hostOS == OS_WIN:
+ oneFile = False
+ makeSetup = True
+ else:
+ print("Error: Argument 'setup' for Inno Setup is Windows only.")
+ sys.exit(1)
+
+ if doFreeze:
+ freezePackage(buildWindowed, oneFile, makeSetup, hostOS)
+
+ if makeSetup:
+ innoSetup()
+
+# END Main
diff --git a/nw/__init__.py b/nw/__init__.py
index 38c9a263..516b471d 100644
--- a/nw/__init__.py
+++ b/nw/__init__.py
@@ -35,6 +35,28 @@ from PyQt5.QtWidgets import QApplication, QErrorMessage
from nw.error import exceptionHandler
from nw.config import Config
+#
+# Version Scheme
+# ================
+# Generally follows PEP 440
+# Hex Version:
+# - Digit 1,2 : Major Version (01, 02, 03)
+# = Digit 3,4 : Minor Version (01, 09, 10, 99)
+# - Digit 5,6 : Patch Version (01, 09, 10, 99)
+# = Digit 7 : Release Type (a: aplha, b: beta, c: candidate, f: final)
+# - Digit 8 : Release Number (0-9)
+#
+# Example : Full Short Description
+# -------------------------------------------------------------------------
+# 0x010200a0 : 1.2-alpha0 1.2a0 Can be used for the dev branch
+# 0x010200a1 : 1.2-alpha1 1.2a1 First alpha release
+# 0x010200b1 : 1.2-beta1 1.2b1 First beta release
+# 0x010200c1 : 1.2-rc1 1.2rc1 First release candidate
+# 0x010200f0 : 1.2 1.2 Final release
+# 0x010200f1 : 1.2-post1 1.2.post1 Post release, but not a code patch!
+# 0x010201f0 : 1.2.1 1.2.1 Patch release
+#
+
__package__ = "nw"
__author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen"
diff --git a/nw/constants/iso.py b/nw/constants/iso.py
index 69bd6d23..ccdc0808 100644
--- a/nw/constants/iso.py
+++ b/nw/constants/iso.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
-"""novelWriter Language Codes
+"""novelWriter ISO Codes
- novelWriter – Language Codes
-==============================
+ novelWriter – ISO Codes
+=========================
Handles translating language codes to language names
File History:
diff --git a/nw/core/document.py b/nw/core/document.py
index 1261c12d..1452e9dd 100644
--- a/nw/core/document.py
+++ b/nw/core/document.py
@@ -67,7 +67,9 @@ class NWDoc():
def openDocument(self, tHandle, showStatus=True, isOrphan=False):
"""Open a document from handle, capturing potential file system
- errors and parse meta data.
+ errors and parse meta data. If the document doesn't exist on
+ disk, return an empty string. If something went wrong, return
+ None.
"""
if not isHandle(tHandle):
return None
@@ -120,13 +122,13 @@ class NWDoc():
logger.verbose("DocMeta: '%s'" % self._docMeta)
if showStatus and not isOrphan:
- self.theParent.statusBar.setStatus("Opened Document: %s" % self._theItem.itemName)
+ self.theParent.setStatus("Opened Document: %s" % self._theItem.itemName)
return theText
def saveDocument(self, docText):
- """Save the document via temp file in case of save failure, and
- in any case keep a backup of the file.
+ """Save the document. The file is saved via a temp file in case
+ of save failure. Returns True if successful, False if not.
"""
if self._docHandle is None:
return False
@@ -139,6 +141,7 @@ class NWDoc():
docPath = os.path.join(self.theProject.projContent, docFile)
docTemp = os.path.join(self.theProject.projContent, docFile+"~")
+ # DocMeta line
if self._theItem is None:
docMeta = ""
else:
@@ -166,12 +169,12 @@ class NWDoc():
os.unlink(docPath)
os.rename(docTemp, docPath)
- self.theParent.statusBar.setStatus("Saved Document: %s" % self._theItem.itemName)
+ self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName)
return True
def deleteDocument(self, tHandle):
- """Permanently delete a document source file and its backups
+ """Permanently delete a document source file and related files
from the project data folder.
"""
if not isHandle(tHandle):
diff --git a/nw/core/index.py b/nw/core/index.py
index f715d835..c08562e1 100644
--- a/nw/core/index.py
+++ b/nw/core/index.py
@@ -279,7 +279,7 @@ class NWIndex():
if theItem.itemLayout == nwItemLayout.NO_LAYOUT:
logger.info("Not indexing no-layout item %s" % tHandle)
return False
- if theItem.parHandle is None:
+ if theItem.itemParent is None:
logger.info("Not indexing orphaned item %s" % tHandle)
return False
@@ -288,7 +288,7 @@ class NWIndex():
self.textCounts[tHandle] = [cC, wC, pC]
# If the file is archived or trashed, we don't index the file itself
- if self.theProject.projTree.isTrashRoot(theItem.parHandle):
+ if self.theProject.projTree.isTrashRoot(theItem.itemParent):
logger.info("Not indexing trash item %s" % tHandle)
return False
if theRoot.itemClass == nwItemClass.ARCHIVE:
@@ -583,7 +583,7 @@ class NWIndex():
def getCounts(self, tHandle, sTitle=None):
"""Returns the counts for a file, or a section of a file
- starting at title nTitle.
+ starting at title sTitle if it is provided.
"""
cC = 0
wC = 0
diff --git a/nw/core/item.py b/nw/core/item.py
index 702287dd..944b5f0c 100644
--- a/nw/core/item.py
+++ b/nw/core/item.py
@@ -42,7 +42,7 @@ class NWItem():
self.itemName = ""
self.itemHandle = None
- self.parHandle = None
+ self.itemParent = None
self.itemOrder = None
self.itemType = nwItemType.NO_TYPE
self.itemClass = nwItemClass.NO_CLASS
@@ -70,7 +70,7 @@ class NWItem():
xPack = etree.SubElement(xParent, "item", attrib={
"handle" : str(self.itemHandle),
"order" : str(self.itemOrder),
- "parent" : str(self.parHandle),
+ "parent" : str(self.itemParent),
})
self._subPack(xPack, "name", text=str(self.itemName))
self._subPack(xPack, "type", text=str(self.itemType.name))
@@ -85,6 +85,7 @@ class NWItem():
self._subPack(xPack, "cursorPos", text=str(self.cursorPos), none=False)
else:
self._subPack(xPack, "expanded", text=str(self.isExpanded))
+
return
def unpackXML(self, xItem):
@@ -101,7 +102,7 @@ class NWItem():
return False
if "parent" in xItem.attrib:
- self.parHandle = xItem.attrib["parent"]
+ self.itemParent = xItem.attrib["parent"]
setMap = {
"name" : self.setName,
@@ -131,9 +132,11 @@ class NWItem():
"""
if not none and (text is None or text == "None"):
return None
- xSub = etree.SubElement(xParent, name, attrib=attrib)
+ xAttr = {} if attrib is None else attrib
+ xSub = etree.SubElement(xParent, name, attrib=xAttr)
if text is not None:
xSub.text = text
+
return
##
@@ -162,14 +165,14 @@ class NWItem():
"""Set the parent handle, and ensure that it is valid.
"""
if theParent is None:
- self.parHandle = None
+ self.itemParent = None
elif isinstance(theParent, str):
if len(theParent) == 13:
- self.parHandle = theParent
+ self.itemParent = theParent
else:
- self.parHandle = None
+ self.itemParent = None
else:
- self.parHandle = None
+ self.itemParent = None
return
def setOrder(self, theOrder):
diff --git a/nw/core/project.py b/nw/core/project.py
index 46e37202..622bcc38 100644
--- a/nw/core/project.py
+++ b/nw/core/project.py
@@ -782,7 +782,7 @@ class NWProject():
"""Create a zip file of the entire project.
"""
logger.info("Backing up project")
- self.theParent.statusBar.setStatus("Backing up project ...")
+ self.theParent.setStatus("Backing up project ...")
if self.mainConf.backupPath is None or self.mainConf.backupPath == "":
self.theParent.makeAlert((
@@ -847,7 +847,7 @@ class NWProject():
)
return False
- self.theParent.statusBar.setStatus("Project backed up to '%s.zip'" % baseName)
+ self.theParent.setStatus("Project backed up to '%s.zip'" % baseName)
return True
@@ -1139,16 +1139,16 @@ class NWProject():
# Technically a bug since treeOrder is built from the
# same data as projTree
continue
- elif tItem.parHandle is None:
+ elif tItem.itemParent is None:
# Item is a root, or already been identified as an
# orphaned item
sentItems.append(tHandle)
yield tItem
- elif tItem.parHandle in sentItems:
+ elif tItem.itemParent in sentItems:
# Item's parent has been sent, so all is fine
sentItems.append(tHandle)
yield tItem
- elif tItem.parHandle in iterItems:
+ elif tItem.itemParent in iterItems:
# Item's parent exists, but hasn't been sent yet, so add
# it again to the end
logger.warning("Item %s found before its parent" % tHandle)
diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py
index ff338f62..e3093070 100644
--- a/nw/core/spellcheck.py
+++ b/nw/core/spellcheck.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
-"""novelWriter Spell Check Wrapper
+"""novelWriter Spell Check Classes
- novelWriter – Spell Check Wrapper
+ novelWriter – Spell Check Classes
===================================
Wrapper class for spell checking
diff --git a/nw/core/tohtml.py b/nw/core/tohtml.py
index 770505db..df6c4a09 100644
--- a/nw/core/tohtml.py
+++ b/nw/core/tohtml.py
@@ -41,6 +41,7 @@ class ToHtml(Tokenizer):
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
+
self.genMode = self.M_EXPORT
self.cssStyles = True
diff --git a/nw/core/tools.py b/nw/core/tools.py
index 59c141d9..ef9fbc6b 100644
--- a/nw/core/tools.py
+++ b/nw/core/tools.py
@@ -1,14 +1,15 @@
# -*- coding: utf-8 -*-
-"""novelWriter Word Counter
+"""novelWriter Various Tools
- novelWriter – Word Counter
-============================
- Simple word counter
+ novelWriter – Various Tools
+=============================
+ Various core tool functions
File History:
- Created: 2019-04-22 [0.0.1] countWords
- Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN
- Merged: 2020-05-08 [0.4.5] All of the above into this file
+ Created: 2019-04-22 [0.0.1] countWords
+ Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN
+ Merged: 2020-05-08 [0.4.5] All of the above into this file
+ Created: 2020-07-05 [0.10.0] numberToRoman
This file is a part of novelWriter
Copyright 2018–2020, Veronica Berglyd Olsen
@@ -136,7 +137,6 @@ def numberToWord(numVal, theLanguage):
def _numberToWordEN(numVal):
"""Convert numbers to English words.
"""
- numWord = ""
oneWord = ""
tenWord = ""
hunWord = ""
@@ -145,8 +145,8 @@ def _numberToWordEN(numVal):
return "Zero"
oneVal = numVal % 10
- tenVal = (numVal-oneVal) % 100
- hunVal = (numVal-tenVal-oneVal) % 1000
+ tenVal = (numVal - oneVal) % 100
+ hunVal = (numVal - tenVal - oneVal) % 1000
theHundreds = {
100: "One Hundred", 200: "Two Hundred", 300: "Three Hundred",
@@ -167,18 +167,18 @@ def _numberToWordEN(numVal):
}
hunWord = theHundreds.get(hunVal, "")
- tenWord = theTens.get(tenVal, "")
if tenVal == 10:
oneWord = theTeens.get(oneVal, "")
- numWord = ("%s %s" % (hunWord, oneWord)).strip()
+ return f"{hunWord} {oneWord}".strip()
else:
oneWord = theOnes.get(oneVal, "")
if tenVal == 0:
- numWord = ("%s %s" % (hunWord, oneWord)).strip()
+ return f"{hunWord} {oneWord}".strip()
else:
+ tenWord = theTens.get(tenVal, "")
if oneVal == 0:
- numWord = ("%s %s" % (hunWord, tenWord)).strip()
+ return f"{hunWord} {tenWord}".strip()
else:
- numWord = ("%s %s-%s" % (hunWord, tenWord, oneWord)).strip()
+ return f"{hunWord} {tenWord}-{oneWord}".strip()
- return numWord
+ return ""
diff --git a/nw/core/tree.py b/nw/core/tree.py
index c51ae16c..b43230a9 100644
--- a/nw/core/tree.py
+++ b/nw/core/tree.py
@@ -134,7 +134,7 @@ class NWTree():
for xItem in xContent:
nwItem = NWItem(self.theProject)
if nwItem.unpackXML(xItem):
- self.append(nwItem.itemHandle, nwItem.parHandle, nwItem)
+ self.append(nwItem.itemHandle, nwItem.itemParent, nwItem)
nwItem.saveInitialCount()
return True
@@ -261,10 +261,10 @@ class NWTree():
tItem = self.__getitem__(tHandle)
if tItem is not None:
for i in range(nwConst.maxDepth + 1):
- if tItem.parHandle is None:
+ if tItem.itemParent is None:
return tItem
else:
- tHandle = tItem.parHandle
+ tHandle = tItem.itemParent
tItem = self.__getitem__(tHandle)
return None
@@ -279,10 +279,10 @@ class NWTree():
if tItem is not None:
tTree.append(tHandle)
for i in range(nwConst.maxDepth + 1):
- if tItem.parHandle is None:
+ if tItem.itemParent is None:
return tTree
else:
- tHandle = tItem.parHandle
+ tHandle = tItem.itemParent
tItem = self.__getitem__(tHandle)
if tItem is None:
return tTree
diff --git a/nw/error.py b/nw/error.py
index a8f3c149..988b49e6 100644
--- a/nw/error.py
+++ b/nw/error.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-"""novelWriter Init
+"""novelWriter Exception Handling
novelWriter – Exception Handling
==================================
diff --git a/nw/gui/about.py b/nw/gui/about.py
index c6957bae..248cf7d1 100644
--- a/nw/gui/about.py
+++ b/nw/gui/about.py
@@ -222,7 +222,6 @@ class GuiAbout(QDialog):
hColB = self.theParent.theTheme.colHead[2],
)
self.pageAbout.document().setDefaultStyleSheet(styleSheet)
- # self.pageCredit.document().setDefaultStyleSheet(styleSheet)
self.pageLicense.document().setDefaultStyleSheet(styleSheet)
return
diff --git a/nw/gui/build.py b/nw/gui/build.py
index 1a69b62e..45a1a073 100644
--- a/nw/gui/build.py
+++ b/nw/gui/build.py
@@ -684,8 +684,8 @@ class GuiBuildNovel(QDialog):
isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT
isNone |= theItem.itemClass == nwItemClass.NO_CLASS
isNone |= theItem.itemClass == nwItemClass.TRASH
- isNone |= theItem.parHandle == self.theProject.projTree.trashRoot()
- isNone |= theItem.parHandle is None
+ isNone |= theItem.itemParent == self.theProject.projTree.trashRoot()
+ isNone |= theItem.itemParent is None
isNote = theItem.itemLayout == nwItemLayout.NOTE
isNovel = not isNone and not isNote
diff --git a/nw/gui/custom.py b/nw/gui/custom.py
index 2955c447..a6260f33 100644
--- a/nw/gui/custom.py
+++ b/nw/gui/custom.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
-"""novelWriter Addition QConfigLayout
+"""novelWriter Custom Widgets and Layouts
- novelWriter – Addition QConfigLayout
-======================================
- A custom Qt grid layout for config forms similar to QFormLayout
+ novelWriter – Custom Widgets and Layouts
+==========================================
+ Various custom widget and layout classes
File History:
Created: 2020-05-03 [0.4.5] QConfigLayout
diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py
index d5e3b045..6d431319 100644
--- a/nw/gui/doceditor.py
+++ b/nw/gui/doceditor.py
@@ -807,16 +807,16 @@ class GuiDocEditor(QTextEdit):
return self.docSearch.cycleFocus(toNext)
return True
- def mouseReleaseEvent(self, mEvent):
+ def mouseReleaseEvent(self, theEvent):
"""If the mouse button is released and the control key is
pressed, check if we're clicking on a tag, and trigger the
follow tag function.
"""
if qApp.keyboardModifiers() == Qt.ControlModifier:
- theCursor = self.cursorForPosition(mEvent.pos())
+ theCursor = self.cursorForPosition(theEvent.pos())
self._followTag(theCursor)
- QTextEdit.mouseReleaseEvent(self, mEvent)
+ QTextEdit.mouseReleaseEvent(self, theEvent)
self.docFooter.updateLineCount()
return
@@ -1203,20 +1203,19 @@ class GuiDocEditor(QTextEdit):
"""Check if document size crosses the big document limit set in
config. If so, we will set the big document flag to True.
"""
- newState = theSize > self.mainConf.bigDocLimit*1000
+ bigLim = self.mainConf.bigDocLimit*1000
+ newState = theSize > bigLim
if newState != self.bigDoc:
if newState:
logger.info(
- "The document size is {:n} > {:n}, big doc mode has been enabled".format(
- theSize, self.mainConf.bigDocLimit*1000
- )
+ f"The document size is {theSize:n} > {bigLim:n}, "
+ f"big doc mode has been enabled"
)
else:
logger.info(
- "The document size is {:n} <= {:n}, big doc mode has been disabled".format(
- theSize, self.mainConf.bigDocLimit*1000
- )
+ f"The document size is {theSize:n} <= {bigLim:n}, "
+ f"big doc mode has been disabled"
)
self.bigDoc = newState
diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py
index ba39b6e6..cc11ef86 100644
--- a/nw/gui/dochighlight.py
+++ b/nw/gui/dochighlight.py
@@ -149,18 +149,22 @@ class GuiDocHighlighter(QSyntaxHighlighter):
# Quoted Strings
if self.mainConf.highlightQuotes:
+ fmtDO = self.mainConf.fmtDoubleQuotes[0]
+ fmtDC = self.mainConf.fmtDoubleQuotes[1]
+ fmtSO = self.mainConf.fmtSingleQuotes[0]
+ fmtSC = self.mainConf.fmtSingleQuotes[1]
self.hRules.append((
- "\\B{:s}(.*?){:s}\\B".format('"', '"'), {
+ "\\B\"(.*?)\"\\B", {
0 : self.hStyles["dialogue1"],
}
))
self.hRules.append((
- "\\B{:s}(.*?){:s}\\B".format(*self.mainConf.fmtDoubleQuotes), {
+ f"\\B{fmtDO:s}(.*?){fmtDC:s}\\B", {
0 : self.hStyles["dialogue2"],
}
))
self.hRules.append((
- "\\B{:s}(.*?){:s}\\B".format(*self.mainConf.fmtSingleQuotes), {
+ f"\\B{fmtSO:s}(.*?){fmtSC:s}\\B", {
0 : self.hStyles["dialogue3"],
}
))
diff --git a/nw/gui/docmerge.py b/nw/gui/docmerge.py
index 2418a161..d7d128fe 100644
--- a/nw/gui/docmerge.py
+++ b/nw/gui/docmerge.py
@@ -127,7 +127,7 @@ class GuiDocMerge(QDialog):
), nwAlert.ERROR)
return
- nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.parHandle)
+ nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent)
newItem = self.theProject.projTree[nHandle]
newItem.setStatus(srcItem.itemStatus)
diff --git a/nw/gui/docsplit.py b/nw/gui/docsplit.py
index 3f497c18..1a5e9491 100644
--- a/nw/gui/docsplit.py
+++ b/nw/gui/docsplit.py
@@ -154,7 +154,7 @@ class GuiDocSplit(QDialog):
return
# Check that another folder can be created
- parTree = self.theProject.projTree.getItemPath(srcItem.parHandle)
+ parTree = self.theProject.projTree.getItemPath(srcItem.itemParent)
if len(parTree) >= nwConst.maxDepth - 1:
self.theParent.makeAlert((
"Cannot add new folder for the document split. "
@@ -176,7 +176,7 @@ class GuiDocSplit(QDialog):
# Create the folder
fHandle = self.theProject.newFolder(
- srcItem.itemName, srcItem.itemClass, srcItem.parHandle
+ srcItem.itemName, srcItem.itemClass, srcItem.itemParent
)
self.theParent.treeView.revealNewTreeItem(fHandle)
logger.verbose("Creating folder %s" % fHandle)
diff --git a/nw/gui/outline.py b/nw/gui/outline.py
index d3dc4815..4238cc44 100644
--- a/nw/gui/outline.py
+++ b/nw/gui/outline.py
@@ -439,6 +439,10 @@ class GuiOutline(QTreeWidget):
newItem = QTreeWidgetItem()
hIcon = "doc_%s" % tLevel.lower()
+ cC = int(novIdx["cCount"])
+ wC = int(novIdx["wCount"])
+ pC = int(novIdx["pCount"])
+
newItem.setText(self.colIndex[nwOutline.TITLE], novIdx["title"])
newItem.setData(self.colIndex[nwOutline.TITLE], Qt.UserRole, tHandle)
newItem.setIcon(self.colIndex[nwOutline.TITLE], self.theTheme.getIcon(hIcon))
@@ -448,9 +452,9 @@ class GuiOutline(QTreeWidget):
newItem.setText(self.colIndex[nwOutline.LINE], sTitle[1:].lstrip("0"))
newItem.setData(self.colIndex[nwOutline.LINE], Qt.UserRole, sTitle)
newItem.setText(self.colIndex[nwOutline.SYNOP], novIdx["synopsis"])
- newItem.setText(self.colIndex[nwOutline.CCOUNT], "{:n}".format(novIdx["cCount"]))
- newItem.setText(self.colIndex[nwOutline.WCOUNT], "{:n}".format(novIdx["wCount"]))
- newItem.setText(self.colIndex[nwOutline.PCOUNT], "{:n}".format(novIdx["pCount"]))
+ newItem.setText(self.colIndex[nwOutline.CCOUNT], f"{cC:n}")
+ newItem.setText(self.colIndex[nwOutline.WCOUNT], f"{wC:n}")
+ newItem.setText(self.colIndex[nwOutline.PCOUNT], f"{pC:n}")
newItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight)
diff --git a/nw/gui/outlinedetails.py b/nw/gui/outlinedetails.py
index 49998dd9..18f2b90f 100644
--- a/nw/gui/outlinedetails.py
+++ b/nw/gui/outlinedetails.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
-"""novelWriter GUI Project Outline
+"""novelWriter GUI Project Outline Details
- novelWriter – GUI Project Outline
-===================================
- Class holding the project outline view
+ novelWriter – GUI Project Outline Details
+===========================================
+ Class holding the project outline details view
File History:
Created: 2020-06-02 [0.7.0]
@@ -266,9 +266,13 @@ class GuiOutlineDetails(QScrollArea):
self.fileValue.setText(nwItem.itemName)
self.itemValue.setText(nwItem.itemStatus)
- self.cCValue.setText("{:n}".format(checkInt(novIdx["cCount"], 0)))
- self.wCValue.setText("{:n}".format(checkInt(novIdx["wCount"], 0)))
- self.pCValue.setText("{:n}".format(checkInt(novIdx["pCount"], 0)))
+ cC = checkInt(novIdx["cCount"], 0)
+ wC = checkInt(novIdx["wCount"], 0)
+ pC = checkInt(novIdx["pCount"], 0)
+
+ self.cCValue.setText(f"{cC:n}")
+ self.wCValue.setText(f"{wC:n}")
+ self.pCValue.setText(f"{pC:n}")
self.synopValue.setText(novIdx["synopsis"])
diff --git a/nw/gui/preferences.py b/nw/gui/preferences.py
index 2a1cf653..7b8e0905 100644
--- a/nw/gui/preferences.py
+++ b/nw/gui/preferences.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
-"""novelWriter GUI Config Editor
+"""novelWriter GUI Preferences
- novelWriter – GUI Config Editor
-=================================
- Class holding the config dialog
+ novelWriter – GUI Preferences
+===============================
+ Class holding the preferences dialog
File History:
Created: 2019-06-10 [0.1.5]
diff --git a/nw/gui/projload.py b/nw/gui/projload.py
index 8f18f7bb..29c9c9ab 100644
--- a/nw/gui/projload.py
+++ b/nw/gui/projload.py
@@ -3,7 +3,7 @@
novelWriter – GUI Open Project
================================
- New and open project dialog
+ The open project dialog
File History:
Created: 2020-02-26 [0.4.5]
diff --git a/nw/gui/projsettings.py b/nw/gui/projsettings.py
index 30d9ad54..d51d8006 100644
--- a/nw/gui/projsettings.py
+++ b/nw/gui/projsettings.py
@@ -270,11 +270,12 @@ class GuiProjectEditMeta(QWidget):
self.revLabel = QLabel("Revision count:")
self.revLabel.setIndent(xInd)
- self.revValue = QLabel("{:n}".format(self.theProject.saveCount))
+ self.revValue = QLabel(f"{self.theProject.saveCount:n}")
+ editHours = self.theProject.editTime/3600
self.editLabel = QLabel("Edit time:")
self.editLabel.setIndent(xInd)
- self.editValue = QLabel("{:.2f} hours".format(self.theProject.editTime/3600))
+ self.editValue = QLabel(f"{editHours:.2f} hours")
self.statsLabel = QLabel("Project Stats")
@@ -294,7 +295,7 @@ class GuiProjectEditMeta(QWidget):
self.wordsLabel = QLabel("Word count:")
self.wordsLabel.setIndent(xInd)
- self.wordsValue = QLabel("{:n}".format(self.theProject.currWCount))
+ self.wordsValue = QLabel(f"{self.theProject.currWCount:n}")
self.mainForm.addWidget(self.headLabel, 0, 0, 1, 2, Qt.AlignTop)
self.mainForm.addWidget(self.nameLabel, 1, 0, 1, 1, Qt.AlignTop)
diff --git a/nw/gui/projtree.py b/nw/gui/projtree.py
index 9f4bb5e6..77fdf610 100644
--- a/nw/gui/projtree.py
+++ b/nw/gui/projtree.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
-"""novelWriter GUI Document Tree
+"""novelWriter GUI Project Tree
- novelWriter – GUI Document Tree
-=================================
- Class holding the left side document tree view
+ novelWriter – GUI project Tree
+================================
+ Class holding the left side project tree view
File History:
Created: 2018-09-29 [0.0.1] GuiProjectTree
@@ -219,7 +219,7 @@ class GuiProjectTree(QTreeWidget):
pItem = self.theProject.projTree[pHandle]
if pItem.itemType == nwItemType.FILE:
nHandle = pHandle
- pHandle = pItem.parHandle
+ pHandle = pItem.itemParent
# If we again have no home, give up
if pHandle is None:
@@ -270,7 +270,7 @@ class GuiProjectTree(QTreeWidget):
"""
nwItem = self.theProject.projTree[tHandle]
trItem = self._addTreeItem(nwItem, nHandle)
- pHandle = nwItem.parHandle
+ pHandle = nwItem.itemParent
if pHandle is not None and pHandle in self.theMap:
self.theMap[pHandle].setExpanded(True)
self.clearSelection()
@@ -430,7 +430,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("Could not delete item")
return False
- pHandle = nwItemS.parHandle
+ pHandle = nwItemS.itemParent
if self.theProject.projTree.isTrashRoot(pHandle):
# If the file is in the trash folder already, as the
# user if they want to permanently delete the file.
@@ -815,7 +815,7 @@ class GuiProjectTree(QTreeWidget):
project tree.
"""
tHandle = nwItem.itemHandle
- pHandle = nwItem.parHandle
+ pHandle = nwItem.itemParent
tClass = nwItem.itemClass
newItem = QTreeWidgetItem([""]*4)
@@ -1022,11 +1022,11 @@ class GuiProjectTreeMenu(QMenu):
trashHandle = self.theTree.theProject.projTree.trashRoot()
- inTrash = theItem.parHandle == trashHandle and trashHandle is not None
+ inTrash = theItem.itemParent == trashHandle and trashHandle is not None
isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
isFile = theItem.itemType == nwItemType.FILE
isArch = theRoot.itemClass == nwItemClass.ARCHIVE
- isOrph = isFile and theItem.parHandle is None
+ isOrph = isFile and theItem.itemParent is None
showOpen = isFile
showView = isFile
diff --git a/nw/gui/statusbar.py b/nw/gui/statusbar.py
index c43c3f6f..0b95ef16 100644
--- a/nw/gui/statusbar.py
+++ b/nw/gui/statusbar.py
@@ -195,28 +195,21 @@ class GuiMainStatus(QStatusBar):
self.statsText.setToolTip(
"Project word count (session change)"
)
- self.statsText.setText((
- "Words: {pWC:n} ({sWC:+n})"
- ).format(
- pWC = self.projWords,
- sWC = self.sessWords,
- ))
+ self.statsText.setText(
+ f"Words: {self.projWords:n} ({self.sessWords:+n})"
+ )
return
def _updateTime(self):
"""Update the session clock.
"""
if self.refTime is None:
- theTime = "00:00:00"
+ self.timeText.setText("00:00:00")
else:
- # This is much faster than using datetime format
tS = int(time() - self.refTime)
- tM = int(tS/60)
- tH = int(tM/60)
- tM = tM - tH*60
- tS = tS - tM*60 - tH*3600
- theTime = "%02d:%02d:%02d" % (tH, tM, tS)
- self.timeText.setText(theTime)
+ self.timeText.setText(
+ f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}"
+ )
return
# END Class GuiMainStatus
@@ -237,7 +230,7 @@ class StatusLED(QAbstractButton):
return
##
- # Getters and Setters
+ # Setters
##
def setState(self, theState):
diff --git a/nw/gui/theme.py b/nw/gui/theme.py
index 5683d1a3..9d907a67 100644
--- a/nw/gui/theme.py
+++ b/nw/gui/theme.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
-"""novelWriter Theme Class
+"""novelWriter Theme and Icons Classes
- novelWriter – Theme Class
-===========================
- This class reads and store the main theme
+ novelWriter – Theme and Icons Classs
+======================================
+ This class reads and stores the themes and the icons
File History:
Created: 2019-05-18 [0.1.3] GuiTheme
diff --git a/nw/guimain.py b/nw/guimain.py
index 18801727..6742a0d9 100644
--- a/nw/guimain.py
+++ b/nw/guimain.py
@@ -62,7 +62,9 @@ class GuiMain(QMainWindow):
self.mainConf = nw.CONFIG
self.threadPool = QThreadPool()
- # Some runtime info useful for debugging
+ # System Info
+ # ===========
+
logger.info("OS: %s" % self.mainConf.osType)
logger.info("Kernel: %s" % self.mainConf.kernelVer)
logger.info("Host: %s" % self.mainConf.hostName)
@@ -76,6 +78,9 @@ class GuiMain(QMainWindow):
self.mainConf.verPyString, self.mainConf.verPyHexVal)
)
+ # Core Classes
+ # ============
+
# Core Classes and settings
self.theTheme = GuiTheme(self)
self.theProject = NWProject(self)
@@ -89,7 +94,7 @@ class GuiMain(QMainWindow):
self.setWindowIcon(QIcon(self.mainConf.appIcon))
# Build the GUI
- ################
+ # =============
# Main GUI Elements
self.statusBar = GuiMainStatus(self)
@@ -106,7 +111,7 @@ class GuiMain(QMainWindow):
self.statusIcons = []
self.importIcons = []
- # Assemble Main Window
+ # Project Tree View
self.treePane = QWidget()
self.treeBox = QVBoxLayout()
self.treeBox.setContentsMargins(0, 0, 0, 0)
@@ -114,20 +119,24 @@ class GuiMain(QMainWindow):
self.treeBox.addWidget(self.treeMeta)
self.treePane.setLayout(self.treeBox)
+ # Splitter : Document Viewer / Document Meta
self.splitView = QSplitter(Qt.Vertical)
self.splitView.addWidget(self.docViewer)
self.splitView.addWidget(self.viewMeta)
self.splitView.setSizes(self.mainConf.getViewPanePos())
+ # Splitter : Document Editor / Document Viewer
self.splitDocs = QSplitter(Qt.Horizontal)
self.splitDocs.addWidget(self.docEditor)
self.splitDocs.addWidget(self.splitView)
+ # Splitter : Project Outlie / Outline Details
self.splitOutline = QSplitter(Qt.Vertical)
self.splitOutline.addWidget(self.projView)
self.splitOutline.addWidget(self.projMeta)
self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
+ # Main Tabs : Edirot / Outline
self.tabWidget = QTabWidget()
self.tabWidget.setTabPosition(QTabWidget.East)
self.tabWidget.setStyleSheet("QTabWidget::pane {border: 0;}")
@@ -135,6 +144,7 @@ class GuiMain(QMainWindow):
self.tabWidget.addTab(self.splitOutline, "Outline")
self.tabWidget.currentChanged.connect(self._mainTabChanged)
+ # Splitter : Project Tree / Main Tabs
xCM = self.mainConf.pxInt(4)
self.splitMain = QSplitter(Qt.Horizontal)
self.splitMain.setContentsMargins(xCM, xCM, xCM, xCM)
@@ -142,6 +152,7 @@ class GuiMain(QMainWindow):
self.splitMain.addWidget(self.tabWidget)
self.splitMain.setSizes(self.mainConf.getMainPanePos())
+ # Indices of All Splitter Widgets
self.idxTree = self.splitMain.indexOf(self.treePane)
self.idxMain = self.splitMain.indexOf(self.tabWidget)
self.idxEditor = self.splitDocs.indexOf(self.docEditor)
@@ -151,6 +162,7 @@ class GuiMain(QMainWindow):
self.idxTabEdit = self.tabWidget.indexOf(self.splitDocs)
self.idxTabProj = self.tabWidget.indexOf(self.splitOutline)
+ # Splitter Behaviour
self.splitMain.setCollapsible(self.idxTree, False)
self.splitMain.setCollapsible(self.idxMain, False)
self.splitDocs.setCollapsible(self.idxEditor, False)
@@ -158,10 +170,11 @@ class GuiMain(QMainWindow):
self.splitView.setCollapsible(self.idxViewDoc, False)
self.splitView.setCollapsible(self.idxViewMeta, False)
+ # Editor / Viewer Default State
self.splitView.setVisible(False)
self.docEditor.closeSearch()
- # Build the Tree View
+ # Initialise the Project Tree
self.treeView.itemSelectionChanged.connect(self._treeSingleClick)
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.rebuildTree()
@@ -172,13 +185,13 @@ class GuiMain(QMainWindow):
self.setStatusBar(self.statusBar)
# Finalise Initialisation
- ##########################
+ # =======================
- # Set Up Autosaving Project Timer
+ # Set Up Auto-Save Project Timer
self.asProjTimer = QTimer()
self.asProjTimer.timeout.connect(self._autoSaveProject)
- # Set Up Autosaving Document Timer
+ # Set Up Auto-Save Document Timer
self.asDocTimer = QTimer()
self.asDocTimer.timeout.connect(self._autoSaveDocument)
@@ -203,11 +216,13 @@ class GuiMain(QMainWindow):
# Check that config loaded fine
self.reportConfErr()
+ # Initialise Main GUI
self.initMain()
self.asProjTimer.start()
self.asDocTimer.start()
self.statusBar.clearStatus()
+ # Handle Windows Mode
self.showNormal()
if self.mainConf.isFullScreen:
self.toggleFullScreenMode()
@@ -224,7 +239,7 @@ class GuiMain(QMainWindow):
self.showProjectLoadDialog()
logger.debug("novelWriter is ready ...")
- self.statusBar.setStatus("novelWriter is ready ...")
+ self.setStatus("novelWriter is ready ...")
return
@@ -249,8 +264,7 @@ class GuiMain(QMainWindow):
##
def newProject(self, projData=None):
- """Create new project with a few default files and folders.
- The variable forceNew is used for testing.
+ """Create new project via the new project wizard.
"""
if self.hasProject:
self.makeAlert(
@@ -293,7 +307,7 @@ class GuiMain(QMainWindow):
def closeProject(self, isYes=False):
"""Closes the project if one is open. isYes is passed on from
the close application event so the user doesn't get prompted
- twice.
+ twice to confirm.
"""
if not self.hasProject:
# There is no project loaded, everything OK
@@ -302,7 +316,7 @@ class GuiMain(QMainWindow):
if not isYes:
msgBox = QMessageBox()
msgRes = msgBox.question(
- self, "Close Project", "Save changes and close current project?"
+ self, "Close Project", "Save changes and close the current project?"
)
if msgRes != QMessageBox.Yes:
return False
@@ -318,7 +332,7 @@ class GuiMain(QMainWindow):
if self.mainConf.askBeforeBackup:
msgBox = QMessageBox()
msgRes = msgBox.question(
- self, "Backup Project", "Backup current project?"
+ self, "Backup Project", "Backup the current project?"
)
if msgRes != QMessageBox.Yes:
doBackup = False
@@ -433,6 +447,7 @@ class GuiMain(QMainWindow):
if self.theProject.projPath is None:
projPath = self.selectProjectPath()
self.theProject.setProjectPath(projPath)
+
if self.theProject.projPath is None:
return False
@@ -454,6 +469,7 @@ class GuiMain(QMainWindow):
if self.docEditor.docChanged:
self.saveDocument()
self.docEditor.clearEditor()
+
return True
def openDocument(self, tHandle, tLine=None, changeFocus=True, doScroll=False):
@@ -469,6 +485,7 @@ class GuiMain(QMainWindow):
self.treeView.setSelectedHandle(tHandle, doScroll=doScroll)
else:
return False
+
return True
def openNextDocument(self, tHandle, wrapAround=False):
@@ -546,6 +563,7 @@ class GuiMain(QMainWindow):
vPos[1] = bPos[1] - vPos[0]
self.splitDocs.setSizes(vPos)
self.viewMeta.setVisible(self.mainConf.showRefPanel)
+
self.docViewer.navigateTo(tAnchor)
return True
@@ -697,9 +715,9 @@ class GuiMain(QMainWindow):
for nDone, tItem in enumerate(self.theProject.projTree):
if tItem is not None:
- self.statusBar.setStatus("Indexing: '%s'" % tItem.itemName)
+ self.setStatus("Indexing: '%s'" % tItem.itemName)
else:
- self.statusBar.setStatus("Indexing: Unknown item")
+ self.setStatus("Indexing: Unknown item")
if tItem is not None and tItem.itemType == nwItemType.FILE:
logger.verbose("Scanning: %s" % tItem.itemName)
@@ -717,7 +735,7 @@ class GuiMain(QMainWindow):
self.treeView.projectWordCount()
tEnd = time()
- self.statusBar.setStatus("Indexing completed in %.1f ms" % ((tEnd - tStart)*1000.0))
+ self.setStatus("Indexing completed in %.1f ms" % ((tEnd - tStart)*1000.0))
self.docEditor.updateTagHighLighting()
qApp.restoreOverrideCursor()
@@ -754,7 +772,8 @@ class GuiMain(QMainWindow):
def showProjectLoadDialog(self):
"""Opens the projects dialog for selecting either existing
projects from a cache of recently opened projects, or provide a
- browse button for projects not yet cached.
+ browse button for projects not yet cached. Selecting to create a
+ new project is forwarded to the new project wizard.
"""
dlgProj = GuiProjectLoad(self)
dlgProj.exec_()
@@ -767,7 +786,7 @@ class GuiMain(QMainWindow):
return True
def showNewProjectDialog(self):
- """Open the wizard and assemble the project options dict.
+ """Open the wizard and assemble a project options dict.
"""
newProj = GuiProjectWizard(self)
newProj.exec_()
@@ -865,8 +884,7 @@ class GuiMain(QMainWindow):
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.
+ can be either a string or an array of strings.
"""
if isinstance(theMessage, list):
popMsg = "
".join(theMessage)
@@ -955,7 +973,7 @@ class GuiMain(QMainWindow):
return True
def setFocus(self, paneNo):
- """Switch focus to one of the three main gUi panes.
+ """Switch focus to one of the three main GUI panes.
"""
if paneNo == 1:
self.treeView.setFocus()
@@ -1236,9 +1254,9 @@ class GuiMain(QMainWindow):
return
def _treeKeyPressReturn(self):
- """The user pressed return an item in the tree. If it is a file,
- we open it. Otherwise, we do nothing. Pressing return does not
- change focus to the editor as double click does.
+ """The user pressed return on an item in the tree. If it is a
+ file, we open it. Otherwise, we do nothing. Pressing return does
+ not change focus to the editor as double click does.
"""
tHandle = self.treeView.getSelectedHandle()
logger.verbose("User pressed return on tree item with handle %s" % tHandle)
@@ -1257,7 +1275,6 @@ class GuiMain(QMainWindow):
"""
if self.docEditor.docSearch.isVisible():
self.docEditor.closeSearch()
- return
elif self.isFocusMode:
self.toggleFocusMode()
return
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000..9787c3bd
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta"
diff --git a/setup.cfg b/setup.cfg
index a791e31e..ad1c6ddb 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,11 +1,54 @@
[metadata]
-license_files = LICENSE.md
+name = novelWriter
version = attr: nw.__version__
+author = Veronica Berglyd Olsen
+author_email = code@vkbo.net
+description = A markdown-like document editor for writing novels
+url = https://novelwriter.io
+long_description = file: README.md
+long_description_content_type = text/markdown
+license_file = LICENSE.md
+license = GNU General Public License v3
+classifiers =
+ Programming Language :: Python :: 3 :: Only
+ Programming Language :: Python :: 3.6
+ Programming Language :: Python :: 3.7
+ Programming Language :: Python :: 3.8
+ Programming Language :: Python :: 3.9
+ Programming Language :: Python :: Implementation :: CPython
+ License :: OSI Approved :: GNU General Public License v3 (GPLv3)
+ Development Status :: 4 - Beta
+ Operating System :: OS Independent
+ Intended Audience :: End Users/Desktop
+ Natural Language :: English
+ Topic :: Text Editors
+python_requires = >=3.6
+install_requires =
+ pyqt5>=5.2.1
+ lxml>=4.2.0
+ pyenchant>=3.0.0
+project_urls =
+ Bug Tracker = https://github.com/vkbo/novelWriter/issues
+ Documentation = https://github.com/vkbo/novelWriter/issues
+ Source Code = https://github.com/vkbo/novelWriter
+
+[options]
+include_package_data = True
+packages = find:
+
+[options.packages.find]
+exclude = docs, tests, sample
+
+[options.entry_points]
+console_script =
+ novelWriter-cli = nw:main
+gui_scripts =
+ novelWriter = nw:main
[bdist_wheel]
universal = 0
[flake8]
-ignore = E203,E221,E226,E241,E251,E261,E266,E302,E305
+ignore = E203,E221,E226,E228,E241,E251,E261,E266,E302,E305
max-line-length = 99
exclude = docs/*
diff --git a/setup.py b/setup.py
index 6b10a84e..c3c5710b 100755
--- a/setup.py
+++ b/setup.py
@@ -1,30 +1,44 @@
#!/usr/bin/env python3
+"""
+The main setup script for novelWeiter.
+
+It runs the standard setuptool.setup() with all options taken from the
+setup.cfg file.
+
+In addtion, a few speicalised commands are available:
+
+ * sample: Will build a sample.zip file, which is the way the sample project is
+ included into distributable packages.
+ * qthelp: Will build a QtAssistant readable version of the novelWriter
+ documentation. This should also be a part of distributed packages. It allows
+ for reading the help offline. Otherwise, the F1 button redirects to the
+ online documentation only.
+ * launcher: Will attempt to install novelWriter icons, mime type and create a
+ launcher for the application.
+
+"""
import os
import sys
+import shutil
import subprocess
import setuptools
-##
-# Build the Package
-##
+# =============================================================================================== #
+# Qt Assistant Documentation Builder
+# =============================================================================================== #
-buildDocs = False
-buildSample = False
+def buildQtDocs():
+ """This function will build the documentation as a Qt help file. The
+ file is then copied into the nw/assets/help directory and can be
+ included in builds.
-if "qthelp" in sys.argv:
- buildDocs = True
- sys.argv.remove("qthelp")
-
-if "sample" in sys.argv:
- buildSample = True
- sys.argv.remove("sample")
-
-##
-# Qt Assistant Documentation
-##
-
-if buildDocs:
+ Depends on packages:
+ * pip install sphinx
+ * pip install sphinx-rtd-theme
+ * pip install sphinxcontrib-qthelp
+ It also requires the qhelpgenerator to be available on the system.
+ """
buildDir = os.path.join("docs", "build", "qthelp")
helpDir = os.path.join("nw", "assets", "help")
@@ -41,14 +55,14 @@ if buildDocs:
try:
subprocess.call(["make", "-C", "docs", "qthelp"])
except Exception as e:
- print("Failed with error:")
+ print("QtHelp Build Error:")
print(str(e))
buildFail = True
try:
subprocess.call(["qhelpgenerator", os.path.join(buildDir, inFile)])
except Exception as e:
- print("Failed with error:")
+ print("QtHelp Build Error:")
print(str(e))
buildFail = True
@@ -56,7 +70,7 @@ if buildDocs:
try:
os.mkdir(helpDir)
except Exception as e:
- print("Failed with error:")
+ print("QtHelp Build Error:")
print(str(e))
buildFail = True
@@ -68,7 +82,7 @@ if buildDocs:
os.rename(os.path.join(buildDir, outFile), os.path.join(helpDir, outFile))
os.rename(os.path.join(buildDir, datFile), os.path.join(helpDir, datFile))
except Exception as e:
- print("Failed with error:")
+ print("QtHelp Build Error:")
print(str(e))
buildFail = True
@@ -80,11 +94,20 @@ if buildDocs:
print("Documentation build: OK")
print("")
-##
-# Sample Project ZIP file
-##
+ return
-if buildSample:
+# =============================================================================================== #
+# Sample Project ZIP File Builder
+# =============================================================================================== #
+
+def buildSampleZip():
+ """Bundle the sample project into a single zip file to be saved into
+ the nw/assets folder for further bundling into builds.
+ """
+ print("")
+ print("Building Sample ZIP File")
+ print("========================")
+ print("")
srcSample = "sample"
dstSample = os.path.join("nw", "assets", "sample.zip")
@@ -96,8 +119,10 @@ if buildSample:
from zipfile import ZipFile
with ZipFile(dstSample, "w") as zipObj:
+ print("Compressing: nwProject.nwx")
zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx")
for docFile in os.listdir(os.path.join(srcSample, "content")):
+ print("Compressing: content/%s" % docFile)
srcDoc = os.path.join(srcSample, "content", docFile)
zipObj.write(srcDoc, "content/"+docFile)
@@ -105,58 +130,197 @@ if buildSample:
print("Error: Could not find sample project source directory.")
sys.exit(1)
-if len(sys.argv) == 1:
- # Nothing more to do
- sys.exit(0)
+ print("")
+ print("Built file: %s" % dstSample)
+ print("")
-##
-# Build the Package
-##
+ return
-# Read content from files
-with open("README.md", "r") as inFile:
- longDescription = inFile.read()
+# =============================================================================================== #
+# Create Launcher
+# =============================================================================================== #
-setuptools.setup(
- name = "novelWriter",
- # version = __version__, # Set in setup.cfg
- author = "Veronica Berglyd Olsen",
- author_email = "code@vkbo.net",
- description = "A markdown-like document editor for writing novels",
- long_description = longDescription,
- long_description_content_type = "text/markdown",
- license = "GNU General Public License v3",
- url = "https://novelwriter.io",
- entry_points = {
- "console_scripts" : ["novelWriter-cli=nw:main"],
- "gui_scripts" : ["novelWriter=nw:main"],
- },
- packages = setuptools.find_packages(exclude=["docs", "tests", "sample"]),
- include_package_data = True,
- package_data = {"": ["*.conf"]},
- project_urls = {
- "Bug Tracker": "https://github.com/vkbo/novelWriter/issues",
- "Documentation": "https://github.com/vkbo/novelWriter/issues",
- "Source Code": "https://github.com/vkbo/novelWriter",
- },
- classifiers = [
- "Programming Language :: Python :: 3 :: Only",
- "Programming Language :: Python :: 3.6",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: 3.8",
- "Programming Language :: Python :: 3.9",
- "Programming Language :: Python :: Implementation :: CPython",
- "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
- "Development Status :: 4 - Beta",
- "Operating System :: OS Independent",
- "Intended Audience :: End Users/Desktop",
- "Natural Language :: English",
- "Topic :: Text Editors",
- ],
- python_requires = ">=3.6",
- install_requires = [
- "pyqt5>=5.2.1",
- "lxml>=4.2.0",
- "pyenchant>=3.0.0",
- ],
-)
+def makeLauncherLinux():
+ """Will attempt to install icons and make a launcher.
+ """
+ print("")
+ print("Creating Launcher")
+ print("=================")
+ print("")
+
+ exOpts = []
+
+ testExec = shutil.which("novelWriter")
+ if testExec is not None:
+ exOpts.append(testExec)
+
+ testExec = shutil.which("novelwriter")
+ if testExec is not None:
+ exOpts.append(testExec)
+
+ testExec = os.path.join(os.getcwd(), "novelWriter.py")
+ if os.path.isfile(testExec):
+ exOpts.append(testExec)
+
+ useExec = ""
+ nOpts = len(exOpts)
+ if nOpts == 0:
+ print("Error: No executables for novelWriter found.")
+ sys.exit(1)
+ elif nOpts == 1:
+ useExec = exOpts[0]
+ else:
+ print("Found multiple novelWriter executables:")
+ print("")
+ for iExec, anExec in enumerate(exOpts):
+ print(" [%d] %s" % (iExec, anExec))
+ print("")
+ intVal = int(input("Please select which novelWriter executable to use: "))
+ print("")
+
+ if intVal >= 0 and intVal < nOpts:
+ useExec = exOpts[intVal]
+ else:
+ print("Error: Invalid selection.")
+ sys.exit(1)
+
+ print("Using executable: %s " % useExec)
+
+ # Read the Template
+ desktopData = ""
+ with open(os.path.join("setup", "novelwriter.desktop"), mode="r") as inFile:
+ desktopData = inFile.read()
+
+ desktopData = desktopData.replace(r"%%exec%%", useExec)
+
+ desktopFile = "/usr/share/applications/novelwriter.desktop"
+ try:
+ with open(desktopFile, mode="w+") as outFile:
+ outFile.write(desktopData)
+ print("Wrote file: %s" % desktopFile)
+ except Exception as e:
+ print("Error: Could not write novelwriter.desktop file.")
+ print(str(e))
+ sys.exit(1)
+
+ print("")
+
+ # Copy Icons
+
+ iconDirs = [
+ "/usr/share/icons/hicolor/24x24/apps",
+ "/usr/share/icons/hicolor/48x48/apps",
+ "/usr/share/icons/hicolor/96x96/apps",
+ "/usr/share/icons/hicolor/256x256/apps",
+ "/usr/share/icons/hicolor/512x512/apps",
+ "/usr/share/icons/hicolor/scalable/apps",
+ "/usr/share/icons/hicolor/scalable/mimetypes",
+ ]
+ for iconDir in iconDirs:
+ if not os.path.isdir:
+ try:
+ os.mkdir(iconDir)
+ print("Created folder: %s" % iconDir)
+ except Exception as e:
+ print("Error: Could not make folder: %s" % iconDir)
+ print(str(e))
+
+ copyList = [(
+ "setup/icons/24x24/novelwriter.png",
+ "/usr/share/icons/hicolor/24x24/apps/novelwriter.png"
+ ), (
+ "setup/icons/48x48/novelwriter.png",
+ "/usr/share/icons/hicolor/48x48/apps/novelwriter.png"
+ ), (
+ "setup/icons/96x96/novelwriter.png",
+ "/usr/share/icons/hicolor/96x96/apps/novelwriter.png"
+ ), (
+ "setup/icons/256x256/novelwriter.png",
+ "/usr/share/icons/hicolor/256x256/apps/novelwriter.png"
+ ), (
+ "setup/icons/512x512/novelwriter.png",
+ "/usr/share/icons/hicolor/512x512/apps/novelwriter.png"
+ ), (
+ "setup/icons/novelwriter.svg",
+ "/usr/share/icons/hicolor/scalable/apps/novelwriter.svg"
+ ), (
+ "setup/icons/x-novelwriter-project.svg",
+ "/usr/share/icons/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg"
+ ), (
+ "setup/mime/x-novelwriter-project.xml",
+ "/usr/share/mime/packages/x-novelwriter-project.xml"
+ )]
+ for srcFile, dstFile in copyList:
+ try:
+ shutil.copyfile(srcFile, dstFile)
+ print("Copied file to: %s" % dstFile)
+ except Exception as e:
+ print("Error: Could not copy file: %s" % srcFile)
+ print(str(e))
+
+ print("")
+
+ # Update System
+ try:
+ subprocess.call(["update-mime-database", "/usr/share/mime/"])
+ print("Updated mime database.")
+ except Exception as e:
+ print("Error: Filed to update mime database.")
+ print(str(e))
+
+ try:
+ subprocess.call(["update-icon-caches", "/usr/share/icons/*"])
+ print("Updated icon cache.")
+ except Exception as e:
+ print("Error: Filed to update icon cache.")
+ print(str(e))
+
+ print("")
+ print("Done!")
+ print("")
+
+ return
+
+# =============================================================================================== #
+# Process Jobs
+# =============================================================================================== #
+
+if __name__ == "__main__":
+
+ helpMsg = (
+ "\n"
+ "novelWriter Setup Tool\n"
+ "======================\n"
+ "This tool provides some additional setup commands for novelWriter.\n"
+ "\n"
+ "help Print the help message.\n"
+ "qthelp Build the help documentation for use with the QtAssistant.\n"
+ "sample Build the sample project as a zip file.\n"
+ "launcher Install launcher icons for freedesktop systems.\n"
+ )
+
+ if "help" in sys.argv:
+ sys.argv.remove("help")
+ print(helpMsg)
+ sys.exit(0)
+
+ if "qthelp" in sys.argv:
+ sys.argv.remove("qthelp")
+ buildQtDocs()
+
+ if "sample" in sys.argv:
+ sys.argv.remove("sample")
+ buildSampleZip()
+
+ if "launcher" in sys.argv:
+ sys.argv.remove("launcher")
+ makeLauncherLinux()
+
+ if len(sys.argv) <= 1:
+ # Nothing more to do
+ sys.exit(0)
+
+ # Run the standard setup
+ setuptools.setup()
+
+# END Main
diff --git a/setup/README.md b/setup/README.md
new file mode 100644
index 00000000..81479cf7
--- /dev/null
+++ b/setup/README.md
@@ -0,0 +1,47 @@
+# Build and Install novelWriter
+
+The root folder of the repository contains two scripts for setup and install:
+
+
+## Script `setup.py`
+
+The `setup.py` is a standard Python setup script with a couple of additional options:
+
+* `qthelp`: Will attempt to build a single file QtAssistand documentation file.
+ This requires the Qt tools to be installed on the local system, as well as the sphinx build tools
+ for the documentation.
+* `sample`: Will create a `sample.zip` file in the `nw/assets` folder.
+ This is the file the New Project Wizard uses to generate an example project.
+ If novelWriter is run from source, this file is not needed.
+* `launcher`: Will try to copy the novelWriter icons and create a novelWriter.desktop file to launch
+ the application. This should work on standard Linux desktops.
+
+To install novelWriter as a local Python package, run:
+```bash
+sudo python setup.py install
+```
+
+## Script `make.py`
+
+The `make.py` script provides a number of convenient options for building packages if novelWriter.
+
+Usage:
+```bash
+python make.py [command]
+```
+
+It currently accept the following commands:
+
+* `help`: Print the help message.
+* `freeze`: Freeze the package and produces a folder of all dependencies using pyinstaller.
+* `onefile`: Build a standalone executable with all dependencies bundled.
+ Implies `freeze`, cannot be used with `setup`.
+* `pip`: Run pip to install all package dependencies for novelWriter and this build tool.
+* `setup`: Build a setup.exe installer for Windows.
+ This option automaticall disables the `onefile` option.
+* `clean`: This will attempt to delete the `build` and `dist` folders in the current folder.
+
+For instance, to create a Windows installer, run:
+```bash
+python make.py freeze setup
+```
diff --git a/assets/icons/1024x1024/novelwriter.png b/setup/icons/1024x1024/novelwriter.png
similarity index 100%
rename from assets/icons/1024x1024/novelwriter.png
rename to setup/icons/1024x1024/novelwriter.png
diff --git a/assets/icons/1024x1024/x-novelwriter-project.png b/setup/icons/1024x1024/x-novelwriter-project.png
similarity index 100%
rename from assets/icons/1024x1024/x-novelwriter-project.png
rename to setup/icons/1024x1024/x-novelwriter-project.png
diff --git a/assets/icons/128x128/novelwriter.png b/setup/icons/128x128/novelwriter.png
similarity index 100%
rename from assets/icons/128x128/novelwriter.png
rename to setup/icons/128x128/novelwriter.png
diff --git a/assets/icons/128x128/x-novelwriter-project.png b/setup/icons/128x128/x-novelwriter-project.png
similarity index 100%
rename from assets/icons/128x128/x-novelwriter-project.png
rename to setup/icons/128x128/x-novelwriter-project.png
diff --git a/assets/icons/16x16/novelwriter.png b/setup/icons/16x16/novelwriter.png
similarity index 100%
rename from assets/icons/16x16/novelwriter.png
rename to setup/icons/16x16/novelwriter.png
diff --git a/assets/icons/16x16/x-novelwriter-project.png b/setup/icons/16x16/x-novelwriter-project.png
similarity index 100%
rename from assets/icons/16x16/x-novelwriter-project.png
rename to setup/icons/16x16/x-novelwriter-project.png
diff --git a/assets/icons/16x16@2x/novelwriter.png b/setup/icons/16x16@2x/novelwriter.png
similarity index 100%
rename from assets/icons/16x16@2x/novelwriter.png
rename to setup/icons/16x16@2x/novelwriter.png
diff --git a/assets/icons/16x16@2x/x-novelwriter-project.png b/setup/icons/16x16@2x/x-novelwriter-project.png
similarity index 100%
rename from assets/icons/16x16@2x/x-novelwriter-project.png
rename to setup/icons/16x16@2x/x-novelwriter-project.png
diff --git a/assets/icons/18x18@2x/novelwriter.png b/setup/icons/18x18@2x/novelwriter.png
similarity index 100%
rename from assets/icons/18x18@2x/novelwriter.png
rename to setup/icons/18x18@2x/novelwriter.png
diff --git a/assets/icons/18x18@2x/x-novelwriter-project.png b/setup/icons/18x18@2x/x-novelwriter-project.png
similarity index 100%
rename from assets/icons/18x18@2x/x-novelwriter-project.png
rename to setup/icons/18x18@2x/x-novelwriter-project.png
diff --git a/assets/icons/24x24/novelwriter.png b/setup/icons/24x24/novelwriter.png
similarity index 100%
rename from assets/icons/24x24/novelwriter.png
rename to setup/icons/24x24/novelwriter.png
diff --git a/assets/icons/24x24/x-novelwriter-project.png b/setup/icons/24x24/x-novelwriter-project.png
similarity index 100%
rename from assets/icons/24x24/x-novelwriter-project.png
rename to setup/icons/24x24/x-novelwriter-project.png
diff --git a/assets/icons/256x256/novelwriter.png b/setup/icons/256x256/novelwriter.png
similarity index 100%
rename from assets/icons/256x256/novelwriter.png
rename to setup/icons/256x256/novelwriter.png
diff --git a/assets/icons/256x256/x-novelwriter-project.png b/setup/icons/256x256/x-novelwriter-project.png
similarity index 100%
rename from assets/icons/256x256/x-novelwriter-project.png
rename to setup/icons/256x256/x-novelwriter-project.png
diff --git a/assets/icons/32x32/novelwriter.png b/setup/icons/32x32/novelwriter.png
similarity index 100%
rename from assets/icons/32x32/novelwriter.png
rename to setup/icons/32x32/novelwriter.png
diff --git a/assets/icons/32x32/x-novelwriter-project.png b/setup/icons/32x32/x-novelwriter-project.png
similarity index 100%
rename from assets/icons/32x32/x-novelwriter-project.png
rename to setup/icons/32x32/x-novelwriter-project.png
diff --git a/assets/icons/32x32@2x/novelwriter.png b/setup/icons/32x32@2x/novelwriter.png
similarity index 100%
rename from assets/icons/32x32@2x/novelwriter.png
rename to setup/icons/32x32@2x/novelwriter.png
diff --git a/assets/icons/32x32@2x/x-novelwriter-project.png b/setup/icons/32x32@2x/x-novelwriter-project.png
similarity index 100%
rename from assets/icons/32x32@2x/x-novelwriter-project.png
rename to setup/icons/32x32@2x/x-novelwriter-project.png
diff --git a/assets/icons/48x48/novelwriter.png b/setup/icons/48x48/novelwriter.png
similarity index 100%
rename from assets/icons/48x48/novelwriter.png
rename to setup/icons/48x48/novelwriter.png
diff --git a/assets/icons/48x48/x-novelwriter-project.png b/setup/icons/48x48/x-novelwriter-project.png
similarity index 100%
rename from assets/icons/48x48/x-novelwriter-project.png
rename to setup/icons/48x48/x-novelwriter-project.png
diff --git a/assets/icons/512x512/novelwriter.png b/setup/icons/512x512/novelwriter.png
similarity index 100%
rename from assets/icons/512x512/novelwriter.png
rename to setup/icons/512x512/novelwriter.png
diff --git a/assets/icons/512x512/x-novelwriter-project.png b/setup/icons/512x512/x-novelwriter-project.png
similarity index 100%
rename from assets/icons/512x512/x-novelwriter-project.png
rename to setup/icons/512x512/x-novelwriter-project.png
diff --git a/assets/icons/64x64/novelwriter.png b/setup/icons/64x64/novelwriter.png
similarity index 100%
rename from assets/icons/64x64/novelwriter.png
rename to setup/icons/64x64/novelwriter.png
diff --git a/assets/icons/64x64/x-novelwriter-project.png b/setup/icons/64x64/x-novelwriter-project.png
similarity index 100%
rename from assets/icons/64x64/x-novelwriter-project.png
rename to setup/icons/64x64/x-novelwriter-project.png
diff --git a/assets/icons/96x96/novelwriter.png b/setup/icons/96x96/novelwriter.png
similarity index 100%
rename from assets/icons/96x96/novelwriter.png
rename to setup/icons/96x96/novelwriter.png
diff --git a/assets/icons/96x96/x-novelwriter-project.png b/setup/icons/96x96/x-novelwriter-project.png
similarity index 100%
rename from assets/icons/96x96/x-novelwriter-project.png
rename to setup/icons/96x96/x-novelwriter-project.png
diff --git a/assets/icons/novelwriter.ico b/setup/icons/novelwriter.ico
similarity index 100%
rename from assets/icons/novelwriter.ico
rename to setup/icons/novelwriter.ico
diff --git a/assets/icons/novelwriter.svg b/setup/icons/novelwriter.svg
similarity index 100%
rename from assets/icons/novelwriter.svg
rename to setup/icons/novelwriter.svg
diff --git a/assets/icons/x-novelwriter-project.svg b/setup/icons/x-novelwriter-project.svg
similarity index 100%
rename from assets/icons/x-novelwriter-project.svg
rename to setup/icons/x-novelwriter-project.svg
diff --git a/assets/mime/x-novelwriter-project.xml b/setup/mime/x-novelwriter-project.xml
similarity index 85%
rename from assets/mime/x-novelwriter-project.xml
rename to setup/mime/x-novelwriter-project.xml
index 21ecbe0a..789c08fd 100644
--- a/assets/mime/x-novelwriter-project.xml
+++ b/setup/mime/x-novelwriter-project.xml
@@ -1,7 +1,7 @@
- novelWriter Project
+ novelWriter Project
diff --git a/assets/novelwriter.desktop b/setup/novelwriter.desktop
similarity index 100%
rename from assets/novelwriter.desktop
rename to setup/novelwriter.desktop
diff --git a/setup/win_setup.iss b/setup/win_setup.iss
new file mode 100644
index 00000000..8b6be609
--- /dev/null
+++ b/setup/win_setup.iss
@@ -0,0 +1,52 @@
+; Script generated by the Inno Setup Script Wizard.
+; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
+
+#define nwAppDir "%%dir%%\dist"
+#define nwAppName "novelWriter"
+#define nwAppVersion "%%version%%"
+#define nwAppPublisher "novelWriter"
+#define nwAppURL "http://novelWriter.io"
+#define nwAppExeName "novelWriter.exe"
+
+[Setup]
+; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
+; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
+AppId={{459A75D0-951F-4932-9809-6002EC8E733E}
+AppName={#nwAppName}
+AppVersion={#nwAppVersion}
+AppVerName={#nwAppName} {#nwAppVersion}
+AppPublisher={#nwAppPublisher}
+AppPublisherURL={#nwAppURL}
+AppSupportURL={#nwAppURL}
+AppUpdatesURL={#nwAppURL}
+DefaultDirName={autopf}\{#nwAppName}
+DisableProgramGroupPage=yes
+; The [Icons] "quicklaunchicon" entry uses {userappdata} but its [Tasks] entry has a proper IsAdminInstallMode Check.
+UsedUserAreasWarning=no
+; Uncomment the following line to run in non administrative install mode (install for current user only.)
+;PrivilegesRequired=lowest
+PrivilegesRequiredOverridesAllowed=dialog
+OutputDir={#nwAppDir}
+OutputBaseFilename=novelwriter_{#nwAppVersion}_win_amd64_setup
+Compression=lzma
+SolidCompression=yes
+WizardStyle=modern
+ArchitecturesInstallIn64BitMode=x64
+
+[Languages]
+Name: "english"; MessagesFile: "compiler:Default.isl"
+
+[Tasks]
+Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
+Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode
+
+[Files]
+Source: "{#nwAppDir}\novelWriter\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
+
+[Icons]
+Name: "{autoprograms}\{#nwAppName}"; Filename: "{app}\{#nwAppExeName}"
+Name: "{autodesktop}\{#nwAppName}"; Filename: "{app}\{#nwAppExeName}"; Tasks: desktopicon
+Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#nwAppName}"; Filename: "{app}\{#nwAppExeName}"; Tasks: quicklaunchicon
+
+[Run]
+Filename: "{app}\{#nwAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(nwAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
diff --git a/tests/test_item.py b/tests/test_item.py
index d1097161..936ba185 100644
--- a/tests/test_item.py
+++ b/tests/test_item.py
@@ -31,13 +31,13 @@ def testItemSettersSimple(nwDummy):
# Parent
theItem.setParent(None)
- assert theItem.parHandle is None
+ assert theItem.itemParent is None
theItem.setParent(123)
- assert theItem.parHandle is None
+ assert theItem.itemParent is None
theItem.setParent("0123456789abcdef")
- assert theItem.parHandle is None
+ assert theItem.itemParent is None
theItem.setParent("0123456789abc")
- assert theItem.parHandle == "0123456789abc"
+ assert theItem.itemParent == "0123456789abc"
# Order
theItem.setOrder(None)
@@ -227,7 +227,7 @@ def testItemXMLPackUnpack(nwDummy):
# Unpack
assert theItem.unpackXML(xContent[0])
assert theItem.itemHandle == "0123456789abc"
- assert theItem.parHandle == "0123456789abc"
+ assert theItem.itemParent == "0123456789abc"
assert theItem.itemOrder == 1
assert theItem.isExpanded
assert theItem.paraCount == 3
diff --git a/tests/test_project.py b/tests/test_project.py
index 282acbc7..e8f1442b 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -521,7 +521,7 @@ def testProjectOrphanedFiles(nwDummy, nwLipsum):
assert oItem is not None
assert oItem.itemName == "Mars"
assert oItem.itemHandle == "636b6aa9b697b"
- assert oItem.parHandle is None
+ assert oItem.itemParent is None
assert oItem.itemClass == nwItemClass.WORLD
assert oItem.itemType == nwItemType.FILE
assert oItem.itemLayout == nwItemLayout.NOTE
@@ -531,7 +531,7 @@ def testProjectOrphanedFiles(nwDummy, nwLipsum):
assert oItem is not None
assert oItem.itemName == "Orphaned File 1"
assert oItem.itemHandle == "736b6aa9b697b"
- assert oItem.parHandle is None
+ assert oItem.itemParent is None
assert oItem.itemClass == nwItemClass.NO_CLASS
assert oItem.itemType == nwItemType.FILE
assert oItem.itemLayout == nwItemLayout.NO_LAYOUT
diff --git a/tests/test_tools.py b/tests/test_tools.py
index bfff8f3f..4832aadb 100644
--- a/tests/test_tools.py
+++ b/tests/test_tools.py
@@ -61,6 +61,7 @@ def testNumberWords():
assert numberToWord(21, "en") == "Twenty-One"
assert numberToWord(29, "en") == "Twenty-Nine"
assert numberToWord(42, "en") == "Forty-Two"
+ assert numberToWord(114, "en") == "One Hundred Fourteen"
assert numberToWord(142, "en") == "One Hundred Forty-Two"
assert numberToWord(999, "en") == "Nine Hundred Ninety-Nine"