diff --git a/.gitignore b/.gitignore index 52d76a47..a2a18869 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ -# Setup +# Setup/Install MANIFEST dist/ +build/ deploy/ +*.spec # Documentation docs/build/ diff --git a/docs/source/images/python_win_install.png b/docs/source/images/python_win_install.png new file mode 100644 index 00000000..320305d0 Binary files /dev/null and b/docs/source/images/python_win_install.png differ diff --git a/docs/source/index.txt b/docs/source/index.txt index 766fbd5f..532aef70 100644 --- a/docs/source/index.txt +++ b/docs/source/index.txt @@ -10,6 +10,7 @@ This is the documentation for novelWriter |version|. :maxdepth: 2 introduction + started interface projects structure diff --git a/docs/source/started.txt b/docs/source/started.txt new file mode 100644 index 00000000..2ac3eec5 --- /dev/null +++ b/docs/source/started.txt @@ -0,0 +1,97 @@ +*************** +Getting Started +*************** + +You can download novelWriter from https://github.com/vkbo/novelWriter/releases + +Latest version is |version|: + +* ZIP file: https://github.com/vkbo/novelWriter/archive/v0.3.2.zip +* TAR file: https://github.com/vkbo/novelWriter/archive/v0.3.2.tar.gz + +Extract the archive to a location of your choice. + +Installing Dependencies +======================= + +If you already have Python installed, and don't mind starting novelWriter from command line, all you need to do is to open your command line tool, find the folder and run: + +.. code-block:: console + + python -m pip install -r requirements.txt + +On some operating systems you need to use ``python3`` instead of ``python``. + +The following Python packages are required to run novelWriter: + +* ``pyqt5`` for the GUI +* ``appdirs`` for locating the system's config folder +* ``lxml`` for writing project files + +.. note:: + Sometimes the SVG graphics package for pyqt5 must be installed separately. + +The following are optional, but recommended: + +* ``pyenchant`` for spell checking +* ``pycountry`` for translating language codes to language names +* ``latexcodec`` for escaping unicode characters in LaTeX export +* ``pypandoc`` for additional exports to Word, Open Office, eBooks, etc. + + +Running novelWriter +=================== + +If all the required dependencies are met, you can run novelWriter from the command line: + +.. code-block:: console + + python novelWriter.py + +A few switches are supported from the command line, mostly to assist in debugging if an error is encountered. +To list all options, run: + +.. code-block:: console + + python novelWriter.py --help + + +Building a Standalone Executable +================================ + +A standalone executable can be built with pyinstaller, using the python script named "install.py" in the source folder. +This script will automatically try to install all dependencies and build the standalone executable of novelWriter. +You can run the script by typing the following into your command prompt: + +.. code-block:: console + + python install.py + +If successful, the executable will be in the "dist" folder. + + +Additional Instructions for Windows +----------------------------------- + +If you don't have Python installed, you can download it from the python.org website. +The installers for Windows are available at https://www.python.org/downloads/windows/ + +novelWriter should work with Python 3.5 or higher, and the executable installer is the easiest to install. +Please note that the `pyenchant` package for spell checking does not currently work with the x86-64 version, so if you want spell checking, you must install the x86 version. + +Also, make sure you select the "Add Python to PATH" option. + +.. image:: images/python_win_install.png + :width: 600 + +Once Python is set up and running, you can either run novelWriter from the folder where you extracted it, or you can build an executable and run that from a desktop icon instead. + +An install script is provided to automatically pull all dependencies and build a single executable. +Open a command prompt in the folder where you extracted novelWriter, and run: + +.. code-block:: console + + python install.py + +If everything went well, you should find a "novelWriter.exe" file in a folder named "dist". +You can right-click it and create a desktop icon if you wish. diff --git a/install.py b/install.py new file mode 100755 index 00000000..00d6ac00 --- /dev/null +++ b/install.py @@ -0,0 +1,85 @@ +#!/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() + 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", + "--onefile", + "--add-data=%s%s%s" % (os.path.join("nw", "themes"), dotDot,"themes"), + "--add-data=%s%s%s" % (os.path.join("nw", "graphics"),dotDot,"graphics"), + "--icon=%s" % os.path.join("nw", "graphics", "novelWriter.ico"), +] +if buildWindowed: + instOpt.append("--windowed") + +instOpt.append("novelWriter.py") + +import PyInstaller.__main__ +PyInstaller.__main__.run(instOpt) + +print("") +print("##################") +print(" Build Finished") +print("##################") +print("") +print("If everything went well, the novelWriter executable should be in the folder named 'dist'") +print("") + diff --git a/nw/__init__.py b/nw/__init__.py index f3e8954b..91348d7e 100644 --- a/nw/__init__.py +++ b/nw/__init__.py @@ -10,8 +10,9 @@ """ -import logging +import sys import getopt +import logging from os import path, remove, rename from PyQt5.QtWidgets import QApplication @@ -126,15 +127,15 @@ def main(sysArgs): inOpts, inArgs = getopt.getopt(sysArgs,shortOpt,longOpt) except getopt.GetoptError: print(helpMsg) - exit(2) + sys.exit(2) for inOpt, inArg in inOpts: - if inOpt in ("-h","--help"): + if inOpt in ("-h","--help"): print(helpMsg) - exit() + sys.exit() elif inOpt in ("-v", "--version"): print("%s %s Version %s" % (__package__,__status__,__version__)) - exit() + sys.exit() elif inOpt in ("-d", "--debug"): debugLevel = logging.DEBUG debugStr = "{name:>22}:{lineno:<4d} {levelname:8} {message:}" @@ -196,6 +197,6 @@ def main(sysArgs): else: nwApp = QApplication([__package__]) nwGUI = GuiMain() - exit(nwApp.exec_()) + sys.exit(nwApp.exec_()) return diff --git a/nw/config.py b/nw/config.py index bf865b91..c7f4ab52 100644 --- a/nw/config.py +++ b/nw/config.py @@ -158,13 +158,13 @@ class Config: self.confFile = self.appHandle+".conf" self.homePath = path.expanduser("~") self.lastPath = self.homePath - self.appPath = path.dirname(__file__) + self.appPath = getattr(sys, "_MEIPASS", path.abspath(path.dirname(__file__))) self.appRoot = path.join(self.appPath,path.pardir) self.helpPath = path.join(self.appRoot,"help","en_GB") self.guiPath = path.join(self.appPath,"gui") self.themeRoot = path.join(self.appPath,"themes") - self.themePath = path.join(self.themeRoot) - self.appIcon = path.join(self.appRoot, nwFiles.APP_ICON) + self.graphPath = path.join(self.appPath,"graphics") + self.appIcon = path.join(self.graphPath, nwFiles.APP_ICON) # If config folder does not exist, make it. # This assumes that the os config folder itself exists. diff --git a/nw/graphics/novelWriter.ico b/nw/graphics/novelWriter.ico new file mode 100644 index 00000000..0a27ee4f Binary files /dev/null and b/nw/graphics/novelWriter.ico differ diff --git a/novelWriter.svg b/nw/graphics/novelWriter.svg similarity index 100% rename from novelWriter.svg rename to nw/graphics/novelWriter.svg diff --git a/nw/gui/configeditor.py b/nw/gui/configeditor.py index 9c82a2ea..2f13ef89 100644 --- a/nw/gui/configeditor.py +++ b/nw/gui/configeditor.py @@ -43,7 +43,7 @@ class GuiConfigEditor(QDialog): self.setWindowTitle("Preferences") - self.gradPath = path.abspath(path.join(self.mainConf.appPath,"graphics","gear.svg")) + self.gradPath = path.abspath(path.join(self.mainConf.graphPath,"gear.svg")) self.svgGradient = QSvgWidget(path.join(self.gradPath)) self.svgGradient.setFixedSize(QSize(64,64)) diff --git a/nw/gui/export.py b/nw/gui/export.py index f0b92250..1b33ade6 100644 --- a/nw/gui/export.py +++ b/nw/gui/export.py @@ -55,7 +55,7 @@ class GuiExport(QDialog): self.setWindowTitle("Export Project") self.setLayout(self.outerBox) - self.gradPath = path.abspath(path.join(self.mainConf.appPath,"graphics","export.svg")) + self.gradPath = path.abspath(path.join(self.mainConf.graphPath,"export.svg")) self.svgGradient = QSvgWidget(self.gradPath) self.svgGradient.setFixedSize(QSize(64,64)) diff --git a/nw/gui/itemeditor.py b/nw/gui/itemeditor.py index 52019bf9..d5ae459d 100644 --- a/nw/gui/itemeditor.py +++ b/nw/gui/itemeditor.py @@ -41,7 +41,7 @@ class GuiItemEditor(QDialog): self.setWindowTitle("Item Settings") - self.gradPath = path.abspath(path.join(self.mainConf.appPath,"graphics","gear.svg")) + self.gradPath = path.abspath(path.join(self.mainConf.graphPath,"gear.svg")) self.svgGradient = QSvgWidget(self.gradPath) self.svgGradient.setFixedSize(QSize(64,64)) diff --git a/nw/gui/projecteditor.py b/nw/gui/projecteditor.py index 7bcc4d62..bd42cdb5 100644 --- a/nw/gui/projecteditor.py +++ b/nw/gui/projecteditor.py @@ -43,7 +43,7 @@ class GuiProjectEditor(QDialog): self.setWindowTitle("Project Settings") self.setLayout(self.outerBox) - self.gradPath = path.abspath(path.join(self.mainConf.appPath,"graphics","gear.svg")) + self.gradPath = path.abspath(path.join(self.mainConf.graphPath,"gear.svg")) self.svgGradient = QSvgWidget(self.gradPath) self.svgGradient.setFixedSize(QSize(64,64)) diff --git a/nw/tools/spellenchant.py b/nw/tools/spellenchant.py index 4b56238d..b2faa472 100644 --- a/nw/tools/spellenchant.py +++ b/nw/tools/spellenchant.py @@ -32,6 +32,9 @@ class NWSpellEnchant(NWSpellCheck): return def setLanguage(self, theLang, projectDict=None): + """Load a dictionary for the language specified in the config. If that fails, we load a + dummy dictionary so that lookups don't crash. + """ try: if projectDict is None: self.theDict = enchant.Dict(theLang) @@ -40,7 +43,8 @@ class NWSpellEnchant(NWSpellCheck): logger.debug("Enchant spell checking for language %s loaded" % theLang) except: logger.error("Failed to load enchant spell checking for language %s" % theLang) - self.theDict = None + self.theDict = NWSpellEnchantDummy() + return def checkWord(self, theWord): @@ -73,3 +77,19 @@ class NWSpellEnchant(NWSpellCheck): return retList # END Class NWSpellEnchant + +class NWSpellEnchantDummy: + + def __init__(self): + return + + def check(self, theWord): + return True + + def suggest(self, theWord): + return [] + + def add_to_pwl(self, theWord): + return + +# END Class NWSpellEnchantDummy