Merge branch 'main' into release_1.0rc1
@@ -25,5 +25,5 @@ jobs:
|
||||
flake8 tests --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
- name: Coding Style Violations
|
||||
run: |
|
||||
flake8 nw --count --max-line-length=99 --ignore E203,E221,E226,E241,E251,E261,E266,E302,E305 --show-source --statistics
|
||||
flake8 tests --count --max-line-length=99 --ignore E203,E221,E226,E241,E251,E261,E266,E302,E305 --show-source --statistics
|
||||
flake8 nw --count --max-line-length=99 --ignore E203,E221,E226,E228,E241,E251,E261,E266,E302,E305 --show-source --statistics
|
||||
flake8 tests --count --max-line-length=99 --ignore E203,E221,E226,E228,E241,E251,E261,E266,E302,E305 --show-source --statistics
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
/deploy/
|
||||
*.spec
|
||||
*.egg-info
|
||||
setup.iss
|
||||
|
||||
# Documentation
|
||||
/docs/build/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
include LICENSE.md
|
||||
recursive-include assets *
|
||||
recursive-include setup *
|
||||
recursive-include nw/assets *
|
||||
recursive-include sample *.nwx *.nwd
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
[](https://pypi.org/project/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
|
||||
smaller plain text documents. It uses its own flavour of Markdown that supports a meta data syntax
|
||||
@@ -52,63 +52,65 @@ in principle work fine on other operating systems as well as long as dependencie
|
||||
tests are run on the latest versions of Ubuntu Linux, Windows Server and macOS.
|
||||
|
||||
|
||||
## Installing and Running
|
||||
# Installing and Running
|
||||
|
||||
You can runt novelWriter either from a downloaded copy of the source code, or by running:
|
||||
novelWriter is available on [pypi.org](https://pypi.org/project/novelWriter/), and can be installed with:
|
||||
```bash
|
||||
pip install novelwriter
|
||||
```
|
||||
**Note:** On some systems you must use `pip3` instead for the Python 3 version.
|
||||
|
||||
You can update novelWriter to the latest version by running:
|
||||
To upgrade an existing installation, use:
|
||||
```bash
|
||||
pip install --upgrade novelwriter
|
||||
```
|
||||
|
||||
The application can then be started with one of the commands, depending on your Python configuration:
|
||||
Dependencies are installed automatically, but can generally be installed with:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
Below are some brief instructions on how to get started on different operating systems.
|
||||
|
||||
|
||||
## Linux
|
||||
|
||||
Either download the source, or install with pip.
|
||||
|
||||
If you run from source, install the dependencies via pip, or directly from the OS repo.
|
||||
There are very few dependencies, and they should be available in the standard repo.
|
||||
The Python packages needed are `pyqt5`, `lxml` and `pyenchant`.
|
||||
|
||||
### Installing from Source
|
||||
|
||||
You can also install novelWriter from source with:
|
||||
```bash
|
||||
python3 setup.py sample
|
||||
sudo python3 setup.py install
|
||||
sudo python3 setup.py launcher
|
||||
```
|
||||
|
||||
The last line will install the application icons and set up a launcher for novelWriter.
|
||||
The method uses hardcoded paths, so it may or may not work for your Linux distro.
|
||||
If you have any issues, please submit a ticket so the script can be tuned.
|
||||
|
||||
The script may prompt you to choose which executable to configure if it finds more than one.
|
||||
|
||||
### Running from Source
|
||||
|
||||
If you want to run directly from the source, the application can be started with:
|
||||
```bash
|
||||
./novelWriter.py
|
||||
python novelWriter.py
|
||||
python3 novelWriter.py
|
||||
```
|
||||
|
||||
It also takes a few parameters for debugging and such, which can be listed with the switch `--help`.
|
||||
The `--info`, `--debug` or `--verbose` flags are particularly useful for increasing logging output
|
||||
for debugging.
|
||||
|
||||
You can also provide a path to a folder containing a novelWriter project as the last parameter.
|
||||
|
||||
|
||||
### Launcher and Icons
|
||||
|
||||
In the root assets folder there are icons and scripts and a template for setting up a launcher on
|
||||
Gnome desktops. You may need to modify those scripts slightly, but as they are, they work on Debian
|
||||
and Ubuntu. For other operating systems, please consult your operating system documentation for how
|
||||
to make those. Feel free to submit more if you are able to make them.
|
||||
|
||||
|
||||
## Package Dependencies
|
||||
|
||||
It is recommended that novelWriter runs with Qt 5.10 or later, and requires Python 3.6 or later.
|
||||
Minimum version of Qt is 5.2.
|
||||
|
||||
|
||||
### Linux
|
||||
|
||||
Generally, dependencies can be installed via `pip` with:
|
||||
You can also create a launcher for running directly from source with:
|
||||
```bash
|
||||
pip3 install -r requirements.txt
|
||||
sudo python3 setup.py launcher
|
||||
```
|
||||
|
||||
You can also install the packages from the distro's own package manager.
|
||||
For the apt package manager on Debian/Ubuntu systems, the following Python3 packages are needed:
|
||||
|
||||
* `python3-pyqt5` for the GUI
|
||||
* `python3-lxml` for writing project files
|
||||
* `python3-enchant` for better spell checking (optional)
|
||||
For more install options, see [Build and Install novelWriter](setup/README.md).
|
||||
|
||||
|
||||
### macOS
|
||||
## macOS
|
||||
|
||||
These instructions assume you're using brew, and have Python and pip set up.
|
||||
If not, see the [brew docs](https://docs.brew.sh/Homebrew-and-Python) for help.
|
||||
@@ -126,11 +128,16 @@ It comes with a lot of default dictionaries.
|
||||
brew install enchant
|
||||
```
|
||||
|
||||
## Windows
|
||||
|
||||
### Windows
|
||||
On Windows, you may first need to install Python.
|
||||
See the [python.org](https://www.python.org/) website for download packages.
|
||||
It is recommended that you install the latest version of Python 3.8.
|
||||
|
||||
On Windows, the `pip install` command is generally sufficient to install everything you need.
|
||||
That should also install the Qt libraries and the spell check dictionary dependencies.
|
||||
To install dependencies, run:
|
||||
```bash
|
||||
pip install --user -r requirements.txt
|
||||
```
|
||||
|
||||
**Note:** On Windows, make sure Python3 is in your PATH if you want to launch novelWriter from
|
||||
command line. You can also right click the `novelWriter.py` file, create a shortcut, then right
|
||||
@@ -142,8 +149,11 @@ It should look something like this:
|
||||
C:\...\AppData\Local\Programs\Python\Python38\python.exe novelWriter.py
|
||||
```
|
||||
|
||||
You can also run the `make.py` script to generate a single executable, or an installer.
|
||||
See [Build and Install novelWriter](setup/README.md) for more details.
|
||||
|
||||
### Package Versions
|
||||
|
||||
## Package Versions
|
||||
|
||||
Exporting to Markdown requires PyQt/Qt 5.14. There are no known minimum for `lxml`, but the code
|
||||
was originally written with 4.2. The optional spell check library must be at least 3.0.0 to work
|
||||
@@ -155,8 +165,15 @@ checker, but more can be added to the `nw/assets/dict` folder. See the [README](
|
||||
file in that folder for how to generate more dictionaries. Note that the difflib-based option is
|
||||
both slow and limited.
|
||||
|
||||
## Debugging
|
||||
|
||||
## Key Features
|
||||
If you need to debug novelWriter, you must run it from command line.
|
||||
It takes a few parameters, which can be listed with the switch `--help`.
|
||||
The `--info`, `--debug` or `--verbose` flags are particularly useful for increasing logging output
|
||||
for debugging.
|
||||
|
||||
|
||||
# Key Features
|
||||
|
||||
Some features of novelWriter are listed below. Consult the documentation for more information.
|
||||
|
||||
|
||||
@@ -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/
|
||||
@@ -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/*
|
||||
@@ -12,7 +12,6 @@
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath("."))
|
||||
import os
|
||||
|
||||
@@ -140,7 +140,7 @@ encountered. To list all options, run:
|
||||
|
||||
python novelWriter.py --help
|
||||
|
||||
There are also a couple of install scripts in the assets folder which will assist in setting up a
|
||||
There are also a couple of install scripts in the setup folder which will assist in setting up a
|
||||
launch icon and the novelWriter project file mimetype for Gnome desktops on Linux. Currently,
|
||||
there's one script for Debian and one for Ubuntu.
|
||||
|
||||
|
||||
@@ -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("")
|
||||
@@ -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
|
||||
@@ -35,6 +35,28 @@ from PyQt5.QtWidgets import QApplication, QErrorMessage
|
||||
from nw.error import exceptionHandler
|
||||
from nw.config import Config
|
||||
|
||||
#
|
||||
# Version Scheme
|
||||
# ================
|
||||
# Generally follows PEP 440
|
||||
# Hex Version:
|
||||
# - Digit 1,2 : Major Version (01, 02, 03)
|
||||
# = Digit 3,4 : Minor Version (01, 09, 10, 99)
|
||||
# - Digit 5,6 : Patch Version (01, 09, 10, 99)
|
||||
# = Digit 7 : Release Type (a: aplha, b: beta, c: candidate, f: final)
|
||||
# - Digit 8 : Release Number (0-9)
|
||||
#
|
||||
# Example : Full Short Description
|
||||
# -------------------------------------------------------------------------
|
||||
# 0x010200a0 : 1.2-alpha0 1.2a0 Can be used for the dev branch
|
||||
# 0x010200a1 : 1.2-alpha1 1.2a1 First alpha release
|
||||
# 0x010200b1 : 1.2-beta1 1.2b1 First beta release
|
||||
# 0x010200c1 : 1.2-rc1 1.2rc1 First release candidate
|
||||
# 0x010200f0 : 1.2 1.2 Final release
|
||||
# 0x010200f1 : 1.2-post1 1.2.post1 Post release, but not a code patch!
|
||||
# 0x010201f0 : 1.2.1 1.2.1 Patch release
|
||||
#
|
||||
|
||||
__package__ = "nw"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__copyright__ = "Copyright 2018–2020, Veronica Berglyd Olsen"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Language Codes
|
||||
"""novelWriter ISO Codes
|
||||
|
||||
novelWriter – Language Codes
|
||||
==============================
|
||||
novelWriter – ISO Codes
|
||||
=========================
|
||||
Handles translating language codes to language names
|
||||
|
||||
File History:
|
||||
|
||||
@@ -67,7 +67,9 @@ class NWDoc():
|
||||
|
||||
def openDocument(self, tHandle, showStatus=True, isOrphan=False):
|
||||
"""Open a document from handle, capturing potential file system
|
||||
errors and parse meta data.
|
||||
errors and parse meta data. If the document doesn't exist on
|
||||
disk, return an empty string. If something went wrong, return
|
||||
None.
|
||||
"""
|
||||
if not isHandle(tHandle):
|
||||
return None
|
||||
@@ -120,13 +122,13 @@ class NWDoc():
|
||||
logger.verbose("DocMeta: '%s'" % self._docMeta)
|
||||
|
||||
if showStatus and not isOrphan:
|
||||
self.theParent.statusBar.setStatus("Opened Document: %s" % self._theItem.itemName)
|
||||
self.theParent.setStatus("Opened Document: %s" % self._theItem.itemName)
|
||||
|
||||
return theText
|
||||
|
||||
def saveDocument(self, docText):
|
||||
"""Save the document via temp file in case of save failure, and
|
||||
in any case keep a backup of the file.
|
||||
"""Save the document. The file is saved via a temp file in case
|
||||
of save failure. Returns True if successful, False if not.
|
||||
"""
|
||||
if self._docHandle is None:
|
||||
return False
|
||||
@@ -139,6 +141,7 @@ class NWDoc():
|
||||
docPath = os.path.join(self.theProject.projContent, docFile)
|
||||
docTemp = os.path.join(self.theProject.projContent, docFile+"~")
|
||||
|
||||
# DocMeta line
|
||||
if self._theItem is None:
|
||||
docMeta = ""
|
||||
else:
|
||||
@@ -166,12 +169,12 @@ class NWDoc():
|
||||
os.unlink(docPath)
|
||||
os.rename(docTemp, docPath)
|
||||
|
||||
self.theParent.statusBar.setStatus("Saved Document: %s" % self._theItem.itemName)
|
||||
self.theParent.setStatus("Saved Document: %s" % self._theItem.itemName)
|
||||
|
||||
return True
|
||||
|
||||
def deleteDocument(self, tHandle):
|
||||
"""Permanently delete a document source file and its backups
|
||||
"""Permanently delete a document source file and related files
|
||||
from the project data folder.
|
||||
"""
|
||||
if not isHandle(tHandle):
|
||||
|
||||
@@ -279,7 +279,7 @@ class NWIndex():
|
||||
if theItem.itemLayout == nwItemLayout.NO_LAYOUT:
|
||||
logger.info("Not indexing no-layout item %s" % tHandle)
|
||||
return False
|
||||
if theItem.parHandle is None:
|
||||
if theItem.itemParent is None:
|
||||
logger.info("Not indexing orphaned item %s" % tHandle)
|
||||
return False
|
||||
|
||||
@@ -288,7 +288,7 @@ class NWIndex():
|
||||
self.textCounts[tHandle] = [cC, wC, pC]
|
||||
|
||||
# If the file is archived or trashed, we don't index the file itself
|
||||
if self.theProject.projTree.isTrashRoot(theItem.parHandle):
|
||||
if self.theProject.projTree.isTrashRoot(theItem.itemParent):
|
||||
logger.info("Not indexing trash item %s" % tHandle)
|
||||
return False
|
||||
if theRoot.itemClass == nwItemClass.ARCHIVE:
|
||||
@@ -583,7 +583,7 @@ class NWIndex():
|
||||
|
||||
def getCounts(self, tHandle, sTitle=None):
|
||||
"""Returns the counts for a file, or a section of a file
|
||||
starting at title nTitle.
|
||||
starting at title sTitle if it is provided.
|
||||
"""
|
||||
cC = 0
|
||||
wC = 0
|
||||
|
||||
@@ -42,7 +42,7 @@ class NWItem():
|
||||
|
||||
self.itemName = ""
|
||||
self.itemHandle = None
|
||||
self.parHandle = None
|
||||
self.itemParent = None
|
||||
self.itemOrder = None
|
||||
self.itemType = nwItemType.NO_TYPE
|
||||
self.itemClass = nwItemClass.NO_CLASS
|
||||
@@ -70,7 +70,7 @@ class NWItem():
|
||||
xPack = etree.SubElement(xParent, "item", attrib={
|
||||
"handle" : str(self.itemHandle),
|
||||
"order" : str(self.itemOrder),
|
||||
"parent" : str(self.parHandle),
|
||||
"parent" : str(self.itemParent),
|
||||
})
|
||||
self._subPack(xPack, "name", text=str(self.itemName))
|
||||
self._subPack(xPack, "type", text=str(self.itemType.name))
|
||||
@@ -85,6 +85,7 @@ class NWItem():
|
||||
self._subPack(xPack, "cursorPos", text=str(self.cursorPos), none=False)
|
||||
else:
|
||||
self._subPack(xPack, "expanded", text=str(self.isExpanded))
|
||||
|
||||
return
|
||||
|
||||
def unpackXML(self, xItem):
|
||||
@@ -101,7 +102,7 @@ class NWItem():
|
||||
return False
|
||||
|
||||
if "parent" in xItem.attrib:
|
||||
self.parHandle = xItem.attrib["parent"]
|
||||
self.itemParent = xItem.attrib["parent"]
|
||||
|
||||
setMap = {
|
||||
"name" : self.setName,
|
||||
@@ -131,9 +132,11 @@ class NWItem():
|
||||
"""
|
||||
if not none and (text is None or text == "None"):
|
||||
return None
|
||||
xSub = etree.SubElement(xParent, name, attrib=attrib)
|
||||
xAttr = {} if attrib is None else attrib
|
||||
xSub = etree.SubElement(xParent, name, attrib=xAttr)
|
||||
if text is not None:
|
||||
xSub.text = text
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
@@ -162,14 +165,14 @@ class NWItem():
|
||||
"""Set the parent handle, and ensure that it is valid.
|
||||
"""
|
||||
if theParent is None:
|
||||
self.parHandle = None
|
||||
self.itemParent = None
|
||||
elif isinstance(theParent, str):
|
||||
if len(theParent) == 13:
|
||||
self.parHandle = theParent
|
||||
self.itemParent = theParent
|
||||
else:
|
||||
self.parHandle = None
|
||||
self.itemParent = None
|
||||
else:
|
||||
self.parHandle = None
|
||||
self.itemParent = None
|
||||
return
|
||||
|
||||
def setOrder(self, theOrder):
|
||||
|
||||
@@ -782,7 +782,7 @@ class NWProject():
|
||||
"""Create a zip file of the entire project.
|
||||
"""
|
||||
logger.info("Backing up project")
|
||||
self.theParent.statusBar.setStatus("Backing up project ...")
|
||||
self.theParent.setStatus("Backing up project ...")
|
||||
|
||||
if self.mainConf.backupPath is None or self.mainConf.backupPath == "":
|
||||
self.theParent.makeAlert((
|
||||
@@ -847,7 +847,7 @@ class NWProject():
|
||||
)
|
||||
return False
|
||||
|
||||
self.theParent.statusBar.setStatus("Project backed up to '%s.zip'" % baseName)
|
||||
self.theParent.setStatus("Project backed up to '%s.zip'" % baseName)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1139,16 +1139,16 @@ class NWProject():
|
||||
# Technically a bug since treeOrder is built from the
|
||||
# same data as projTree
|
||||
continue
|
||||
elif tItem.parHandle is None:
|
||||
elif tItem.itemParent is None:
|
||||
# Item is a root, or already been identified as an
|
||||
# orphaned item
|
||||
sentItems.append(tHandle)
|
||||
yield tItem
|
||||
elif tItem.parHandle in sentItems:
|
||||
elif tItem.itemParent in sentItems:
|
||||
# Item's parent has been sent, so all is fine
|
||||
sentItems.append(tHandle)
|
||||
yield tItem
|
||||
elif tItem.parHandle in iterItems:
|
||||
elif tItem.itemParent in iterItems:
|
||||
# Item's parent exists, but hasn't been sent yet, so add
|
||||
# it again to the end
|
||||
logger.warning("Item %s found before its parent" % tHandle)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Spell Check Wrapper
|
||||
"""novelWriter Spell Check Classes
|
||||
|
||||
novelWriter – Spell Check Wrapper
|
||||
novelWriter – Spell Check Classes
|
||||
===================================
|
||||
Wrapper class for spell checking
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ class ToHtml(Tokenizer):
|
||||
|
||||
def __init__(self, theProject, theParent):
|
||||
Tokenizer.__init__(self, theProject, theParent)
|
||||
|
||||
self.genMode = self.M_EXPORT
|
||||
self.cssStyles = True
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Word Counter
|
||||
"""novelWriter Various Tools
|
||||
|
||||
novelWriter – Word Counter
|
||||
============================
|
||||
Simple word counter
|
||||
novelWriter – Various Tools
|
||||
=============================
|
||||
Various core tool functions
|
||||
|
||||
File History:
|
||||
Created: 2019-04-22 [0.0.1] countWords
|
||||
Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN
|
||||
Merged: 2020-05-08 [0.4.5] All of the above into this file
|
||||
Created: 2019-04-22 [0.0.1] countWords
|
||||
Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN
|
||||
Merged: 2020-05-08 [0.4.5] All of the above into this file
|
||||
Created: 2020-07-05 [0.10.0] numberToRoman
|
||||
|
||||
This file is a part of novelWriter
|
||||
Copyright 2018–2020, Veronica Berglyd Olsen
|
||||
@@ -136,7 +137,6 @@ def numberToWord(numVal, theLanguage):
|
||||
def _numberToWordEN(numVal):
|
||||
"""Convert numbers to English words.
|
||||
"""
|
||||
numWord = ""
|
||||
oneWord = ""
|
||||
tenWord = ""
|
||||
hunWord = ""
|
||||
@@ -145,8 +145,8 @@ def _numberToWordEN(numVal):
|
||||
return "Zero"
|
||||
|
||||
oneVal = numVal % 10
|
||||
tenVal = (numVal-oneVal) % 100
|
||||
hunVal = (numVal-tenVal-oneVal) % 1000
|
||||
tenVal = (numVal - oneVal) % 100
|
||||
hunVal = (numVal - tenVal - oneVal) % 1000
|
||||
|
||||
theHundreds = {
|
||||
100: "One Hundred", 200: "Two Hundred", 300: "Three Hundred",
|
||||
@@ -167,18 +167,18 @@ def _numberToWordEN(numVal):
|
||||
}
|
||||
|
||||
hunWord = theHundreds.get(hunVal, "")
|
||||
tenWord = theTens.get(tenVal, "")
|
||||
if tenVal == 10:
|
||||
oneWord = theTeens.get(oneVal, "")
|
||||
numWord = ("%s %s" % (hunWord, oneWord)).strip()
|
||||
return f"{hunWord} {oneWord}".strip()
|
||||
else:
|
||||
oneWord = theOnes.get(oneVal, "")
|
||||
if tenVal == 0:
|
||||
numWord = ("%s %s" % (hunWord, oneWord)).strip()
|
||||
return f"{hunWord} {oneWord}".strip()
|
||||
else:
|
||||
tenWord = theTens.get(tenVal, "")
|
||||
if oneVal == 0:
|
||||
numWord = ("%s %s" % (hunWord, tenWord)).strip()
|
||||
return f"{hunWord} {tenWord}".strip()
|
||||
else:
|
||||
numWord = ("%s %s-%s" % (hunWord, tenWord, oneWord)).strip()
|
||||
return f"{hunWord} {tenWord}-{oneWord}".strip()
|
||||
|
||||
return numWord
|
||||
return ""
|
||||
|
||||
@@ -134,7 +134,7 @@ class NWTree():
|
||||
for xItem in xContent:
|
||||
nwItem = NWItem(self.theProject)
|
||||
if nwItem.unpackXML(xItem):
|
||||
self.append(nwItem.itemHandle, nwItem.parHandle, nwItem)
|
||||
self.append(nwItem.itemHandle, nwItem.itemParent, nwItem)
|
||||
nwItem.saveInitialCount()
|
||||
|
||||
return True
|
||||
@@ -261,10 +261,10 @@ class NWTree():
|
||||
tItem = self.__getitem__(tHandle)
|
||||
if tItem is not None:
|
||||
for i in range(nwConst.maxDepth + 1):
|
||||
if tItem.parHandle is None:
|
||||
if tItem.itemParent is None:
|
||||
return tItem
|
||||
else:
|
||||
tHandle = tItem.parHandle
|
||||
tHandle = tItem.itemParent
|
||||
tItem = self.__getitem__(tHandle)
|
||||
return None
|
||||
|
||||
@@ -279,10 +279,10 @@ class NWTree():
|
||||
if tItem is not None:
|
||||
tTree.append(tHandle)
|
||||
for i in range(nwConst.maxDepth + 1):
|
||||
if tItem.parHandle is None:
|
||||
if tItem.itemParent is None:
|
||||
return tTree
|
||||
else:
|
||||
tHandle = tItem.parHandle
|
||||
tHandle = tItem.itemParent
|
||||
tItem = self.__getitem__(tHandle)
|
||||
if tItem is None:
|
||||
return tTree
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Init
|
||||
"""novelWriter Exception Handling
|
||||
|
||||
novelWriter – Exception Handling
|
||||
==================================
|
||||
|
||||
@@ -222,7 +222,6 @@ class GuiAbout(QDialog):
|
||||
hColB = self.theParent.theTheme.colHead[2],
|
||||
)
|
||||
self.pageAbout.document().setDefaultStyleSheet(styleSheet)
|
||||
# self.pageCredit.document().setDefaultStyleSheet(styleSheet)
|
||||
self.pageLicense.document().setDefaultStyleSheet(styleSheet)
|
||||
|
||||
return
|
||||
|
||||
@@ -684,8 +684,8 @@ class GuiBuildNovel(QDialog):
|
||||
isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||
isNone |= theItem.itemClass == nwItemClass.NO_CLASS
|
||||
isNone |= theItem.itemClass == nwItemClass.TRASH
|
||||
isNone |= theItem.parHandle == self.theProject.projTree.trashRoot()
|
||||
isNone |= theItem.parHandle is None
|
||||
isNone |= theItem.itemParent == self.theProject.projTree.trashRoot()
|
||||
isNone |= theItem.itemParent is None
|
||||
isNote = theItem.itemLayout == nwItemLayout.NOTE
|
||||
isNovel = not isNone and not isNote
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Addition QConfigLayout
|
||||
"""novelWriter Custom Widgets and Layouts
|
||||
|
||||
novelWriter – Addition QConfigLayout
|
||||
======================================
|
||||
A custom Qt grid layout for config forms similar to QFormLayout
|
||||
novelWriter – Custom Widgets and Layouts
|
||||
==========================================
|
||||
Various custom widget and layout classes
|
||||
|
||||
File History:
|
||||
Created: 2020-05-03 [0.4.5] QConfigLayout
|
||||
|
||||
@@ -807,16 +807,16 @@ class GuiDocEditor(QTextEdit):
|
||||
return self.docSearch.cycleFocus(toNext)
|
||||
return True
|
||||
|
||||
def mouseReleaseEvent(self, mEvent):
|
||||
def mouseReleaseEvent(self, theEvent):
|
||||
"""If the mouse button is released and the control key is
|
||||
pressed, check if we're clicking on a tag, and trigger the
|
||||
follow tag function.
|
||||
"""
|
||||
if qApp.keyboardModifiers() == Qt.ControlModifier:
|
||||
theCursor = self.cursorForPosition(mEvent.pos())
|
||||
theCursor = self.cursorForPosition(theEvent.pos())
|
||||
self._followTag(theCursor)
|
||||
|
||||
QTextEdit.mouseReleaseEvent(self, mEvent)
|
||||
QTextEdit.mouseReleaseEvent(self, theEvent)
|
||||
self.docFooter.updateLineCount()
|
||||
|
||||
return
|
||||
@@ -1203,20 +1203,19 @@ class GuiDocEditor(QTextEdit):
|
||||
"""Check if document size crosses the big document limit set in
|
||||
config. If so, we will set the big document flag to True.
|
||||
"""
|
||||
newState = theSize > self.mainConf.bigDocLimit*1000
|
||||
bigLim = self.mainConf.bigDocLimit*1000
|
||||
newState = theSize > bigLim
|
||||
|
||||
if newState != self.bigDoc:
|
||||
if newState:
|
||||
logger.info(
|
||||
"The document size is {:n} > {:n}, big doc mode has been enabled".format(
|
||||
theSize, self.mainConf.bigDocLimit*1000
|
||||
)
|
||||
f"The document size is {theSize:n} > {bigLim:n}, "
|
||||
f"big doc mode has been enabled"
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
"The document size is {:n} <= {:n}, big doc mode has been disabled".format(
|
||||
theSize, self.mainConf.bigDocLimit*1000
|
||||
)
|
||||
f"The document size is {theSize:n} <= {bigLim:n}, "
|
||||
f"big doc mode has been disabled"
|
||||
)
|
||||
|
||||
self.bigDoc = newState
|
||||
|
||||
@@ -149,18 +149,22 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
||||
|
||||
# Quoted Strings
|
||||
if self.mainConf.highlightQuotes:
|
||||
fmtDO = self.mainConf.fmtDoubleQuotes[0]
|
||||
fmtDC = self.mainConf.fmtDoubleQuotes[1]
|
||||
fmtSO = self.mainConf.fmtSingleQuotes[0]
|
||||
fmtSC = self.mainConf.fmtSingleQuotes[1]
|
||||
self.hRules.append((
|
||||
"\\B{:s}(.*?){:s}\\B".format('"', '"'), {
|
||||
"\\B\"(.*?)\"\\B", {
|
||||
0 : self.hStyles["dialogue1"],
|
||||
}
|
||||
))
|
||||
self.hRules.append((
|
||||
"\\B{:s}(.*?){:s}\\B".format(*self.mainConf.fmtDoubleQuotes), {
|
||||
f"\\B{fmtDO:s}(.*?){fmtDC:s}\\B", {
|
||||
0 : self.hStyles["dialogue2"],
|
||||
}
|
||||
))
|
||||
self.hRules.append((
|
||||
"\\B{:s}(.*?){:s}\\B".format(*self.mainConf.fmtSingleQuotes), {
|
||||
f"\\B{fmtSO:s}(.*?){fmtSC:s}\\B", {
|
||||
0 : self.hStyles["dialogue3"],
|
||||
}
|
||||
))
|
||||
|
||||
@@ -127,7 +127,7 @@ class GuiDocMerge(QDialog):
|
||||
), nwAlert.ERROR)
|
||||
return
|
||||
|
||||
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.parHandle)
|
||||
nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent)
|
||||
newItem = self.theProject.projTree[nHandle]
|
||||
newItem.setStatus(srcItem.itemStatus)
|
||||
|
||||
|
||||
@@ -154,7 +154,7 @@ class GuiDocSplit(QDialog):
|
||||
return
|
||||
|
||||
# Check that another folder can be created
|
||||
parTree = self.theProject.projTree.getItemPath(srcItem.parHandle)
|
||||
parTree = self.theProject.projTree.getItemPath(srcItem.itemParent)
|
||||
if len(parTree) >= nwConst.maxDepth - 1:
|
||||
self.theParent.makeAlert((
|
||||
"Cannot add new folder for the document split. "
|
||||
@@ -176,7 +176,7 @@ class GuiDocSplit(QDialog):
|
||||
|
||||
# Create the folder
|
||||
fHandle = self.theProject.newFolder(
|
||||
srcItem.itemName, srcItem.itemClass, srcItem.parHandle
|
||||
srcItem.itemName, srcItem.itemClass, srcItem.itemParent
|
||||
)
|
||||
self.theParent.treeView.revealNewTreeItem(fHandle)
|
||||
logger.verbose("Creating folder %s" % fHandle)
|
||||
|
||||
@@ -439,6 +439,10 @@ class GuiOutline(QTreeWidget):
|
||||
newItem = QTreeWidgetItem()
|
||||
hIcon = "doc_%s" % tLevel.lower()
|
||||
|
||||
cC = int(novIdx["cCount"])
|
||||
wC = int(novIdx["wCount"])
|
||||
pC = int(novIdx["pCount"])
|
||||
|
||||
newItem.setText(self.colIndex[nwOutline.TITLE], novIdx["title"])
|
||||
newItem.setData(self.colIndex[nwOutline.TITLE], Qt.UserRole, tHandle)
|
||||
newItem.setIcon(self.colIndex[nwOutline.TITLE], self.theTheme.getIcon(hIcon))
|
||||
@@ -448,9 +452,9 @@ class GuiOutline(QTreeWidget):
|
||||
newItem.setText(self.colIndex[nwOutline.LINE], sTitle[1:].lstrip("0"))
|
||||
newItem.setData(self.colIndex[nwOutline.LINE], Qt.UserRole, sTitle)
|
||||
newItem.setText(self.colIndex[nwOutline.SYNOP], novIdx["synopsis"])
|
||||
newItem.setText(self.colIndex[nwOutline.CCOUNT], "{:n}".format(novIdx["cCount"]))
|
||||
newItem.setText(self.colIndex[nwOutline.WCOUNT], "{:n}".format(novIdx["wCount"]))
|
||||
newItem.setText(self.colIndex[nwOutline.PCOUNT], "{:n}".format(novIdx["pCount"]))
|
||||
newItem.setText(self.colIndex[nwOutline.CCOUNT], f"{cC:n}")
|
||||
newItem.setText(self.colIndex[nwOutline.WCOUNT], f"{wC:n}")
|
||||
newItem.setText(self.colIndex[nwOutline.PCOUNT], f"{pC:n}")
|
||||
newItem.setTextAlignment(self.colIndex[nwOutline.CCOUNT], Qt.AlignRight)
|
||||
newItem.setTextAlignment(self.colIndex[nwOutline.WCOUNT], Qt.AlignRight)
|
||||
newItem.setTextAlignment(self.colIndex[nwOutline.PCOUNT], Qt.AlignRight)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Project Outline
|
||||
"""novelWriter GUI Project Outline Details
|
||||
|
||||
novelWriter – GUI Project Outline
|
||||
===================================
|
||||
Class holding the project outline view
|
||||
novelWriter – GUI Project Outline Details
|
||||
===========================================
|
||||
Class holding the project outline details view
|
||||
|
||||
File History:
|
||||
Created: 2020-06-02 [0.7.0]
|
||||
@@ -266,9 +266,13 @@ class GuiOutlineDetails(QScrollArea):
|
||||
self.fileValue.setText(nwItem.itemName)
|
||||
self.itemValue.setText(nwItem.itemStatus)
|
||||
|
||||
self.cCValue.setText("{:n}".format(checkInt(novIdx["cCount"], 0)))
|
||||
self.wCValue.setText("{:n}".format(checkInt(novIdx["wCount"], 0)))
|
||||
self.pCValue.setText("{:n}".format(checkInt(novIdx["pCount"], 0)))
|
||||
cC = checkInt(novIdx["cCount"], 0)
|
||||
wC = checkInt(novIdx["wCount"], 0)
|
||||
pC = checkInt(novIdx["pCount"], 0)
|
||||
|
||||
self.cCValue.setText(f"{cC:n}")
|
||||
self.wCValue.setText(f"{wC:n}")
|
||||
self.pCValue.setText(f"{pC:n}")
|
||||
|
||||
self.synopValue.setText(novIdx["synopsis"])
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Config Editor
|
||||
"""novelWriter GUI Preferences
|
||||
|
||||
novelWriter – GUI Config Editor
|
||||
=================================
|
||||
Class holding the config dialog
|
||||
novelWriter – GUI Preferences
|
||||
===============================
|
||||
Class holding the preferences dialog
|
||||
|
||||
File History:
|
||||
Created: 2019-06-10 [0.1.5]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
novelWriter – GUI Open Project
|
||||
================================
|
||||
New and open project dialog
|
||||
The open project dialog
|
||||
|
||||
File History:
|
||||
Created: 2020-02-26 [0.4.5]
|
||||
|
||||
@@ -270,11 +270,12 @@ class GuiProjectEditMeta(QWidget):
|
||||
|
||||
self.revLabel = QLabel("Revision count:")
|
||||
self.revLabel.setIndent(xInd)
|
||||
self.revValue = QLabel("{:n}".format(self.theProject.saveCount))
|
||||
self.revValue = QLabel(f"{self.theProject.saveCount:n}")
|
||||
|
||||
editHours = self.theProject.editTime/3600
|
||||
self.editLabel = QLabel("Edit time:")
|
||||
self.editLabel.setIndent(xInd)
|
||||
self.editValue = QLabel("{:.2f} hours".format(self.theProject.editTime/3600))
|
||||
self.editValue = QLabel(f"{editHours:.2f} hours")
|
||||
|
||||
self.statsLabel = QLabel("<b>Project Stats</b>")
|
||||
|
||||
@@ -294,7 +295,7 @@ class GuiProjectEditMeta(QWidget):
|
||||
|
||||
self.wordsLabel = QLabel("Word count:")
|
||||
self.wordsLabel.setIndent(xInd)
|
||||
self.wordsValue = QLabel("{:n}".format(self.theProject.currWCount))
|
||||
self.wordsValue = QLabel(f"{self.theProject.currWCount:n}")
|
||||
|
||||
self.mainForm.addWidget(self.headLabel, 0, 0, 1, 2, Qt.AlignTop)
|
||||
self.mainForm.addWidget(self.nameLabel, 1, 0, 1, 1, Qt.AlignTop)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter GUI Document Tree
|
||||
"""novelWriter GUI Project Tree
|
||||
|
||||
novelWriter – GUI Document Tree
|
||||
=================================
|
||||
Class holding the left side document tree view
|
||||
novelWriter – GUI project Tree
|
||||
================================
|
||||
Class holding the left side project tree view
|
||||
|
||||
File History:
|
||||
Created: 2018-09-29 [0.0.1] GuiProjectTree
|
||||
@@ -219,7 +219,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
pItem = self.theProject.projTree[pHandle]
|
||||
if pItem.itemType == nwItemType.FILE:
|
||||
nHandle = pHandle
|
||||
pHandle = pItem.parHandle
|
||||
pHandle = pItem.itemParent
|
||||
|
||||
# If we again have no home, give up
|
||||
if pHandle is None:
|
||||
@@ -270,7 +270,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
"""
|
||||
nwItem = self.theProject.projTree[tHandle]
|
||||
trItem = self._addTreeItem(nwItem, nHandle)
|
||||
pHandle = nwItem.parHandle
|
||||
pHandle = nwItem.itemParent
|
||||
if pHandle is not None and pHandle in self.theMap:
|
||||
self.theMap[pHandle].setExpanded(True)
|
||||
self.clearSelection()
|
||||
@@ -430,7 +430,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
logger.error("Could not delete item")
|
||||
return False
|
||||
|
||||
pHandle = nwItemS.parHandle
|
||||
pHandle = nwItemS.itemParent
|
||||
if self.theProject.projTree.isTrashRoot(pHandle):
|
||||
# If the file is in the trash folder already, as the
|
||||
# user if they want to permanently delete the file.
|
||||
@@ -815,7 +815,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
project tree.
|
||||
"""
|
||||
tHandle = nwItem.itemHandle
|
||||
pHandle = nwItem.parHandle
|
||||
pHandle = nwItem.itemParent
|
||||
tClass = nwItem.itemClass
|
||||
newItem = QTreeWidgetItem([""]*4)
|
||||
|
||||
@@ -1022,11 +1022,11 @@ class GuiProjectTreeMenu(QMenu):
|
||||
|
||||
trashHandle = self.theTree.theProject.projTree.trashRoot()
|
||||
|
||||
inTrash = theItem.parHandle == trashHandle and trashHandle is not None
|
||||
inTrash = theItem.itemParent == trashHandle and trashHandle is not None
|
||||
isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
|
||||
isFile = theItem.itemType == nwItemType.FILE
|
||||
isArch = theRoot.itemClass == nwItemClass.ARCHIVE
|
||||
isOrph = isFile and theItem.parHandle is None
|
||||
isOrph = isFile and theItem.itemParent is None
|
||||
|
||||
showOpen = isFile
|
||||
showView = isFile
|
||||
|
||||
@@ -195,28 +195,21 @@ class GuiMainStatus(QStatusBar):
|
||||
self.statsText.setToolTip(
|
||||
"Project word count (session change)"
|
||||
)
|
||||
self.statsText.setText((
|
||||
"Words: {pWC:n} ({sWC:+n})"
|
||||
).format(
|
||||
pWC = self.projWords,
|
||||
sWC = self.sessWords,
|
||||
))
|
||||
self.statsText.setText(
|
||||
f"Words: {self.projWords:n} ({self.sessWords:+n})"
|
||||
)
|
||||
return
|
||||
|
||||
def _updateTime(self):
|
||||
"""Update the session clock.
|
||||
"""
|
||||
if self.refTime is None:
|
||||
theTime = "00:00:00"
|
||||
self.timeText.setText("00:00:00")
|
||||
else:
|
||||
# This is much faster than using datetime format
|
||||
tS = int(time() - self.refTime)
|
||||
tM = int(tS/60)
|
||||
tH = int(tM/60)
|
||||
tM = tM - tH*60
|
||||
tS = tS - tM*60 - tH*3600
|
||||
theTime = "%02d:%02d:%02d" % (tH, tM, tS)
|
||||
self.timeText.setText(theTime)
|
||||
self.timeText.setText(
|
||||
f"{tS//3600:02d}:{tS%3600//60:02d}:{tS%60:02d}"
|
||||
)
|
||||
return
|
||||
|
||||
# END Class GuiMainStatus
|
||||
@@ -237,7 +230,7 @@ class StatusLED(QAbstractButton):
|
||||
return
|
||||
|
||||
##
|
||||
# Getters and Setters
|
||||
# Setters
|
||||
##
|
||||
|
||||
def setState(self, theState):
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""novelWriter Theme Class
|
||||
"""novelWriter Theme and Icons Classes
|
||||
|
||||
novelWriter – Theme Class
|
||||
===========================
|
||||
This class reads and store the main theme
|
||||
novelWriter – Theme and Icons Classs
|
||||
======================================
|
||||
This class reads and stores the themes and the icons
|
||||
|
||||
File History:
|
||||
Created: 2019-05-18 [0.1.3] GuiTheme
|
||||
|
||||
@@ -62,7 +62,9 @@ class GuiMain(QMainWindow):
|
||||
self.mainConf = nw.CONFIG
|
||||
self.threadPool = QThreadPool()
|
||||
|
||||
# Some runtime info useful for debugging
|
||||
# System Info
|
||||
# ===========
|
||||
|
||||
logger.info("OS: %s" % self.mainConf.osType)
|
||||
logger.info("Kernel: %s" % self.mainConf.kernelVer)
|
||||
logger.info("Host: %s" % self.mainConf.hostName)
|
||||
@@ -76,6 +78,9 @@ class GuiMain(QMainWindow):
|
||||
self.mainConf.verPyString, self.mainConf.verPyHexVal)
|
||||
)
|
||||
|
||||
# Core Classes
|
||||
# ============
|
||||
|
||||
# Core Classes and settings
|
||||
self.theTheme = GuiTheme(self)
|
||||
self.theProject = NWProject(self)
|
||||
@@ -89,7 +94,7 @@ class GuiMain(QMainWindow):
|
||||
self.setWindowIcon(QIcon(self.mainConf.appIcon))
|
||||
|
||||
# Build the GUI
|
||||
################
|
||||
# =============
|
||||
|
||||
# Main GUI Elements
|
||||
self.statusBar = GuiMainStatus(self)
|
||||
@@ -106,7 +111,7 @@ class GuiMain(QMainWindow):
|
||||
self.statusIcons = []
|
||||
self.importIcons = []
|
||||
|
||||
# Assemble Main Window
|
||||
# Project Tree View
|
||||
self.treePane = QWidget()
|
||||
self.treeBox = QVBoxLayout()
|
||||
self.treeBox.setContentsMargins(0, 0, 0, 0)
|
||||
@@ -114,20 +119,24 @@ class GuiMain(QMainWindow):
|
||||
self.treeBox.addWidget(self.treeMeta)
|
||||
self.treePane.setLayout(self.treeBox)
|
||||
|
||||
# Splitter : Document Viewer / Document Meta
|
||||
self.splitView = QSplitter(Qt.Vertical)
|
||||
self.splitView.addWidget(self.docViewer)
|
||||
self.splitView.addWidget(self.viewMeta)
|
||||
self.splitView.setSizes(self.mainConf.getViewPanePos())
|
||||
|
||||
# Splitter : Document Editor / Document Viewer
|
||||
self.splitDocs = QSplitter(Qt.Horizontal)
|
||||
self.splitDocs.addWidget(self.docEditor)
|
||||
self.splitDocs.addWidget(self.splitView)
|
||||
|
||||
# Splitter : Project Outlie / Outline Details
|
||||
self.splitOutline = QSplitter(Qt.Vertical)
|
||||
self.splitOutline.addWidget(self.projView)
|
||||
self.splitOutline.addWidget(self.projMeta)
|
||||
self.splitOutline.setSizes(self.mainConf.getOutlinePanePos())
|
||||
|
||||
# Main Tabs : Edirot / Outline
|
||||
self.tabWidget = QTabWidget()
|
||||
self.tabWidget.setTabPosition(QTabWidget.East)
|
||||
self.tabWidget.setStyleSheet("QTabWidget::pane {border: 0;}")
|
||||
@@ -135,6 +144,7 @@ class GuiMain(QMainWindow):
|
||||
self.tabWidget.addTab(self.splitOutline, "Outline")
|
||||
self.tabWidget.currentChanged.connect(self._mainTabChanged)
|
||||
|
||||
# Splitter : Project Tree / Main Tabs
|
||||
xCM = self.mainConf.pxInt(4)
|
||||
self.splitMain = QSplitter(Qt.Horizontal)
|
||||
self.splitMain.setContentsMargins(xCM, xCM, xCM, xCM)
|
||||
@@ -142,6 +152,7 @@ class GuiMain(QMainWindow):
|
||||
self.splitMain.addWidget(self.tabWidget)
|
||||
self.splitMain.setSizes(self.mainConf.getMainPanePos())
|
||||
|
||||
# Indices of All Splitter Widgets
|
||||
self.idxTree = self.splitMain.indexOf(self.treePane)
|
||||
self.idxMain = self.splitMain.indexOf(self.tabWidget)
|
||||
self.idxEditor = self.splitDocs.indexOf(self.docEditor)
|
||||
@@ -151,6 +162,7 @@ class GuiMain(QMainWindow):
|
||||
self.idxTabEdit = self.tabWidget.indexOf(self.splitDocs)
|
||||
self.idxTabProj = self.tabWidget.indexOf(self.splitOutline)
|
||||
|
||||
# Splitter Behaviour
|
||||
self.splitMain.setCollapsible(self.idxTree, False)
|
||||
self.splitMain.setCollapsible(self.idxMain, False)
|
||||
self.splitDocs.setCollapsible(self.idxEditor, False)
|
||||
@@ -158,10 +170,11 @@ class GuiMain(QMainWindow):
|
||||
self.splitView.setCollapsible(self.idxViewDoc, False)
|
||||
self.splitView.setCollapsible(self.idxViewMeta, False)
|
||||
|
||||
# Editor / Viewer Default State
|
||||
self.splitView.setVisible(False)
|
||||
self.docEditor.closeSearch()
|
||||
|
||||
# Build the Tree View
|
||||
# Initialise the Project Tree
|
||||
self.treeView.itemSelectionChanged.connect(self._treeSingleClick)
|
||||
self.treeView.itemDoubleClicked.connect(self._treeDoubleClick)
|
||||
self.rebuildTree()
|
||||
@@ -172,13 +185,13 @@ class GuiMain(QMainWindow):
|
||||
self.setStatusBar(self.statusBar)
|
||||
|
||||
# Finalise Initialisation
|
||||
##########################
|
||||
# =======================
|
||||
|
||||
# Set Up Autosaving Project Timer
|
||||
# Set Up Auto-Save Project Timer
|
||||
self.asProjTimer = QTimer()
|
||||
self.asProjTimer.timeout.connect(self._autoSaveProject)
|
||||
|
||||
# Set Up Autosaving Document Timer
|
||||
# Set Up Auto-Save Document Timer
|
||||
self.asDocTimer = QTimer()
|
||||
self.asDocTimer.timeout.connect(self._autoSaveDocument)
|
||||
|
||||
@@ -203,11 +216,13 @@ class GuiMain(QMainWindow):
|
||||
# Check that config loaded fine
|
||||
self.reportConfErr()
|
||||
|
||||
# Initialise Main GUI
|
||||
self.initMain()
|
||||
self.asProjTimer.start()
|
||||
self.asDocTimer.start()
|
||||
self.statusBar.clearStatus()
|
||||
|
||||
# Handle Windows Mode
|
||||
self.showNormal()
|
||||
if self.mainConf.isFullScreen:
|
||||
self.toggleFullScreenMode()
|
||||
@@ -224,7 +239,7 @@ class GuiMain(QMainWindow):
|
||||
self.showProjectLoadDialog()
|
||||
|
||||
logger.debug("novelWriter is ready ...")
|
||||
self.statusBar.setStatus("novelWriter is ready ...")
|
||||
self.setStatus("novelWriter is ready ...")
|
||||
|
||||
return
|
||||
|
||||
@@ -249,8 +264,7 @@ class GuiMain(QMainWindow):
|
||||
##
|
||||
|
||||
def newProject(self, projData=None):
|
||||
"""Create new project with a few default files and folders.
|
||||
The variable forceNew is used for testing.
|
||||
"""Create new project via the new project wizard.
|
||||
"""
|
||||
if self.hasProject:
|
||||
self.makeAlert(
|
||||
@@ -293,7 +307,7 @@ class GuiMain(QMainWindow):
|
||||
def closeProject(self, isYes=False):
|
||||
"""Closes the project if one is open. isYes is passed on from
|
||||
the close application event so the user doesn't get prompted
|
||||
twice.
|
||||
twice to confirm.
|
||||
"""
|
||||
if not self.hasProject:
|
||||
# There is no project loaded, everything OK
|
||||
@@ -302,7 +316,7 @@ class GuiMain(QMainWindow):
|
||||
if not isYes:
|
||||
msgBox = QMessageBox()
|
||||
msgRes = msgBox.question(
|
||||
self, "Close Project", "Save changes and close current project?"
|
||||
self, "Close Project", "Save changes and close the current project?"
|
||||
)
|
||||
if msgRes != QMessageBox.Yes:
|
||||
return False
|
||||
@@ -318,7 +332,7 @@ class GuiMain(QMainWindow):
|
||||
if self.mainConf.askBeforeBackup:
|
||||
msgBox = QMessageBox()
|
||||
msgRes = msgBox.question(
|
||||
self, "Backup Project", "Backup current project?"
|
||||
self, "Backup Project", "Backup the current project?"
|
||||
)
|
||||
if msgRes != QMessageBox.Yes:
|
||||
doBackup = False
|
||||
@@ -433,6 +447,7 @@ class GuiMain(QMainWindow):
|
||||
if self.theProject.projPath is None:
|
||||
projPath = self.selectProjectPath()
|
||||
self.theProject.setProjectPath(projPath)
|
||||
|
||||
if self.theProject.projPath is None:
|
||||
return False
|
||||
|
||||
@@ -454,6 +469,7 @@ class GuiMain(QMainWindow):
|
||||
if self.docEditor.docChanged:
|
||||
self.saveDocument()
|
||||
self.docEditor.clearEditor()
|
||||
|
||||
return True
|
||||
|
||||
def openDocument(self, tHandle, tLine=None, changeFocus=True, doScroll=False):
|
||||
@@ -469,6 +485,7 @@ class GuiMain(QMainWindow):
|
||||
self.treeView.setSelectedHandle(tHandle, doScroll=doScroll)
|
||||
else:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def openNextDocument(self, tHandle, wrapAround=False):
|
||||
@@ -546,6 +563,7 @@ class GuiMain(QMainWindow):
|
||||
vPos[1] = bPos[1] - vPos[0]
|
||||
self.splitDocs.setSizes(vPos)
|
||||
self.viewMeta.setVisible(self.mainConf.showRefPanel)
|
||||
|
||||
self.docViewer.navigateTo(tAnchor)
|
||||
|
||||
return True
|
||||
@@ -697,9 +715,9 @@ class GuiMain(QMainWindow):
|
||||
for nDone, tItem in enumerate(self.theProject.projTree):
|
||||
|
||||
if tItem is not None:
|
||||
self.statusBar.setStatus("Indexing: '%s'" % tItem.itemName)
|
||||
self.setStatus("Indexing: '%s'" % tItem.itemName)
|
||||
else:
|
||||
self.statusBar.setStatus("Indexing: Unknown item")
|
||||
self.setStatus("Indexing: Unknown item")
|
||||
|
||||
if tItem is not None and tItem.itemType == nwItemType.FILE:
|
||||
logger.verbose("Scanning: %s" % tItem.itemName)
|
||||
@@ -717,7 +735,7 @@ class GuiMain(QMainWindow):
|
||||
self.treeView.projectWordCount()
|
||||
|
||||
tEnd = time()
|
||||
self.statusBar.setStatus("Indexing completed in %.1f ms" % ((tEnd - tStart)*1000.0))
|
||||
self.setStatus("Indexing completed in %.1f ms" % ((tEnd - tStart)*1000.0))
|
||||
self.docEditor.updateTagHighLighting()
|
||||
qApp.restoreOverrideCursor()
|
||||
|
||||
@@ -754,7 +772,8 @@ class GuiMain(QMainWindow):
|
||||
def showProjectLoadDialog(self):
|
||||
"""Opens the projects dialog for selecting either existing
|
||||
projects from a cache of recently opened projects, or provide a
|
||||
browse button for projects not yet cached.
|
||||
browse button for projects not yet cached. Selecting to create a
|
||||
new project is forwarded to the new project wizard.
|
||||
"""
|
||||
dlgProj = GuiProjectLoad(self)
|
||||
dlgProj.exec_()
|
||||
@@ -767,7 +786,7 @@ class GuiMain(QMainWindow):
|
||||
return True
|
||||
|
||||
def showNewProjectDialog(self):
|
||||
"""Open the wizard and assemble the project options dict.
|
||||
"""Open the wizard and assemble a project options dict.
|
||||
"""
|
||||
newProj = GuiProjectWizard(self)
|
||||
newProj.exec_()
|
||||
@@ -865,8 +884,7 @@ class GuiMain(QMainWindow):
|
||||
|
||||
def makeAlert(self, theMessage, theLevel=nwAlert.INFO):
|
||||
"""Alert both the user and the logger at the same time. Message
|
||||
can be either a string or an array of strings. Severity level is
|
||||
0 = info, 1 = warning, and 2 = error.
|
||||
can be either a string or an array of strings.
|
||||
"""
|
||||
if isinstance(theMessage, list):
|
||||
popMsg = "<br>".join(theMessage)
|
||||
@@ -955,7 +973,7 @@ class GuiMain(QMainWindow):
|
||||
return True
|
||||
|
||||
def setFocus(self, paneNo):
|
||||
"""Switch focus to one of the three main gUi panes.
|
||||
"""Switch focus to one of the three main GUI panes.
|
||||
"""
|
||||
if paneNo == 1:
|
||||
self.treeView.setFocus()
|
||||
@@ -1236,9 +1254,9 @@ class GuiMain(QMainWindow):
|
||||
return
|
||||
|
||||
def _treeKeyPressReturn(self):
|
||||
"""The user pressed return an item in the tree. If it is a file,
|
||||
we open it. Otherwise, we do nothing. Pressing return does not
|
||||
change focus to the editor as double click does.
|
||||
"""The user pressed return on an item in the tree. If it is a
|
||||
file, we open it. Otherwise, we do nothing. Pressing return does
|
||||
not change focus to the editor as double click does.
|
||||
"""
|
||||
tHandle = self.treeView.getSelectedHandle()
|
||||
logger.verbose("User pressed return on tree item with handle %s" % tHandle)
|
||||
@@ -1257,7 +1275,6 @@ class GuiMain(QMainWindow):
|
||||
"""
|
||||
if self.docEditor.docSearch.isVisible():
|
||||
self.docEditor.closeSearch()
|
||||
return
|
||||
elif self.isFocusMode:
|
||||
self.toggleFocusMode()
|
||||
return
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
@@ -1,11 +1,54 @@
|
||||
[metadata]
|
||||
license_files = LICENSE.md
|
||||
name = novelWriter
|
||||
version = attr: nw.__version__
|
||||
author = Veronica Berglyd Olsen
|
||||
author_email = code@vkbo.net
|
||||
description = A markdown-like document editor for writing novels
|
||||
url = https://novelwriter.io
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
license_file = LICENSE.md
|
||||
license = GNU General Public License v3
|
||||
classifiers =
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: Implementation :: CPython
|
||||
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
||||
Development Status :: 4 - Beta
|
||||
Operating System :: OS Independent
|
||||
Intended Audience :: End Users/Desktop
|
||||
Natural Language :: English
|
||||
Topic :: Text Editors
|
||||
python_requires = >=3.6
|
||||
install_requires =
|
||||
pyqt5>=5.2.1
|
||||
lxml>=4.2.0
|
||||
pyenchant>=3.0.0
|
||||
project_urls =
|
||||
Bug Tracker = https://github.com/vkbo/novelWriter/issues
|
||||
Documentation = https://github.com/vkbo/novelWriter/issues
|
||||
Source Code = https://github.com/vkbo/novelWriter
|
||||
|
||||
[options]
|
||||
include_package_data = True
|
||||
packages = find:
|
||||
|
||||
[options.packages.find]
|
||||
exclude = docs, tests, sample
|
||||
|
||||
[options.entry_points]
|
||||
console_script =
|
||||
novelWriter-cli = nw:main
|
||||
gui_scripts =
|
||||
novelWriter = nw:main
|
||||
|
||||
[bdist_wheel]
|
||||
universal = 0
|
||||
|
||||
[flake8]
|
||||
ignore = E203,E221,E226,E241,E251,E261,E266,E302,E305
|
||||
ignore = E203,E221,E226,E228,E241,E251,E261,E266,E302,E305
|
||||
max-line-length = 99
|
||||
exclude = docs/*
|
||||
|
||||
@@ -1,30 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
The main setup script for novelWeiter.
|
||||
|
||||
It runs the standard setuptool.setup() with all options taken from the
|
||||
setup.cfg file.
|
||||
|
||||
In addtion, a few speicalised commands are available:
|
||||
|
||||
* sample: Will build a sample.zip file, which is the way the sample project is
|
||||
included into distributable packages.
|
||||
* qthelp: Will build a QtAssistant readable version of the novelWriter
|
||||
documentation. This should also be a part of distributed packages. It allows
|
||||
for reading the help offline. Otherwise, the F1 button redirects to the
|
||||
online documentation only.
|
||||
* launcher: Will attempt to install novelWriter icons, mime type and create a
|
||||
launcher for the application.
|
||||
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
import setuptools
|
||||
|
||||
##
|
||||
# Build the Package
|
||||
##
|
||||
# =============================================================================================== #
|
||||
# Qt Assistant Documentation Builder
|
||||
# =============================================================================================== #
|
||||
|
||||
buildDocs = False
|
||||
buildSample = False
|
||||
def buildQtDocs():
|
||||
"""This function will build the documentation as a Qt help file. The
|
||||
file is then copied into the nw/assets/help directory and can be
|
||||
included in builds.
|
||||
|
||||
if "qthelp" in sys.argv:
|
||||
buildDocs = True
|
||||
sys.argv.remove("qthelp")
|
||||
|
||||
if "sample" in sys.argv:
|
||||
buildSample = True
|
||||
sys.argv.remove("sample")
|
||||
|
||||
##
|
||||
# Qt Assistant Documentation
|
||||
##
|
||||
|
||||
if buildDocs:
|
||||
Depends on packages:
|
||||
* pip install sphinx
|
||||
* pip install sphinx-rtd-theme
|
||||
* pip install sphinxcontrib-qthelp
|
||||
|
||||
It also requires the qhelpgenerator to be available on the system.
|
||||
"""
|
||||
buildDir = os.path.join("docs", "build", "qthelp")
|
||||
helpDir = os.path.join("nw", "assets", "help")
|
||||
|
||||
@@ -41,14 +55,14 @@ if buildDocs:
|
||||
try:
|
||||
subprocess.call(["make", "-C", "docs", "qthelp"])
|
||||
except Exception as e:
|
||||
print("Failed with error:")
|
||||
print("QtHelp Build Error:")
|
||||
print(str(e))
|
||||
buildFail = True
|
||||
|
||||
try:
|
||||
subprocess.call(["qhelpgenerator", os.path.join(buildDir, inFile)])
|
||||
except Exception as e:
|
||||
print("Failed with error:")
|
||||
print("QtHelp Build Error:")
|
||||
print(str(e))
|
||||
buildFail = True
|
||||
|
||||
@@ -56,7 +70,7 @@ if buildDocs:
|
||||
try:
|
||||
os.mkdir(helpDir)
|
||||
except Exception as e:
|
||||
print("Failed with error:")
|
||||
print("QtHelp Build Error:")
|
||||
print(str(e))
|
||||
buildFail = True
|
||||
|
||||
@@ -68,7 +82,7 @@ if buildDocs:
|
||||
os.rename(os.path.join(buildDir, outFile), os.path.join(helpDir, outFile))
|
||||
os.rename(os.path.join(buildDir, datFile), os.path.join(helpDir, datFile))
|
||||
except Exception as e:
|
||||
print("Failed with error:")
|
||||
print("QtHelp Build Error:")
|
||||
print(str(e))
|
||||
buildFail = True
|
||||
|
||||
@@ -80,11 +94,20 @@ if buildDocs:
|
||||
print("Documentation build: OK")
|
||||
print("")
|
||||
|
||||
##
|
||||
# Sample Project ZIP file
|
||||
##
|
||||
return
|
||||
|
||||
if buildSample:
|
||||
# =============================================================================================== #
|
||||
# Sample Project ZIP File Builder
|
||||
# =============================================================================================== #
|
||||
|
||||
def buildSampleZip():
|
||||
"""Bundle the sample project into a single zip file to be saved into
|
||||
the nw/assets folder for further bundling into builds.
|
||||
"""
|
||||
print("")
|
||||
print("Building Sample ZIP File")
|
||||
print("========================")
|
||||
print("")
|
||||
|
||||
srcSample = "sample"
|
||||
dstSample = os.path.join("nw", "assets", "sample.zip")
|
||||
@@ -96,8 +119,10 @@ if buildSample:
|
||||
from zipfile import ZipFile
|
||||
|
||||
with ZipFile(dstSample, "w") as zipObj:
|
||||
print("Compressing: nwProject.nwx")
|
||||
zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx")
|
||||
for docFile in os.listdir(os.path.join(srcSample, "content")):
|
||||
print("Compressing: content/%s" % docFile)
|
||||
srcDoc = os.path.join(srcSample, "content", docFile)
|
||||
zipObj.write(srcDoc, "content/"+docFile)
|
||||
|
||||
@@ -105,58 +130,197 @@ if buildSample:
|
||||
print("Error: Could not find sample project source directory.")
|
||||
sys.exit(1)
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
# Nothing more to do
|
||||
sys.exit(0)
|
||||
print("")
|
||||
print("Built file: %s" % dstSample)
|
||||
print("")
|
||||
|
||||
##
|
||||
# Build the Package
|
||||
##
|
||||
return
|
||||
|
||||
# Read content from files
|
||||
with open("README.md", "r") as inFile:
|
||||
longDescription = inFile.read()
|
||||
# =============================================================================================== #
|
||||
# Create Launcher
|
||||
# =============================================================================================== #
|
||||
|
||||
setuptools.setup(
|
||||
name = "novelWriter",
|
||||
# version = __version__, # Set in setup.cfg
|
||||
author = "Veronica Berglyd Olsen",
|
||||
author_email = "code@vkbo.net",
|
||||
description = "A markdown-like document editor for writing novels",
|
||||
long_description = longDescription,
|
||||
long_description_content_type = "text/markdown",
|
||||
license = "GNU General Public License v3",
|
||||
url = "https://novelwriter.io",
|
||||
entry_points = {
|
||||
"console_scripts" : ["novelWriter-cli=nw:main"],
|
||||
"gui_scripts" : ["novelWriter=nw:main"],
|
||||
},
|
||||
packages = setuptools.find_packages(exclude=["docs", "tests", "sample"]),
|
||||
include_package_data = True,
|
||||
package_data = {"": ["*.conf"]},
|
||||
project_urls = {
|
||||
"Bug Tracker": "https://github.com/vkbo/novelWriter/issues",
|
||||
"Documentation": "https://github.com/vkbo/novelWriter/issues",
|
||||
"Source Code": "https://github.com/vkbo/novelWriter",
|
||||
},
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Development Status :: 4 - Beta",
|
||||
"Operating System :: OS Independent",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"Natural Language :: English",
|
||||
"Topic :: Text Editors",
|
||||
],
|
||||
python_requires = ">=3.6",
|
||||
install_requires = [
|
||||
"pyqt5>=5.2.1",
|
||||
"lxml>=4.2.0",
|
||||
"pyenchant>=3.0.0",
|
||||
],
|
||||
)
|
||||
def makeLauncherLinux():
|
||||
"""Will attempt to install icons and make a launcher.
|
||||
"""
|
||||
print("")
|
||||
print("Creating Launcher")
|
||||
print("=================")
|
||||
print("")
|
||||
|
||||
exOpts = []
|
||||
|
||||
testExec = shutil.which("novelWriter")
|
||||
if testExec is not None:
|
||||
exOpts.append(testExec)
|
||||
|
||||
testExec = shutil.which("novelwriter")
|
||||
if testExec is not None:
|
||||
exOpts.append(testExec)
|
||||
|
||||
testExec = os.path.join(os.getcwd(), "novelWriter.py")
|
||||
if os.path.isfile(testExec):
|
||||
exOpts.append(testExec)
|
||||
|
||||
useExec = ""
|
||||
nOpts = len(exOpts)
|
||||
if nOpts == 0:
|
||||
print("Error: No executables for novelWriter found.")
|
||||
sys.exit(1)
|
||||
elif nOpts == 1:
|
||||
useExec = exOpts[0]
|
||||
else:
|
||||
print("Found multiple novelWriter executables:")
|
||||
print("")
|
||||
for iExec, anExec in enumerate(exOpts):
|
||||
print(" [%d] %s" % (iExec, anExec))
|
||||
print("")
|
||||
intVal = int(input("Please select which novelWriter executable to use: "))
|
||||
print("")
|
||||
|
||||
if intVal >= 0 and intVal < nOpts:
|
||||
useExec = exOpts[intVal]
|
||||
else:
|
||||
print("Error: Invalid selection.")
|
||||
sys.exit(1)
|
||||
|
||||
print("Using executable: %s " % useExec)
|
||||
|
||||
# Read the Template
|
||||
desktopData = ""
|
||||
with open(os.path.join("setup", "novelwriter.desktop"), mode="r") as inFile:
|
||||
desktopData = inFile.read()
|
||||
|
||||
desktopData = desktopData.replace(r"%%exec%%", useExec)
|
||||
|
||||
desktopFile = "/usr/share/applications/novelwriter.desktop"
|
||||
try:
|
||||
with open(desktopFile, mode="w+") as outFile:
|
||||
outFile.write(desktopData)
|
||||
print("Wrote file: %s" % desktopFile)
|
||||
except Exception as e:
|
||||
print("Error: Could not write novelwriter.desktop file.")
|
||||
print(str(e))
|
||||
sys.exit(1)
|
||||
|
||||
print("")
|
||||
|
||||
# Copy Icons
|
||||
|
||||
iconDirs = [
|
||||
"/usr/share/icons/hicolor/24x24/apps",
|
||||
"/usr/share/icons/hicolor/48x48/apps",
|
||||
"/usr/share/icons/hicolor/96x96/apps",
|
||||
"/usr/share/icons/hicolor/256x256/apps",
|
||||
"/usr/share/icons/hicolor/512x512/apps",
|
||||
"/usr/share/icons/hicolor/scalable/apps",
|
||||
"/usr/share/icons/hicolor/scalable/mimetypes",
|
||||
]
|
||||
for iconDir in iconDirs:
|
||||
if not os.path.isdir:
|
||||
try:
|
||||
os.mkdir(iconDir)
|
||||
print("Created folder: %s" % iconDir)
|
||||
except Exception as e:
|
||||
print("Error: Could not make folder: %s" % iconDir)
|
||||
print(str(e))
|
||||
|
||||
copyList = [(
|
||||
"setup/icons/24x24/novelwriter.png",
|
||||
"/usr/share/icons/hicolor/24x24/apps/novelwriter.png"
|
||||
), (
|
||||
"setup/icons/48x48/novelwriter.png",
|
||||
"/usr/share/icons/hicolor/48x48/apps/novelwriter.png"
|
||||
), (
|
||||
"setup/icons/96x96/novelwriter.png",
|
||||
"/usr/share/icons/hicolor/96x96/apps/novelwriter.png"
|
||||
), (
|
||||
"setup/icons/256x256/novelwriter.png",
|
||||
"/usr/share/icons/hicolor/256x256/apps/novelwriter.png"
|
||||
), (
|
||||
"setup/icons/512x512/novelwriter.png",
|
||||
"/usr/share/icons/hicolor/512x512/apps/novelwriter.png"
|
||||
), (
|
||||
"setup/icons/novelwriter.svg",
|
||||
"/usr/share/icons/hicolor/scalable/apps/novelwriter.svg"
|
||||
), (
|
||||
"setup/icons/x-novelwriter-project.svg",
|
||||
"/usr/share/icons/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg"
|
||||
), (
|
||||
"setup/mime/x-novelwriter-project.xml",
|
||||
"/usr/share/mime/packages/x-novelwriter-project.xml"
|
||||
)]
|
||||
for srcFile, dstFile in copyList:
|
||||
try:
|
||||
shutil.copyfile(srcFile, dstFile)
|
||||
print("Copied file to: %s" % dstFile)
|
||||
except Exception as e:
|
||||
print("Error: Could not copy file: %s" % srcFile)
|
||||
print(str(e))
|
||||
|
||||
print("")
|
||||
|
||||
# Update System
|
||||
try:
|
||||
subprocess.call(["update-mime-database", "/usr/share/mime/"])
|
||||
print("Updated mime database.")
|
||||
except Exception as e:
|
||||
print("Error: Filed to update mime database.")
|
||||
print(str(e))
|
||||
|
||||
try:
|
||||
subprocess.call(["update-icon-caches", "/usr/share/icons/*"])
|
||||
print("Updated icon cache.")
|
||||
except Exception as e:
|
||||
print("Error: Filed to update icon cache.")
|
||||
print(str(e))
|
||||
|
||||
print("")
|
||||
print("Done!")
|
||||
print("")
|
||||
|
||||
return
|
||||
|
||||
# =============================================================================================== #
|
||||
# Process Jobs
|
||||
# =============================================================================================== #
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
helpMsg = (
|
||||
"\n"
|
||||
"novelWriter Setup Tool\n"
|
||||
"======================\n"
|
||||
"This tool provides some additional setup commands for novelWriter.\n"
|
||||
"\n"
|
||||
"help Print the help message.\n"
|
||||
"qthelp Build the help documentation for use with the QtAssistant.\n"
|
||||
"sample Build the sample project as a zip file.\n"
|
||||
"launcher Install launcher icons for freedesktop systems.\n"
|
||||
)
|
||||
|
||||
if "help" in sys.argv:
|
||||
sys.argv.remove("help")
|
||||
print(helpMsg)
|
||||
sys.exit(0)
|
||||
|
||||
if "qthelp" in sys.argv:
|
||||
sys.argv.remove("qthelp")
|
||||
buildQtDocs()
|
||||
|
||||
if "sample" in sys.argv:
|
||||
sys.argv.remove("sample")
|
||||
buildSampleZip()
|
||||
|
||||
if "launcher" in sys.argv:
|
||||
sys.argv.remove("launcher")
|
||||
makeLauncherLinux()
|
||||
|
||||
if len(sys.argv) <= 1:
|
||||
# Nothing more to do
|
||||
sys.exit(0)
|
||||
|
||||
# Run the standard setup
|
||||
setuptools.setup()
|
||||
|
||||
# END Main
|
||||
|
||||
@@ -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"?>
|
||||
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||
<mime-type type="application/x-novelwriter-project">
|
||||
<comment>novelWriter Project </comment>
|
||||
<comment>novelWriter Project</comment>
|
||||
<sub-class-of type="application/xml"/>
|
||||
<glob pattern="*.nwx"/>
|
||||
</mime-type>
|
||||
@@ -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
|
||||
@@ -31,13 +31,13 @@ def testItemSettersSimple(nwDummy):
|
||||
|
||||
# Parent
|
||||
theItem.setParent(None)
|
||||
assert theItem.parHandle is None
|
||||
assert theItem.itemParent is None
|
||||
theItem.setParent(123)
|
||||
assert theItem.parHandle is None
|
||||
assert theItem.itemParent is None
|
||||
theItem.setParent("0123456789abcdef")
|
||||
assert theItem.parHandle is None
|
||||
assert theItem.itemParent is None
|
||||
theItem.setParent("0123456789abc")
|
||||
assert theItem.parHandle == "0123456789abc"
|
||||
assert theItem.itemParent == "0123456789abc"
|
||||
|
||||
# Order
|
||||
theItem.setOrder(None)
|
||||
@@ -227,7 +227,7 @@ def testItemXMLPackUnpack(nwDummy):
|
||||
# Unpack
|
||||
assert theItem.unpackXML(xContent[0])
|
||||
assert theItem.itemHandle == "0123456789abc"
|
||||
assert theItem.parHandle == "0123456789abc"
|
||||
assert theItem.itemParent == "0123456789abc"
|
||||
assert theItem.itemOrder == 1
|
||||
assert theItem.isExpanded
|
||||
assert theItem.paraCount == 3
|
||||
|
||||
@@ -521,7 +521,7 @@ def testProjectOrphanedFiles(nwDummy, nwLipsum):
|
||||
assert oItem is not None
|
||||
assert oItem.itemName == "Mars"
|
||||
assert oItem.itemHandle == "636b6aa9b697b"
|
||||
assert oItem.parHandle is None
|
||||
assert oItem.itemParent is None
|
||||
assert oItem.itemClass == nwItemClass.WORLD
|
||||
assert oItem.itemType == nwItemType.FILE
|
||||
assert oItem.itemLayout == nwItemLayout.NOTE
|
||||
@@ -531,7 +531,7 @@ def testProjectOrphanedFiles(nwDummy, nwLipsum):
|
||||
assert oItem is not None
|
||||
assert oItem.itemName == "Orphaned File 1"
|
||||
assert oItem.itemHandle == "736b6aa9b697b"
|
||||
assert oItem.parHandle is None
|
||||
assert oItem.itemParent is None
|
||||
assert oItem.itemClass == nwItemClass.NO_CLASS
|
||||
assert oItem.itemType == nwItemType.FILE
|
||||
assert oItem.itemLayout == nwItemLayout.NO_LAYOUT
|
||||
|
||||
@@ -61,6 +61,7 @@ def testNumberWords():
|
||||
assert numberToWord(21, "en") == "Twenty-One"
|
||||
assert numberToWord(29, "en") == "Twenty-Nine"
|
||||
assert numberToWord(42, "en") == "Forty-Two"
|
||||
assert numberToWord(114, "en") == "One Hundred Fourteen"
|
||||
assert numberToWord(142, "en") == "One Hundred Forty-Two"
|
||||
assert numberToWord(999, "en") == "Nine Hundred Ninety-Nine"
|
||||
|
||||
|
||||