Merge branch 'main' into release_1.0rc1

This commit is contained in:
Veronica K. B. Olsen
2020-10-18 11:45:37 +02:00
76 changed files with 992 additions and 457 deletions
+2 -2
View File
@@ -25,5 +25,5 @@ jobs:
flake8 tests --count --select=E9,F63,F7,F82 --show-source --statistics flake8 tests --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Coding Style Violations - name: Coding Style Violations
run: | run: |
flake8 nw --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,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
+1
View File
@@ -5,6 +5,7 @@
/deploy/ /deploy/
*.spec *.spec
*.egg-info *.egg-info
setup.iss
# Documentation # Documentation
/docs/build/ /docs/build/
+1 -1
View File
@@ -1,4 +1,4 @@
include LICENSE.md include LICENSE.md
recursive-include assets * recursive-include setup *
recursive-include nw/assets * recursive-include nw/assets *
recursive-include sample *.nwx *.nwd recursive-include sample *.nwx *.nwd
+62 -45
View File
@@ -12,7 +12,7 @@
[![pypi](https://img.shields.io/pypi/v/novelwriter)](https://pypi.org/project/novelWriter) [![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) [![python](https://img.shields.io/pypi/pyversions/novelwriter)](https://pypi.org/project/novelWriter)
<img align="left" style="margin: 0 16px 4px 0;" src="https://raw.githubusercontent.com/vkbo/novelWriter/main/assets/icons/96x96/novelwriter.png"> <img align="left" style="margin: 0 16px 4px 0;" src="https://raw.githubusercontent.com/vkbo/novelWriter/main/setup/icons/96x96/novelwriter.png">
novelWriter is a Markdown-like text editor designed for writing novels and larger projects of many 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 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. 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 ```bash
pip install novelwriter 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 ```bash
pip install --upgrade novelwriter 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 ```bash
./novelWriter.py ./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`. You can also create a launcher for running directly from source with:
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:
```bash ```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 more install options, see [Build and Install novelWriter](setup/README.md).
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)
### macOS ## macOS
These instructions assume you're using brew, and have Python and pip set up. 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. 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 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. To install dependencies, run:
That should also install the Qt libraries and the spell check dictionary dependencies. ```bash
pip install --user -r requirements.txt
```
**Note:** On Windows, make sure Python3 is in your PATH if you want to launch novelWriter from **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 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 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 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 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 file in that folder for how to generate more dictionaries. Note that the difflib-based option is
both slow and limited. 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. Some features of novelWriter are listed below. Consult the documentation for more information.
-49
View File
@@ -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/
-42
View File
@@ -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/*
-1
View File
@@ -12,7 +12,6 @@
# add these directories to sys.path here. If the directory is relative to the # 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. # documentation root, use os.path.abspath to make it absolute, like shown here.
# #
# import os
# import sys # import sys
# sys.path.insert(0, os.path.abspath(".")) # sys.path.insert(0, os.path.abspath("."))
import os import os
+1 -1
View File
@@ -140,7 +140,7 @@ encountered. To list all options, run:
python novelWriter.py --help 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, launch icon and the novelWriter project file mimetype for Gnome desktops on Linux. Currently,
there's one script for Debian and one for Ubuntu. there's one script for Debian and one for Ubuntu.
-84
View File
@@ -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("")
Executable
+333
View File
@@ -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
+22
View File
@@ -35,6 +35,28 @@ from PyQt5.QtWidgets import QApplication, QErrorMessage
from nw.error import exceptionHandler from nw.error import exceptionHandler
from nw.config import Config 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" __package__ = "nw"
__author__ = "Veronica Berglyd Olsen" __author__ = "Veronica Berglyd Olsen"
__copyright__ = "Copyright 20182020, Veronica Berglyd Olsen" __copyright__ = "Copyright 20182020, Veronica Berglyd Olsen"
+3 -3
View File
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter Language Codes """novelWriter ISO Codes
novelWriter Language Codes novelWriter ISO Codes
============================== =========================
Handles translating language codes to language names Handles translating language codes to language names
File History: File History:
+9 -6
View File
@@ -67,7 +67,9 @@ class NWDoc():
def openDocument(self, tHandle, showStatus=True, isOrphan=False): def openDocument(self, tHandle, showStatus=True, isOrphan=False):
"""Open a document from handle, capturing potential file system """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): if not isHandle(tHandle):
return None return None
@@ -120,13 +122,13 @@ class NWDoc():
logger.verbose("DocMeta: '%s'" % self._docMeta) logger.verbose("DocMeta: '%s'" % self._docMeta)
if showStatus and not isOrphan: 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 return theText
def saveDocument(self, docText): def saveDocument(self, docText):
"""Save the document via temp file in case of save failure, and """Save the document. The file is saved via a temp file in case
in any case keep a backup of the file. of save failure. Returns True if successful, False if not.
""" """
if self._docHandle is None: if self._docHandle is None:
return False return False
@@ -139,6 +141,7 @@ class NWDoc():
docPath = os.path.join(self.theProject.projContent, docFile) docPath = os.path.join(self.theProject.projContent, docFile)
docTemp = os.path.join(self.theProject.projContent, docFile+"~") docTemp = os.path.join(self.theProject.projContent, docFile+"~")
# DocMeta line
if self._theItem is None: if self._theItem is None:
docMeta = "" docMeta = ""
else: else:
@@ -166,12 +169,12 @@ class NWDoc():
os.unlink(docPath) os.unlink(docPath)
os.rename(docTemp, 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 return True
def deleteDocument(self, tHandle): 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. from the project data folder.
""" """
if not isHandle(tHandle): if not isHandle(tHandle):
+3 -3
View File
@@ -279,7 +279,7 @@ class NWIndex():
if theItem.itemLayout == nwItemLayout.NO_LAYOUT: if theItem.itemLayout == nwItemLayout.NO_LAYOUT:
logger.info("Not indexing no-layout item %s" % tHandle) logger.info("Not indexing no-layout item %s" % tHandle)
return False return False
if theItem.parHandle is None: if theItem.itemParent is None:
logger.info("Not indexing orphaned item %s" % tHandle) logger.info("Not indexing orphaned item %s" % tHandle)
return False return False
@@ -288,7 +288,7 @@ class NWIndex():
self.textCounts[tHandle] = [cC, wC, pC] self.textCounts[tHandle] = [cC, wC, pC]
# If the file is archived or trashed, we don't index the file itself # 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) logger.info("Not indexing trash item %s" % tHandle)
return False return False
if theRoot.itemClass == nwItemClass.ARCHIVE: if theRoot.itemClass == nwItemClass.ARCHIVE:
@@ -583,7 +583,7 @@ class NWIndex():
def getCounts(self, tHandle, sTitle=None): def getCounts(self, tHandle, sTitle=None):
"""Returns the counts for a file, or a section of a file """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 cC = 0
wC = 0 wC = 0
+11 -8
View File
@@ -42,7 +42,7 @@ class NWItem():
self.itemName = "" self.itemName = ""
self.itemHandle = None self.itemHandle = None
self.parHandle = None self.itemParent = None
self.itemOrder = None self.itemOrder = None
self.itemType = nwItemType.NO_TYPE self.itemType = nwItemType.NO_TYPE
self.itemClass = nwItemClass.NO_CLASS self.itemClass = nwItemClass.NO_CLASS
@@ -70,7 +70,7 @@ class NWItem():
xPack = etree.SubElement(xParent, "item", attrib={ xPack = etree.SubElement(xParent, "item", attrib={
"handle" : str(self.itemHandle), "handle" : str(self.itemHandle),
"order" : str(self.itemOrder), "order" : str(self.itemOrder),
"parent" : str(self.parHandle), "parent" : str(self.itemParent),
}) })
self._subPack(xPack, "name", text=str(self.itemName)) self._subPack(xPack, "name", text=str(self.itemName))
self._subPack(xPack, "type", text=str(self.itemType.name)) 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) self._subPack(xPack, "cursorPos", text=str(self.cursorPos), none=False)
else: else:
self._subPack(xPack, "expanded", text=str(self.isExpanded)) self._subPack(xPack, "expanded", text=str(self.isExpanded))
return return
def unpackXML(self, xItem): def unpackXML(self, xItem):
@@ -101,7 +102,7 @@ class NWItem():
return False return False
if "parent" in xItem.attrib: if "parent" in xItem.attrib:
self.parHandle = xItem.attrib["parent"] self.itemParent = xItem.attrib["parent"]
setMap = { setMap = {
"name" : self.setName, "name" : self.setName,
@@ -131,9 +132,11 @@ class NWItem():
""" """
if not none and (text is None or text == "None"): if not none and (text is None or text == "None"):
return 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: if text is not None:
xSub.text = text xSub.text = text
return return
## ##
@@ -162,14 +165,14 @@ class NWItem():
"""Set the parent handle, and ensure that it is valid. """Set the parent handle, and ensure that it is valid.
""" """
if theParent is None: if theParent is None:
self.parHandle = None self.itemParent = None
elif isinstance(theParent, str): elif isinstance(theParent, str):
if len(theParent) == 13: if len(theParent) == 13:
self.parHandle = theParent self.itemParent = theParent
else: else:
self.parHandle = None self.itemParent = None
else: else:
self.parHandle = None self.itemParent = None
return return
def setOrder(self, theOrder): def setOrder(self, theOrder):
+5 -5
View File
@@ -782,7 +782,7 @@ class NWProject():
"""Create a zip file of the entire project. """Create a zip file of the entire project.
""" """
logger.info("Backing up 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 == "": if self.mainConf.backupPath is None or self.mainConf.backupPath == "":
self.theParent.makeAlert(( self.theParent.makeAlert((
@@ -847,7 +847,7 @@ class NWProject():
) )
return False 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 return True
@@ -1139,16 +1139,16 @@ class NWProject():
# Technically a bug since treeOrder is built from the # Technically a bug since treeOrder is built from the
# same data as projTree # same data as projTree
continue continue
elif tItem.parHandle is None: elif tItem.itemParent is None:
# Item is a root, or already been identified as an # Item is a root, or already been identified as an
# orphaned item # orphaned item
sentItems.append(tHandle) sentItems.append(tHandle)
yield tItem yield tItem
elif tItem.parHandle in sentItems: elif tItem.itemParent in sentItems:
# Item's parent has been sent, so all is fine # Item's parent has been sent, so all is fine
sentItems.append(tHandle) sentItems.append(tHandle)
yield tItem yield tItem
elif tItem.parHandle in iterItems: elif tItem.itemParent in iterItems:
# Item's parent exists, but hasn't been sent yet, so add # Item's parent exists, but hasn't been sent yet, so add
# it again to the end # it again to the end
logger.warning("Item %s found before its parent" % tHandle) logger.warning("Item %s found before its parent" % tHandle)
+2 -2
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter Spell Check Wrapper """novelWriter Spell Check Classes
novelWriter Spell Check Wrapper novelWriter Spell Check Classes
=================================== ===================================
Wrapper class for spell checking Wrapper class for spell checking
+1
View File
@@ -41,6 +41,7 @@ class ToHtml(Tokenizer):
def __init__(self, theProject, theParent): def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent) Tokenizer.__init__(self, theProject, theParent)
self.genMode = self.M_EXPORT self.genMode = self.M_EXPORT
self.cssStyles = True self.cssStyles = True
+16 -16
View File
@@ -1,14 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter Word Counter """novelWriter Various Tools
novelWriter Word Counter novelWriter Various Tools
============================ =============================
Simple word counter Various core tool functions
File History: File History:
Created: 2019-04-22 [0.0.1] countWords Created: 2019-04-22 [0.0.1] countWords
Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN
Merged: 2020-05-08 [0.4.5] All of the above into this file 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 This file is a part of novelWriter
Copyright 20182020, Veronica Berglyd Olsen Copyright 20182020, Veronica Berglyd Olsen
@@ -136,7 +137,6 @@ def numberToWord(numVal, theLanguage):
def _numberToWordEN(numVal): def _numberToWordEN(numVal):
"""Convert numbers to English words. """Convert numbers to English words.
""" """
numWord = ""
oneWord = "" oneWord = ""
tenWord = "" tenWord = ""
hunWord = "" hunWord = ""
@@ -145,8 +145,8 @@ def _numberToWordEN(numVal):
return "Zero" return "Zero"
oneVal = numVal % 10 oneVal = numVal % 10
tenVal = (numVal-oneVal) % 100 tenVal = (numVal - oneVal) % 100
hunVal = (numVal-tenVal-oneVal) % 1000 hunVal = (numVal - tenVal - oneVal) % 1000
theHundreds = { theHundreds = {
100: "One Hundred", 200: "Two Hundred", 300: "Three Hundred", 100: "One Hundred", 200: "Two Hundred", 300: "Three Hundred",
@@ -167,18 +167,18 @@ def _numberToWordEN(numVal):
} }
hunWord = theHundreds.get(hunVal, "") hunWord = theHundreds.get(hunVal, "")
tenWord = theTens.get(tenVal, "")
if tenVal == 10: if tenVal == 10:
oneWord = theTeens.get(oneVal, "") oneWord = theTeens.get(oneVal, "")
numWord = ("%s %s" % (hunWord, oneWord)).strip() return f"{hunWord} {oneWord}".strip()
else: else:
oneWord = theOnes.get(oneVal, "") oneWord = theOnes.get(oneVal, "")
if tenVal == 0: if tenVal == 0:
numWord = ("%s %s" % (hunWord, oneWord)).strip() return f"{hunWord} {oneWord}".strip()
else: else:
tenWord = theTens.get(tenVal, "")
if oneVal == 0: if oneVal == 0:
numWord = ("%s %s" % (hunWord, tenWord)).strip() return f"{hunWord} {tenWord}".strip()
else: else:
numWord = ("%s %s-%s" % (hunWord, tenWord, oneWord)).strip() return f"{hunWord} {tenWord}-{oneWord}".strip()
return numWord return ""
+5 -5
View File
@@ -134,7 +134,7 @@ class NWTree():
for xItem in xContent: for xItem in xContent:
nwItem = NWItem(self.theProject) nwItem = NWItem(self.theProject)
if nwItem.unpackXML(xItem): if nwItem.unpackXML(xItem):
self.append(nwItem.itemHandle, nwItem.parHandle, nwItem) self.append(nwItem.itemHandle, nwItem.itemParent, nwItem)
nwItem.saveInitialCount() nwItem.saveInitialCount()
return True return True
@@ -261,10 +261,10 @@ class NWTree():
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem is not None: if tItem is not None:
for i in range(nwConst.maxDepth + 1): for i in range(nwConst.maxDepth + 1):
if tItem.parHandle is None: if tItem.itemParent is None:
return tItem return tItem
else: else:
tHandle = tItem.parHandle tHandle = tItem.itemParent
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
return None return None
@@ -279,10 +279,10 @@ class NWTree():
if tItem is not None: if tItem is not None:
tTree.append(tHandle) tTree.append(tHandle)
for i in range(nwConst.maxDepth + 1): for i in range(nwConst.maxDepth + 1):
if tItem.parHandle is None: if tItem.itemParent is None:
return tTree return tTree
else: else:
tHandle = tItem.parHandle tHandle = tItem.itemParent
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem is None: if tItem is None:
return tTree return tTree
+1 -1
View File
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter Init """novelWriter Exception Handling
novelWriter Exception Handling novelWriter Exception Handling
================================== ==================================
-1
View File
@@ -222,7 +222,6 @@ class GuiAbout(QDialog):
hColB = self.theParent.theTheme.colHead[2], hColB = self.theParent.theTheme.colHead[2],
) )
self.pageAbout.document().setDefaultStyleSheet(styleSheet) self.pageAbout.document().setDefaultStyleSheet(styleSheet)
# self.pageCredit.document().setDefaultStyleSheet(styleSheet)
self.pageLicense.document().setDefaultStyleSheet(styleSheet) self.pageLicense.document().setDefaultStyleSheet(styleSheet)
return return
+2 -2
View File
@@ -684,8 +684,8 @@ class GuiBuildNovel(QDialog):
isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT
isNone |= theItem.itemClass == nwItemClass.NO_CLASS isNone |= theItem.itemClass == nwItemClass.NO_CLASS
isNone |= theItem.itemClass == nwItemClass.TRASH isNone |= theItem.itemClass == nwItemClass.TRASH
isNone |= theItem.parHandle == self.theProject.projTree.trashRoot() isNone |= theItem.itemParent == self.theProject.projTree.trashRoot()
isNone |= theItem.parHandle is None isNone |= theItem.itemParent is None
isNote = theItem.itemLayout == nwItemLayout.NOTE isNote = theItem.itemLayout == nwItemLayout.NOTE
isNovel = not isNone and not isNote isNovel = not isNone and not isNote
+4 -4
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter Addition QConfigLayout """novelWriter Custom Widgets and Layouts
novelWriter Addition QConfigLayout novelWriter Custom Widgets and Layouts
====================================== ==========================================
A custom Qt grid layout for config forms similar to QFormLayout Various custom widget and layout classes
File History: File History:
Created: 2020-05-03 [0.4.5] QConfigLayout Created: 2020-05-03 [0.4.5] QConfigLayout
+9 -10
View File
@@ -807,16 +807,16 @@ class GuiDocEditor(QTextEdit):
return self.docSearch.cycleFocus(toNext) return self.docSearch.cycleFocus(toNext)
return True return True
def mouseReleaseEvent(self, mEvent): def mouseReleaseEvent(self, theEvent):
"""If the mouse button is released and the control key is """If the mouse button is released and the control key is
pressed, check if we're clicking on a tag, and trigger the pressed, check if we're clicking on a tag, and trigger the
follow tag function. follow tag function.
""" """
if qApp.keyboardModifiers() == Qt.ControlModifier: if qApp.keyboardModifiers() == Qt.ControlModifier:
theCursor = self.cursorForPosition(mEvent.pos()) theCursor = self.cursorForPosition(theEvent.pos())
self._followTag(theCursor) self._followTag(theCursor)
QTextEdit.mouseReleaseEvent(self, mEvent) QTextEdit.mouseReleaseEvent(self, theEvent)
self.docFooter.updateLineCount() self.docFooter.updateLineCount()
return return
@@ -1203,20 +1203,19 @@ class GuiDocEditor(QTextEdit):
"""Check if document size crosses the big document limit set in """Check if document size crosses the big document limit set in
config. If so, we will set the big document flag to True. config. If so, we will set the big document flag to True.
""" """
newState = theSize > self.mainConf.bigDocLimit*1000 bigLim = self.mainConf.bigDocLimit*1000
newState = theSize > bigLim
if newState != self.bigDoc: if newState != self.bigDoc:
if newState: if newState:
logger.info( logger.info(
"The document size is {:n} > {:n}, big doc mode has been enabled".format( f"The document size is {theSize:n} > {bigLim:n}, "
theSize, self.mainConf.bigDocLimit*1000 f"big doc mode has been enabled"
)
) )
else: else:
logger.info( logger.info(
"The document size is {:n} <= {:n}, big doc mode has been disabled".format( f"The document size is {theSize:n} <= {bigLim:n}, "
theSize, self.mainConf.bigDocLimit*1000 f"big doc mode has been disabled"
)
) )
self.bigDoc = newState self.bigDoc = newState
+7 -3
View File
@@ -149,18 +149,22 @@ class GuiDocHighlighter(QSyntaxHighlighter):
# Quoted Strings # Quoted Strings
if self.mainConf.highlightQuotes: 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(( self.hRules.append((
"\\B{:s}(.*?){:s}\\B".format('"', '"'), { "\\B\"(.*?)\"\\B", {
0 : self.hStyles["dialogue1"], 0 : self.hStyles["dialogue1"],
} }
)) ))
self.hRules.append(( self.hRules.append((
"\\B{:s}(.*?){:s}\\B".format(*self.mainConf.fmtDoubleQuotes), { f"\\B{fmtDO:s}(.*?){fmtDC:s}\\B", {
0 : self.hStyles["dialogue2"], 0 : self.hStyles["dialogue2"],
} }
)) ))
self.hRules.append(( self.hRules.append((
"\\B{:s}(.*?){:s}\\B".format(*self.mainConf.fmtSingleQuotes), { f"\\B{fmtSO:s}(.*?){fmtSC:s}\\B", {
0 : self.hStyles["dialogue3"], 0 : self.hStyles["dialogue3"],
} }
)) ))
+1 -1
View File
@@ -127,7 +127,7 @@ class GuiDocMerge(QDialog):
), nwAlert.ERROR) ), nwAlert.ERROR)
return 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 = self.theProject.projTree[nHandle]
newItem.setStatus(srcItem.itemStatus) newItem.setStatus(srcItem.itemStatus)
+2 -2
View File
@@ -154,7 +154,7 @@ class GuiDocSplit(QDialog):
return return
# Check that another folder can be created # 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: if len(parTree) >= nwConst.maxDepth - 1:
self.theParent.makeAlert(( self.theParent.makeAlert((
"Cannot add new folder for the document split. " "Cannot add new folder for the document split. "
@@ -176,7 +176,7 @@ class GuiDocSplit(QDialog):
# Create the folder # Create the folder
fHandle = self.theProject.newFolder( fHandle = self.theProject.newFolder(
srcItem.itemName, srcItem.itemClass, srcItem.parHandle srcItem.itemName, srcItem.itemClass, srcItem.itemParent
) )
self.theParent.treeView.revealNewTreeItem(fHandle) self.theParent.treeView.revealNewTreeItem(fHandle)
logger.verbose("Creating folder %s" % fHandle) logger.verbose("Creating folder %s" % fHandle)
+7 -3
View File
@@ -439,6 +439,10 @@ class GuiOutline(QTreeWidget):
newItem = QTreeWidgetItem() newItem = QTreeWidgetItem()
hIcon = "doc_%s" % tLevel.lower() 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.setText(self.colIndex[nwOutline.TITLE], novIdx["title"])
newItem.setData(self.colIndex[nwOutline.TITLE], Qt.UserRole, tHandle) newItem.setData(self.colIndex[nwOutline.TITLE], Qt.UserRole, tHandle)
newItem.setIcon(self.colIndex[nwOutline.TITLE], self.theTheme.getIcon(hIcon)) 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.setText(self.colIndex[nwOutline.LINE], sTitle[1:].lstrip("0"))
newItem.setData(self.colIndex[nwOutline.LINE], Qt.UserRole, sTitle) newItem.setData(self.colIndex[nwOutline.LINE], Qt.UserRole, sTitle)
newItem.setText(self.colIndex[nwOutline.SYNOP], novIdx["synopsis"]) newItem.setText(self.colIndex[nwOutline.SYNOP], novIdx["synopsis"])
newItem.setText(self.colIndex[nwOutline.CCOUNT], "{:n}".format(novIdx["cCount"])) newItem.setText(self.colIndex[nwOutline.CCOUNT], f"{cC:n}")
newItem.setText(self.colIndex[nwOutline.WCOUNT], "{:n}".format(novIdx["wCount"])) newItem.setText(self.colIndex[nwOutline.WCOUNT], f"{wC:n}")
newItem.setText(self.colIndex[nwOutline.PCOUNT], "{:n}".format(novIdx["pCount"])) newItem.setText(self.colIndex[nwOutline.PCOUNT], f"{pC:n}")
newItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight) newItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight) newItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight)
newItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight) newItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight)
+11 -7
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter GUI Project Outline """novelWriter GUI Project Outline Details
novelWriter GUI Project Outline novelWriter GUI Project Outline Details
=================================== ===========================================
Class holding the project outline view Class holding the project outline details view
File History: File History:
Created: 2020-06-02 [0.7.0] Created: 2020-06-02 [0.7.0]
@@ -266,9 +266,13 @@ class GuiOutlineDetails(QScrollArea):
self.fileValue.setText(nwItem.itemName) self.fileValue.setText(nwItem.itemName)
self.itemValue.setText(nwItem.itemStatus) self.itemValue.setText(nwItem.itemStatus)
self.cCValue.setText("{:n}".format(checkInt(novIdx["cCount"], 0))) cC = checkInt(novIdx["cCount"], 0)
self.wCValue.setText("{:n}".format(checkInt(novIdx["wCount"], 0))) wC = checkInt(novIdx["wCount"], 0)
self.pCValue.setText("{:n}".format(checkInt(novIdx["pCount"], 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"]) self.synopValue.setText(novIdx["synopsis"])
+4 -4
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter GUI Config Editor """novelWriter GUI Preferences
novelWriter GUI Config Editor novelWriter GUI Preferences
================================= ===============================
Class holding the config dialog Class holding the preferences dialog
File History: File History:
Created: 2019-06-10 [0.1.5] Created: 2019-06-10 [0.1.5]
+1 -1
View File
@@ -3,7 +3,7 @@
novelWriter GUI Open Project novelWriter GUI Open Project
================================ ================================
New and open project dialog The open project dialog
File History: File History:
Created: 2020-02-26 [0.4.5] Created: 2020-02-26 [0.4.5]
+4 -3
View File
@@ -270,11 +270,12 @@ class GuiProjectEditMeta(QWidget):
self.revLabel = QLabel("Revision count:") self.revLabel = QLabel("Revision count:")
self.revLabel.setIndent(xInd) 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 = QLabel("Edit time:")
self.editLabel.setIndent(xInd) self.editLabel.setIndent(xInd)
self.editValue = QLabel("{:.2f} hours".format(self.theProject.editTime/3600)) self.editValue = QLabel(f"{editHours:.2f} hours")
self.statsLabel = QLabel("<b>Project Stats</b>") self.statsLabel = QLabel("<b>Project Stats</b>")
@@ -294,7 +295,7 @@ class GuiProjectEditMeta(QWidget):
self.wordsLabel = QLabel("Word count:") self.wordsLabel = QLabel("Word count:")
self.wordsLabel.setIndent(xInd) 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.headLabel, 0, 0, 1, 2, Qt.AlignTop)
self.mainForm.addWidget(self.nameLabel, 1, 0, 1, 1, Qt.AlignTop) self.mainForm.addWidget(self.nameLabel, 1, 0, 1, 1, Qt.AlignTop)
+10 -10
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter GUI Document Tree """novelWriter GUI Project Tree
novelWriter GUI Document Tree novelWriter GUI project Tree
================================= ================================
Class holding the left side document tree view Class holding the left side project tree view
File History: File History:
Created: 2018-09-29 [0.0.1] GuiProjectTree Created: 2018-09-29 [0.0.1] GuiProjectTree
@@ -219,7 +219,7 @@ class GuiProjectTree(QTreeWidget):
pItem = self.theProject.projTree[pHandle] pItem = self.theProject.projTree[pHandle]
if pItem.itemType == nwItemType.FILE: if pItem.itemType == nwItemType.FILE:
nHandle = pHandle nHandle = pHandle
pHandle = pItem.parHandle pHandle = pItem.itemParent
# If we again have no home, give up # If we again have no home, give up
if pHandle is None: if pHandle is None:
@@ -270,7 +270,7 @@ class GuiProjectTree(QTreeWidget):
""" """
nwItem = self.theProject.projTree[tHandle] nwItem = self.theProject.projTree[tHandle]
trItem = self._addTreeItem(nwItem, nHandle) trItem = self._addTreeItem(nwItem, nHandle)
pHandle = nwItem.parHandle pHandle = nwItem.itemParent
if pHandle is not None and pHandle in self.theMap: if pHandle is not None and pHandle in self.theMap:
self.theMap[pHandle].setExpanded(True) self.theMap[pHandle].setExpanded(True)
self.clearSelection() self.clearSelection()
@@ -430,7 +430,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("Could not delete item") logger.error("Could not delete item")
return False return False
pHandle = nwItemS.parHandle pHandle = nwItemS.itemParent
if self.theProject.projTree.isTrashRoot(pHandle): if self.theProject.projTree.isTrashRoot(pHandle):
# If the file is in the trash folder already, as the # If the file is in the trash folder already, as the
# user if they want to permanently delete the file. # user if they want to permanently delete the file.
@@ -815,7 +815,7 @@ class GuiProjectTree(QTreeWidget):
project tree. project tree.
""" """
tHandle = nwItem.itemHandle tHandle = nwItem.itemHandle
pHandle = nwItem.parHandle pHandle = nwItem.itemParent
tClass = nwItem.itemClass tClass = nwItem.itemClass
newItem = QTreeWidgetItem([""]*4) newItem = QTreeWidgetItem([""]*4)
@@ -1022,11 +1022,11 @@ class GuiProjectTreeMenu(QMenu):
trashHandle = self.theTree.theProject.projTree.trashRoot() 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 isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
isFile = theItem.itemType == nwItemType.FILE isFile = theItem.itemType == nwItemType.FILE
isArch = theRoot.itemClass == nwItemClass.ARCHIVE isArch = theRoot.itemClass == nwItemClass.ARCHIVE
isOrph = isFile and theItem.parHandle is None isOrph = isFile and theItem.itemParent is None
showOpen = isFile showOpen = isFile
showView = isFile showView = isFile
+8 -15
View File
@@ -195,28 +195,21 @@ class GuiMainStatus(QStatusBar):
self.statsText.setToolTip( self.statsText.setToolTip(
"Project word count (session change)" "Project word count (session change)"
) )
self.statsText.setText(( self.statsText.setText(
"Words: {pWC:n} ({sWC:+n})" f"Words: {self.projWords:n} ({self.sessWords:+n})"
).format( )
pWC = self.projWords,
sWC = self.sessWords,
))
return return
def _updateTime(self): def _updateTime(self):
"""Update the session clock. """Update the session clock.
""" """
if self.refTime is None: if self.refTime is None:
theTime = "00:00:00" self.timeText.setText("00:00:00")
else: else:
# This is much faster than using datetime format
tS = int(time() - self.refTime) tS = int(time() - self.refTime)
tM = int(tS/60) self.timeText.setText(
tH = int(tM/60) f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}"
tM = tM - tH*60 )
tS = tS - tM*60 - tH*3600
theTime = "%02d:%02d:%02d" % (tH, tM, tS)
self.timeText.setText(theTime)
return return
# END Class GuiMainStatus # END Class GuiMainStatus
@@ -237,7 +230,7 @@ class StatusLED(QAbstractButton):
return return
## ##
# Getters and Setters # Setters
## ##
def setState(self, theState): def setState(self, theState):
+4 -4
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""novelWriter Theme Class """novelWriter Theme and Icons Classes
novelWriter Theme Class novelWriter Theme and Icons Classs
=========================== ======================================
This class reads and store the main theme This class reads and stores the themes and the icons
File History: File History:
Created: 2019-05-18 [0.1.3] GuiTheme Created: 2019-05-18 [0.1.3] GuiTheme
+42 -25
View File
@@ -62,7 +62,9 @@ class GuiMain(QMainWindow):
self.mainConf = nw.CONFIG self.mainConf = nw.CONFIG
self.threadPool = QThreadPool() self.threadPool = QThreadPool()
# Some runtime info useful for debugging # System Info
# ===========
logger.info("OS: %s" % self.mainConf.osType) logger.info("OS: %s" % self.mainConf.osType)
logger.info("Kernel: %s" % self.mainConf.kernelVer) logger.info("Kernel: %s" % self.mainConf.kernelVer)
logger.info("Host: %s" % self.mainConf.hostName) logger.info("Host: %s" % self.mainConf.hostName)
@@ -76,6 +78,9 @@ class GuiMain(QMainWindow):
self.mainConf.verPyString, self.mainConf.verPyHexVal) self.mainConf.verPyString, self.mainConf.verPyHexVal)
) )
# Core Classes
# ============
# Core Classes and settings # Core Classes and settings
self.theTheme = GuiTheme(self) self.theTheme = GuiTheme(self)
self.theProject = NWProject(self) self.theProject = NWProject(self)
@@ -89,7 +94,7 @@ class GuiMain(QMainWindow):
self.setWindowIcon(QIcon(self.mainConf.appIcon)) self.setWindowIcon(QIcon(self.mainConf.appIcon))
# Build the GUI # Build the GUI
################ # =============
# Main GUI Elements # Main GUI Elements
self.statusBar = GuiMainStatus(self) self.statusBar = GuiMainStatus(self)
@@ -106,7 +111,7 @@ class GuiMain(QMainWindow):
self.statusIcons = [] self.statusIcons = []
self.importIcons = [] self.importIcons = []
# Assemble Main Window # Project Tree View
self.treePane = QWidget() self.treePane = QWidget()
self.treeBox = QVBoxLayout() self.treeBox = QVBoxLayout()
self.treeBox.setContentsMargins(0, 0, 0, 0) self.treeBox.setContentsMargins(0, 0, 0, 0)
@@ -114,20 +119,24 @@ class GuiMain(QMainWindow):
self.treeBox.addWidget(self.treeMeta) self.treeBox.addWidget(self.treeMeta)
self.treePane.setLayout(self.treeBox) self.treePane.setLayout(self.treeBox)
# Splitter : Document Viewer / Document Meta
self.splitView = QSplitter(Qt.Vertical) self.splitView = QSplitter(Qt.Vertical)
self.splitView.addWidget(self.docViewer) self.splitView.addWidget(self.docViewer)
self.splitView.addWidget(self.viewMeta) self.splitView.addWidget(self.viewMeta)
self.splitView.setSizes(self.mainConf.getViewPanePos()) self.splitView.setSizes(self.mainConf.getViewPanePos())
# Splitter : Document Editor / Document Viewer
self.splitDocs = QSplitter(Qt.Horizontal) self.splitDocs = QSplitter(Qt.Horizontal)
self.splitDocs.addWidget(self.docEditor) self.splitDocs.addWidget(self.docEditor)
self.splitDocs.addWidget(self.splitView) self.splitDocs.addWidget(self.splitView)
# Splitter : Project Outlie / Outline Details
self.splitOutline = QSplitter(Qt.Vertical) self.splitOutline = QSplitter(Qt.Vertical)
self.splitOutline.addWidget(self.projView) self.splitOutline.addWidget(self.projView)
self.splitOutline.addWidget(self.projMeta) self.splitOutline.addWidget(self.projMeta)
self.splitOutline.setSizes(self.mainConf.getOutlinePanePos()) self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
# Main Tabs : Edirot / Outline
self.tabWidget = QTabWidget() self.tabWidget = QTabWidget()
self.tabWidget.setTabPosition(QTabWidget.East) self.tabWidget.setTabPosition(QTabWidget.East)
self.tabWidget.setStyleSheet("QTabWidget::pane {border: 0;}") self.tabWidget.setStyleSheet("QTabWidget::pane {border: 0;}")
@@ -135,6 +144,7 @@ class GuiMain(QMainWindow):
self.tabWidget.addTab(self.splitOutline, "Outline") self.tabWidget.addTab(self.splitOutline, "Outline")
self.tabWidget.currentChanged.connect(self._mainTabChanged) self.tabWidget.currentChanged.connect(self._mainTabChanged)
# Splitter : Project Tree / Main Tabs
xCM = self.mainConf.pxInt(4) xCM = self.mainConf.pxInt(4)
self.splitMain = QSplitter(Qt.Horizontal) self.splitMain = QSplitter(Qt.Horizontal)
self.splitMain.setContentsMargins(xCM, xCM, xCM, xCM) self.splitMain.setContentsMargins(xCM, xCM, xCM, xCM)
@@ -142,6 +152,7 @@ class GuiMain(QMainWindow):
self.splitMain.addWidget(self.tabWidget) self.splitMain.addWidget(self.tabWidget)
self.splitMain.setSizes(self.mainConf.getMainPanePos()) self.splitMain.setSizes(self.mainConf.getMainPanePos())
# Indices of All Splitter Widgets
self.idxTree = self.splitMain.indexOf(self.treePane) self.idxTree = self.splitMain.indexOf(self.treePane)
self.idxMain = self.splitMain.indexOf(self.tabWidget) self.idxMain = self.splitMain.indexOf(self.tabWidget)
self.idxEditor = self.splitDocs.indexOf(self.docEditor) self.idxEditor = self.splitDocs.indexOf(self.docEditor)
@@ -151,6 +162,7 @@ class GuiMain(QMainWindow):
self.idxTabEdit = self.tabWidget.indexOf(self.splitDocs) self.idxTabEdit = self.tabWidget.indexOf(self.splitDocs)
self.idxTabProj = self.tabWidget.indexOf(self.splitOutline) self.idxTabProj = self.tabWidget.indexOf(self.splitOutline)
# Splitter Behaviour
self.splitMain.setCollapsible(self.idxTree, False) self.splitMain.setCollapsible(self.idxTree, False)
self.splitMain.setCollapsible(self.idxMain, False) self.splitMain.setCollapsible(self.idxMain, False)
self.splitDocs.setCollapsible(self.idxEditor, False) self.splitDocs.setCollapsible(self.idxEditor, False)
@@ -158,10 +170,11 @@ class GuiMain(QMainWindow):
self.splitView.setCollapsible(self.idxViewDoc, False) self.splitView.setCollapsible(self.idxViewDoc, False)
self.splitView.setCollapsible(self.idxViewMeta, False) self.splitView.setCollapsible(self.idxViewMeta, False)
# Editor / Viewer Default State
self.splitView.setVisible(False) self.splitView.setVisible(False)
self.docEditor.closeSearch() self.docEditor.closeSearch()
# Build the Tree View # Initialise the Project Tree
self.treeView.itemSelectionChanged.connect(self._treeSingleClick) self.treeView.itemSelectionChanged.connect(self._treeSingleClick)
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick) self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
self.rebuildTree() self.rebuildTree()
@@ -172,13 +185,13 @@ class GuiMain(QMainWindow):
self.setStatusBar(self.statusBar) self.setStatusBar(self.statusBar)
# Finalise Initialisation # Finalise Initialisation
########################## # =======================
# Set Up Autosaving Project Timer # Set Up Auto-Save Project Timer
self.asProjTimer = QTimer() self.asProjTimer = QTimer()
self.asProjTimer.timeout.connect(self._autoSaveProject) self.asProjTimer.timeout.connect(self._autoSaveProject)
# Set Up Autosaving Document Timer # Set Up Auto-Save Document Timer
self.asDocTimer = QTimer() self.asDocTimer = QTimer()
self.asDocTimer.timeout.connect(self._autoSaveDocument) self.asDocTimer.timeout.connect(self._autoSaveDocument)
@@ -203,11 +216,13 @@ class GuiMain(QMainWindow):
# Check that config loaded fine # Check that config loaded fine
self.reportConfErr() self.reportConfErr()
# Initialise Main GUI
self.initMain() self.initMain()
self.asProjTimer.start() self.asProjTimer.start()
self.asDocTimer.start() self.asDocTimer.start()
self.statusBar.clearStatus() self.statusBar.clearStatus()
# Handle Windows Mode
self.showNormal() self.showNormal()
if self.mainConf.isFullScreen: if self.mainConf.isFullScreen:
self.toggleFullScreenMode() self.toggleFullScreenMode()
@@ -224,7 +239,7 @@ class GuiMain(QMainWindow):
self.showProjectLoadDialog() self.showProjectLoadDialog()
logger.debug("novelWriter is ready ...") logger.debug("novelWriter is ready ...")
self.statusBar.setStatus("novelWriter is ready ...") self.setStatus("novelWriter is ready ...")
return return
@@ -249,8 +264,7 @@ class GuiMain(QMainWindow):
## ##
def newProject(self, projData=None): def newProject(self, projData=None):
"""Create new project with a few default files and folders. """Create new project via the new project wizard.
The variable forceNew is used for testing.
""" """
if self.hasProject: if self.hasProject:
self.makeAlert( self.makeAlert(
@@ -293,7 +307,7 @@ class GuiMain(QMainWindow):
def closeProject(self, isYes=False): def closeProject(self, isYes=False):
"""Closes the project if one is open. isYes is passed on from """Closes the project if one is open. isYes is passed on from
the close application event so the user doesn't get prompted the close application event so the user doesn't get prompted
twice. twice to confirm.
""" """
if not self.hasProject: if not self.hasProject:
# There is no project loaded, everything OK # There is no project loaded, everything OK
@@ -302,7 +316,7 @@ class GuiMain(QMainWindow):
if not isYes: if not isYes:
msgBox = QMessageBox() msgBox = QMessageBox()
msgRes = msgBox.question( 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: if msgRes != QMessageBox.Yes:
return False return False
@@ -318,7 +332,7 @@ class GuiMain(QMainWindow):
if self.mainConf.askBeforeBackup: if self.mainConf.askBeforeBackup:
msgBox = QMessageBox() msgBox = QMessageBox()
msgRes = msgBox.question( msgRes = msgBox.question(
self, "Backup Project", "Backup current project?" self, "Backup Project", "Backup the current project?"
) )
if msgRes != QMessageBox.Yes: if msgRes != QMessageBox.Yes:
doBackup = False doBackup = False
@@ -433,6 +447,7 @@ class GuiMain(QMainWindow):
if self.theProject.projPath is None: if self.theProject.projPath is None:
projPath = self.selectProjectPath() projPath = self.selectProjectPath()
self.theProject.setProjectPath(projPath) self.theProject.setProjectPath(projPath)
if self.theProject.projPath is None: if self.theProject.projPath is None:
return False return False
@@ -454,6 +469,7 @@ class GuiMain(QMainWindow):
if self.docEditor.docChanged: if self.docEditor.docChanged:
self.saveDocument() self.saveDocument()
self.docEditor.clearEditor() self.docEditor.clearEditor()
return True return True
def openDocument(self, tHandle, tLine=None, changeFocus=True, doScroll=False): def openDocument(self, tHandle, tLine=None, changeFocus=True, doScroll=False):
@@ -469,6 +485,7 @@ class GuiMain(QMainWindow):
self.treeView.setSelectedHandle(tHandle, doScroll=doScroll) self.treeView.setSelectedHandle(tHandle, doScroll=doScroll)
else: else:
return False return False
return True return True
def openNextDocument(self, tHandle, wrapAround=False): def openNextDocument(self, tHandle, wrapAround=False):
@@ -546,6 +563,7 @@ class GuiMain(QMainWindow):
vPos[1] = bPos[1] - vPos[0] vPos[1] = bPos[1] - vPos[0]
self.splitDocs.setSizes(vPos) self.splitDocs.setSizes(vPos)
self.viewMeta.setVisible(self.mainConf.showRefPanel) self.viewMeta.setVisible(self.mainConf.showRefPanel)
self.docViewer.navigateTo(tAnchor) self.docViewer.navigateTo(tAnchor)
return True return True
@@ -697,9 +715,9 @@ class GuiMain(QMainWindow):
for nDone, tItem in enumerate(self.theProject.projTree): for nDone, tItem in enumerate(self.theProject.projTree):
if tItem is not None: if tItem is not None:
self.statusBar.setStatus("Indexing: '%s'" % tItem.itemName) self.setStatus("Indexing: '%s'" % tItem.itemName)
else: else:
self.statusBar.setStatus("Indexing: Unknown item") self.setStatus("Indexing: Unknown item")
if tItem is not None and tItem.itemType == nwItemType.FILE: if tItem is not None and tItem.itemType == nwItemType.FILE:
logger.verbose("Scanning: %s" % tItem.itemName) logger.verbose("Scanning: %s" % tItem.itemName)
@@ -717,7 +735,7 @@ class GuiMain(QMainWindow):
self.treeView.projectWordCount() self.treeView.projectWordCount()
tEnd = time() 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() self.docEditor.updateTagHighLighting()
qApp.restoreOverrideCursor() qApp.restoreOverrideCursor()
@@ -754,7 +772,8 @@ class GuiMain(QMainWindow):
def showProjectLoadDialog(self): def showProjectLoadDialog(self):
"""Opens the projects dialog for selecting either existing """Opens the projects dialog for selecting either existing
projects from a cache of recently opened projects, or provide a 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 = GuiProjectLoad(self)
dlgProj.exec_() dlgProj.exec_()
@@ -767,7 +786,7 @@ class GuiMain(QMainWindow):
return True return True
def showNewProjectDialog(self): 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 = GuiProjectWizard(self)
newProj.exec_() newProj.exec_()
@@ -865,8 +884,7 @@ class GuiMain(QMainWindow):
def makeAlert(self, theMessage, theLevel=nwAlert.INFO): def makeAlert(self, theMessage, theLevel=nwAlert.INFO):
"""Alert both the user and the logger at the same time. Message """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 can be either a string or an array of strings.
0 = info, 1 = warning, and 2 = error.
""" """
if isinstance(theMessage, list): if isinstance(theMessage, list):
popMsg = "<br>".join(theMessage) popMsg = "<br>".join(theMessage)
@@ -955,7 +973,7 @@ class GuiMain(QMainWindow):
return True return True
def setFocus(self, paneNo): 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: if paneNo == 1:
self.treeView.setFocus() self.treeView.setFocus()
@@ -1236,9 +1254,9 @@ class GuiMain(QMainWindow):
return return
def _treeKeyPressReturn(self): def _treeKeyPressReturn(self):
"""The user pressed return an item in the tree. If it is a file, """The user pressed return on an item in the tree. If it is a
we open it. Otherwise, we do nothing. Pressing return does not file, we open it. Otherwise, we do nothing. Pressing return does
change focus to the editor as double click does. not change focus to the editor as double click does.
""" """
tHandle = self.treeView.getSelectedHandle() tHandle = self.treeView.getSelectedHandle()
logger.verbose("User pressed return on tree item with handle %s" % tHandle) logger.verbose("User pressed return on tree item with handle %s" % tHandle)
@@ -1257,7 +1275,6 @@ class GuiMain(QMainWindow):
""" """
if self.docEditor.docSearch.isVisible(): if self.docEditor.docSearch.isVisible():
self.docEditor.closeSearch() self.docEditor.closeSearch()
return
elif self.isFocusMode: elif self.isFocusMode:
self.toggleFocusMode() self.toggleFocusMode()
return return
+3
View File
@@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
+45 -2
View File
@@ -1,11 +1,54 @@
[metadata] [metadata]
license_files = LICENSE.md name = novelWriter
version = attr: nw.__version__ 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] [bdist_wheel]
universal = 0 universal = 0
[flake8] [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 max-line-length = 99
exclude = docs/* exclude = docs/*
+242 -78
View File
@@ -1,30 +1,44 @@
#!/usr/bin/env python3 #!/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 os
import sys import sys
import shutil
import subprocess import subprocess
import setuptools import setuptools
## # =============================================================================================== #
# Build the Package # Qt Assistant Documentation Builder
## # =============================================================================================== #
buildDocs = False def buildQtDocs():
buildSample = False """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: Depends on packages:
buildDocs = True * pip install sphinx
sys.argv.remove("qthelp") * pip install sphinx-rtd-theme
* pip install sphinxcontrib-qthelp
if "sample" in sys.argv:
buildSample = True
sys.argv.remove("sample")
##
# Qt Assistant Documentation
##
if buildDocs:
It also requires the qhelpgenerator to be available on the system.
"""
buildDir = os.path.join("docs", "build", "qthelp") buildDir = os.path.join("docs", "build", "qthelp")
helpDir = os.path.join("nw", "assets", "help") helpDir = os.path.join("nw", "assets", "help")
@@ -41,14 +55,14 @@ if buildDocs:
try: try:
subprocess.call(["make", "-C", "docs", "qthelp"]) subprocess.call(["make", "-C", "docs", "qthelp"])
except Exception as e: except Exception as e:
print("Failed with error:") print("QtHelp Build Error:")
print(str(e)) print(str(e))
buildFail = True buildFail = True
try: try:
subprocess.call(["qhelpgenerator", os.path.join(buildDir, inFile)]) subprocess.call(["qhelpgenerator", os.path.join(buildDir, inFile)])
except Exception as e: except Exception as e:
print("Failed with error:") print("QtHelp Build Error:")
print(str(e)) print(str(e))
buildFail = True buildFail = True
@@ -56,7 +70,7 @@ if buildDocs:
try: try:
os.mkdir(helpDir) os.mkdir(helpDir)
except Exception as e: except Exception as e:
print("Failed with error:") print("QtHelp Build Error:")
print(str(e)) print(str(e))
buildFail = True 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, outFile), os.path.join(helpDir, outFile))
os.rename(os.path.join(buildDir, datFile), os.path.join(helpDir, datFile)) os.rename(os.path.join(buildDir, datFile), os.path.join(helpDir, datFile))
except Exception as e: except Exception as e:
print("Failed with error:") print("QtHelp Build Error:")
print(str(e)) print(str(e))
buildFail = True buildFail = True
@@ -80,11 +94,20 @@ if buildDocs:
print("Documentation build: OK") print("Documentation build: OK")
print("") print("")
## return
# Sample Project ZIP file
##
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" srcSample = "sample"
dstSample = os.path.join("nw", "assets", "sample.zip") dstSample = os.path.join("nw", "assets", "sample.zip")
@@ -96,8 +119,10 @@ if buildSample:
from zipfile import ZipFile from zipfile import ZipFile
with ZipFile(dstSample, "w") as zipObj: with ZipFile(dstSample, "w") as zipObj:
print("Compressing: nwProject.nwx")
zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx") zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx")
for docFile in os.listdir(os.path.join(srcSample, "content")): for docFile in os.listdir(os.path.join(srcSample, "content")):
print("Compressing: content/%s" % docFile)
srcDoc = os.path.join(srcSample, "content", docFile) srcDoc = os.path.join(srcSample, "content", docFile)
zipObj.write(srcDoc, "content/"+docFile) zipObj.write(srcDoc, "content/"+docFile)
@@ -105,58 +130,197 @@ if buildSample:
print("Error: Could not find sample project source directory.") print("Error: Could not find sample project source directory.")
sys.exit(1) sys.exit(1)
if len(sys.argv) == 1: print("")
# Nothing more to do print("Built file: %s" % dstSample)
sys.exit(0) print("")
## return
# Build the Package
##
# Read content from files # =============================================================================================== #
with open("README.md", "r") as inFile: # Create Launcher
longDescription = inFile.read() # =============================================================================================== #
setuptools.setup( def makeLauncherLinux():
name = "novelWriter", """Will attempt to install icons and make a launcher.
# version = __version__, # Set in setup.cfg """
author = "Veronica Berglyd Olsen", print("")
author_email = "code@vkbo.net", print("Creating Launcher")
description = "A markdown-like document editor for writing novels", print("=================")
long_description = longDescription, print("")
long_description_content_type = "text/markdown",
license = "GNU General Public License v3", exOpts = []
url = "https://novelwriter.io",
entry_points = { testExec = shutil.which("novelWriter")
"console_scripts" : ["novelWriter-cli=nw:main"], if testExec is not None:
"gui_scripts" : ["novelWriter=nw:main"], exOpts.append(testExec)
},
packages = setuptools.find_packages(exclude=["docs", "tests", "sample"]), testExec = shutil.which("novelwriter")
include_package_data = True, if testExec is not None:
package_data = {"": ["*.conf"]}, exOpts.append(testExec)
project_urls = {
"Bug Tracker": "https://github.com/vkbo/novelWriter/issues", testExec = os.path.join(os.getcwd(), "novelWriter.py")
"Documentation": "https://github.com/vkbo/novelWriter/issues", if os.path.isfile(testExec):
"Source Code": "https://github.com/vkbo/novelWriter", exOpts.append(testExec)
},
classifiers = [ useExec = ""
"Programming Language :: Python :: 3 :: Only", nOpts = len(exOpts)
"Programming Language :: Python :: 3.6", if nOpts == 0:
"Programming Language :: Python :: 3.7", print("Error: No executables for novelWriter found.")
"Programming Language :: Python :: 3.8", sys.exit(1)
"Programming Language :: Python :: 3.9", elif nOpts == 1:
"Programming Language :: Python :: Implementation :: CPython", useExec = exOpts[0]
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)", else:
"Development Status :: 4 - Beta", print("Found multiple novelWriter executables:")
"Operating System :: OS Independent", print("")
"Intended Audience :: End Users/Desktop", for iExec, anExec in enumerate(exOpts):
"Natural Language :: English", print(" [%d] %s" % (iExec, anExec))
"Topic :: Text Editors", print("")
], intVal = int(input("Please select which novelWriter executable to use: "))
python_requires = ">=3.6", print("")
install_requires = [
"pyqt5>=5.2.1", if intVal >= 0 and intVal < nOpts:
"lxml>=4.2.0", useExec = exOpts[intVal]
"pyenchant>=3.0.0", 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
+47
View File
@@ -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
```

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Before

Width:  |  Height:  |  Size: 683 B

After

Width:  |  Height:  |  Size: 683 B

Before

Width:  |  Height:  |  Size: 660 B

After

Width:  |  Height:  |  Size: 660 B

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 9.7 KiB

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info"> <mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-novelwriter-project"> <mime-type type="application/x-novelwriter-project">
<comment>novelWriter Project </comment> <comment>novelWriter Project</comment>
<sub-class-of type="application/xml"/> <sub-class-of type="application/xml"/>
<glob pattern="*.nwx"/> <glob pattern="*.nwx"/>
</mime-type> </mime-type>
+52
View File
@@ -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
+5 -5
View File
@@ -31,13 +31,13 @@ def testItemSettersSimple(nwDummy):
# Parent # Parent
theItem.setParent(None) theItem.setParent(None)
assert theItem.parHandle is None assert theItem.itemParent is None
theItem.setParent(123) theItem.setParent(123)
assert theItem.parHandle is None assert theItem.itemParent is None
theItem.setParent("0123456789abcdef") theItem.setParent("0123456789abcdef")
assert theItem.parHandle is None assert theItem.itemParent is None
theItem.setParent("0123456789abc") theItem.setParent("0123456789abc")
assert theItem.parHandle == "0123456789abc" assert theItem.itemParent == "0123456789abc"
# Order # Order
theItem.setOrder(None) theItem.setOrder(None)
@@ -227,7 +227,7 @@ def testItemXMLPackUnpack(nwDummy):
# Unpack # Unpack
assert theItem.unpackXML(xContent[0]) assert theItem.unpackXML(xContent[0])
assert theItem.itemHandle == "0123456789abc" assert theItem.itemHandle == "0123456789abc"
assert theItem.parHandle == "0123456789abc" assert theItem.itemParent == "0123456789abc"
assert theItem.itemOrder == 1 assert theItem.itemOrder == 1
assert theItem.isExpanded assert theItem.isExpanded
assert theItem.paraCount == 3 assert theItem.paraCount == 3
+2 -2
View File
@@ -521,7 +521,7 @@ def testProjectOrphanedFiles(nwDummy, nwLipsum):
assert oItem is not None assert oItem is not None
assert oItem.itemName == "Mars" assert oItem.itemName == "Mars"
assert oItem.itemHandle == "636b6aa9b697b" assert oItem.itemHandle == "636b6aa9b697b"
assert oItem.parHandle is None assert oItem.itemParent is None
assert oItem.itemClass == nwItemClass.WORLD assert oItem.itemClass == nwItemClass.WORLD
assert oItem.itemType == nwItemType.FILE assert oItem.itemType == nwItemType.FILE
assert oItem.itemLayout == nwItemLayout.NOTE assert oItem.itemLayout == nwItemLayout.NOTE
@@ -531,7 +531,7 @@ def testProjectOrphanedFiles(nwDummy, nwLipsum):
assert oItem is not None assert oItem is not None
assert oItem.itemName == "Orphaned File 1" assert oItem.itemName == "Orphaned File 1"
assert oItem.itemHandle == "736b6aa9b697b" assert oItem.itemHandle == "736b6aa9b697b"
assert oItem.parHandle is None assert oItem.itemParent is None
assert oItem.itemClass == nwItemClass.NO_CLASS assert oItem.itemClass == nwItemClass.NO_CLASS
assert oItem.itemType == nwItemType.FILE assert oItem.itemType == nwItemType.FILE
assert oItem.itemLayout == nwItemLayout.NO_LAYOUT assert oItem.itemLayout == nwItemLayout.NO_LAYOUT
+1
View File
@@ -61,6 +61,7 @@ def testNumberWords():
assert numberToWord(21, "en") == "Twenty-One" assert numberToWord(21, "en") == "Twenty-One"
assert numberToWord(29, "en") == "Twenty-Nine" assert numberToWord(29, "en") == "Twenty-Nine"
assert numberToWord(42, "en") == "Forty-Two" assert numberToWord(42, "en") == "Forty-Two"
assert numberToWord(114, "en") == "One Hundred Fourteen"
assert numberToWord(142, "en") == "One Hundred Forty-Two" assert numberToWord(142, "en") == "One Hundred Forty-Two"
assert numberToWord(999, "en") == "Nine Hundred Ninety-Nine" assert numberToWord(999, "en") == "Nine Hundred Ninety-Nine"