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 @@ [![pypi](https://img.shields.io/pypi/v/novelwriter)](https://pypi.org/project/novelWriter) [![python](https://img.shields.io/pypi/pyversions/novelwriter)](https://pypi.org/project/novelWriter) - + 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"