"
+ )
+
+ # Other Checks
+ # ============
+
+ scItem = novelTree.topLevelItem(2)
+ scItem.setSelected(True)
+ assert scItem.isSelected()
+ novelTree.focusOutEvent(QFocusEvent(QEvent.None_, Qt.MouseFocusReason))
+ assert not scItem.isSelected()
+
# Close
# =====
From 211ab290d098b0b1feeba0c917ea7799e89a6fdf Mon Sep 17 00:00:00 2001
From: Rachel Powers <508861+Ryex@users.noreply.github.com>
Date: Fri, 8 Jul 2022 14:06:56 -0700
Subject: [PATCH 008/246] bring .desktop into compliance with Desktop Entry
specification 1.1 validated with `desktop-file-validate`
---
setup/data/novelwriter.desktop | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/setup/data/novelwriter.desktop b/setup/data/novelwriter.desktop
index 2666fa13..8b140ffc 100644
--- a/setup/data/novelwriter.desktop
+++ b/setup/data/novelwriter.desktop
@@ -1,10 +1,9 @@
[Desktop Entry]
Type=Application
-Encoding=UTF-8
Name=novelWriter
Comment=A markdown-like text editor for planning and writing novels
Exec=novelwriter %f
Icon=novelwriter
Categories=Qt;Office;WordProcessor;
Terminal=false
-MimeType=application/x-novelwriter-project
+MimeType=application/x-novelwriter-project;
From b363259a723a24377b5a35b8d2d22825ebe5737a Mon Sep 17 00:00:00 2001
From: Rachel Powers <508861+Ryex@users.noreply.github.com>
Date: Fri, 8 Jul 2022 14:08:13 -0700
Subject: [PATCH 009/246] fix spelling
---
setup/description_short.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup/description_short.txt b/setup/description_short.txt
index ac106560..d3d30690 100644
--- a/setup/description_short.txt
+++ b/setup/description_short.txt
@@ -2,5 +2,5 @@ novelWriter is a plain text editor designed for writing novels assembled from
many smaller text documents. It uses a minimal formatting syntax inspired by
Markdown, and adds a meta data syntax for comments, synopsis, and
cross-referencing. It's designed to be a simple text editor that allows for
-easy organisation of text and notes, using human readable text files as
+easy organization of text and notes, using human readable text files as
storage for robustness.
From 073a9acfb3b7e3f4c9378cc39570b347e4103e47 Mon Sep 17 00:00:00 2001
From: Rachel Powers <508861+Ryex@users.noreply.github.com>
Date: Fri, 8 Jul 2022 15:26:03 -0700
Subject: [PATCH 010/246] add a method to automate the building of an Appimage
package
---
setup.py | 223 ++++++++++++++++++++++++++++++++++
setup/novelwriter.appdata.xml | 21 ++++
2 files changed, 244 insertions(+)
create mode 100644 setup/novelwriter.appdata.xml
diff --git a/setup.py b/setup.py
index f72ce936..b7ce8318 100755
--- a/setup.py
+++ b/setup.py
@@ -835,10 +835,225 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False):
return
+##
+# Make Appimage (build-appimage)
+##
+
+def makeAppimage(sysArgs):
+ """Build an Appimage
+ """
+
+ import argparse
+ import platform
+ import glob
+
+ try:
+ import python_appimage
+ except ImportError:
+ print(
+ "ERROR: Package 'python-appimage' is missing on this system.\n"
+ " Please run 'pip install --user python-appimage' to install it.\n"
+ )
+ sys.exit(1)
+
+ print("")
+ print("Build Appimage")
+ print("==============")
+ print("")
+
+ plat = platform.machine()
+
+ parser = argparse.ArgumentParser(prog='build_appimage',
+ description='Build an Appimage',
+ epilog='see https://appimage.org/ for more details')
+ parser.add_argument('-l', '--linux-tag', nargs='?', default=f"manylinux2014_{plat}",
+ help=(
+ 'linux compatibility tag (e.g. manylinux1_x86_64) \n'
+ 'see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages \n'
+ 'and https://github.com/pypa/manylinux for a list of valid tags'
+ ))
+ parser.add_argument('-p', '--python-version', nargs='?', default='3.11',
+ help='python version (e.g. 3.11)')
+
+ args, unknown = parser.parse_known_args(sysArgs)
+
+ linuxTag = args.linux_tag
+ pythonVer = args.python_version
+
+ # Version Info
+ # ============
+
+ numVers, hexVers, relDate = extractVersion()
+ pkgVers = compactVersion(numVers)
+ relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d")
+ print("")
+
+ # Set Up Folder
+ # =============
+
+ bldDir = "dist_appimage"
+ bldPkg = f"novelwriter_{pkgVers}"
+ outDir = f"{bldDir}/{bldPkg}"
+ imageDir = f"{bldDir}/appimage"
+
+ # Set Up Folders
+ # ==============
+
+ if not os.path.isdir(bldDir):
+ os.mkdir(bldDir)
+
+ if os.path.isdir(outDir):
+ print("Removing old build files ...")
+ print("")
+ shutil.rmtree(outDir)
+
+ os.mkdir(outDir)
+
+ if os.path.isdir(imageDir):
+ print("Removing old build metadata files ...")
+ print("")
+ shutil.rmtree(imageDir)
+
+ os.mkdir(imageDir)
+
+ # Remove old Appimages
+ outFiles = glob.glob(f"{bldDir}/*.AppImage")
+
+ if outFiles:
+ print("Removing old Appimages")
+ print("")
+ for image in outFiles:
+ try:
+ os.remove(image)
+ except OSError:
+ print("Error while deleting file : ", image)
+
+ # Build Additional Assets
+ # =======================
+
+ buildQtI18n()
+ buildSampleZip()
+ buildPdfManual()
+
+ # Copy novelWriter Source
+ # =======================
+
+ print("Copying novelWriter source ...")
+ print("")
+
+ for nPath, _, nFiles in os.walk("novelwriter"):
+ if nPath.endswith("__pycache__"):
+ print("Skipped: %s" % nPath)
+ continue
+
+ pPath = f"{outDir}/{nPath}"
+ if not os.path.isdir(pPath):
+ os.mkdir(pPath)
+
+ fCount = 0
+ for fFile in nFiles:
+ nFile = f"{nPath}/{fFile}"
+ pFile = f"{pPath}/{fFile}"
+
+ if fFile.endswith(".pyc"):
+ print("Skipped: %s" % nFile)
+ continue
+
+ shutil.copyfile(nFile, pFile)
+ fCount += 1
+
+ print("Copied: %s/* [Files: %d]" % (nPath, fCount))
+
+ print("")
+ print("Copying or generating additional files ...")
+ print("")
+
+ # Copy/Write Root Files
+ # =====================
+
+ copyFiles = ["LICENSE.md", "CREDITS.md", "CHANGELOG.md", "pyproject.toml"]
+ for copyFile in copyFiles:
+ shutil.copyfile(copyFile, f"{outDir}/{copyFile}")
+ print("Copied: %s" % copyFile)
+
+ writeFile(f"{outDir}/MANIFEST.in", (
+ "include LICENSE.md\n"
+ "include CREDITS.md\n"
+ "include CHANGELOG.md\n"
+ "include data/*\n"
+ "recursive-include novelwriter/assets *\n"
+ ))
+ print("Wrote: MANIFEST.in")
+
+ writeFile(f"{outDir}/setup.py", (
+ "import setuptools\n"
+ "setuptools.setup()\n"
+ ))
+ print("Wrote: setup.py")
+
+ setupCfg = readFile("setup.cfg").replace(
+ "file: setup/description_pypi.md", "file: data/description_short.txt"
+ )
+ writeFile(f"{outDir}/setup.cfg", setupCfg)
+ print("Wrote: setup.cfg")
+
+ # Write Metadata
+ # ==============
+
+ appDescription = readFile("setup/description_short.txt")
+ appdataXML = readFile("setup/novelwriter.appdata.xml").format(description=appDescription)
+ writeFile(f"{imageDir}/novelwriter.appdata.xml", appdataXML)
+ print("Wrote: novelwriter.appdata.xml")
+
+ writeFile(f"{imageDir}/entrypoint.sh", (
+ '#! /bin/bash \n'
+ '{{ python-executable }} -sE ${APPDIR}/opt/python{{ python-version }}/bin/novelwriter "$@"'
+ ))
+ print("Wrote: entrypoint.sh")
+
+ writeFile(f"{imageDir}/requirements.txt", os.path.abspath(outDir))
+ print("Wrote: requirements.txt")
+
+ shutil.copyfile("setup/data/novelwriter.desktop", f"{imageDir}/novelwriter.desktop")
+ print("Copied: setup/data/novelwriter.desktop")
+
+ shutil.copyfile("setup/icons/novelwriter.svg", f"{imageDir}/novelwriter.svg")
+ print("Copied: setup/icons/novelwriter.svg")
+
+ shutil.copyfile("setup/data/hicolor/256x256/apps/novelwriter.png",
+ f"{imageDir}/novelwriter.png")
+ print("Copied: setup/data/hicolor/256x256/apps/novelwriter.png")
+
+ # Build Appimage
+ # ==============
+
+ try:
+ subprocess.call(
+ ["python", "-m", "python_appimage", "build", "app",
+ "-l", linuxTag, "-p", pythonVer, "appimage"], cwd=bldDir)
+ except Exception as exc:
+ print("Appimage build: FAILED")
+ print("")
+ print(str(exc))
+ print("")
+ print("Dependencies:")
+ print(" * pip install python-appimage")
+ print("")
+ sys.exit(1)
+
+ outFile = glob.glob(f"{bldDir}/*.AppImage")[0]
+ shaFile = makeCheckSum(os.path.basename(outFile), cwd=bldDir)
+
+ toUpload(outFile)
+ toUpload(shaFile)
+
+ return
+
##
# Make Windows Setup EXE (build-win-exe)
##
+
def makeWindowsEmbedded(sysArgs):
"""Set up a package with embedded Python and dependencies for
Windows installation.
@@ -1679,6 +1894,14 @@ if __name__ == "__main__":
makeWindowsEmbedded(sys.argv)
sys.exit(0) # Don't continue execution
+ if "build-appimage" in sys.argv:
+ sys.argv.remove("build-appimage")
+ if hostOS == OS_LINUX:
+ makeAppimage(sys.argv)
+ else:
+ print("ERROR: Command 'build-ubuntu' can only be used on Linux")
+ sys.exit(1)
+
# General Installers
# ==================
diff --git a/setup/novelwriter.appdata.xml b/setup/novelwriter.appdata.xml
new file mode 100644
index 00000000..98a6e33b
--- /dev/null
+++ b/setup/novelwriter.appdata.xml
@@ -0,0 +1,21 @@
+
+
+ novelwriter
+ GPL-3.0
+ GPL-3.0
+ novelWriter
+ A markdown-like text editor for planning and writing novels
+
+
{description}
+
+ novelwriter.desktop
+ https://novelwriter.io/
+
+
+ https://novelwriter.io/images/screenshot-multi.png
+
+
+
+ novelwriter.desktop
+
+
\ No newline at end of file
From 3dc4d1443fc58ec902962873b333b00d4c2654a0 Mon Sep 17 00:00:00 2001
From: Rachel Powers <508861+Ryex@users.noreply.github.com>
Date: Fri, 8 Jul 2022 18:40:54 -0700
Subject: [PATCH 011/246] ensure prefix matching of `build-appimage` args do
not shadow normal setup move defaults to pyhton 3.10 and manylinux2010 for
better compatability cleanup
---
setup.py | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/setup.py b/setup.py
index b7ce8318..15144a99 100755
--- a/setup.py
+++ b/setup.py
@@ -866,16 +866,16 @@ def makeAppimage(sysArgs):
parser = argparse.ArgumentParser(prog='build_appimage',
description='Build an Appimage',
epilog='see https://appimage.org/ for more details')
- parser.add_argument('-l', '--linux-tag', nargs='?', default=f"manylinux2014_{plat}",
+ parser.add_argument('--linux-tag', nargs='?', default=f"manylinux2010_{plat}",
help=(
'linux compatibility tag (e.g. manylinux1_x86_64) \n'
'see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages \n'
'and https://github.com/pypa/manylinux for a list of valid tags'
))
- parser.add_argument('-p', '--python-version', nargs='?', default='3.11',
- help='python version (e.g. 3.11)')
+ parser.add_argument('--python-version', nargs='?', default='3.10',
+ help='python version (e.g. 3.10)')
- args, unknown = parser.parse_known_args(sysArgs)
+ args, unparsedArgs = parser.parse_known_args(sysArgs)
linuxTag = args.linux_tag
pythonVer = args.python_version
@@ -1047,7 +1047,7 @@ def makeAppimage(sysArgs):
toUpload(outFile)
toUpload(shaFile)
- return
+ return unparsedArgs
##
# Make Windows Setup EXE (build-win-exe)
@@ -1897,9 +1897,9 @@ if __name__ == "__main__":
if "build-appimage" in sys.argv:
sys.argv.remove("build-appimage")
if hostOS == OS_LINUX:
- makeAppimage(sys.argv)
+ sys.argv = makeAppimage(sys.argv) # Build appimage and prune it's args
else:
- print("ERROR: Command 'build-ubuntu' can only be used on Linux")
+ print("ERROR: Command 'build-appimage' can only be used on Linux")
sys.exit(1)
# General Installers
From 82a5da391fa0392e6b9c594002638e267ee2c3ea Mon Sep 17 00:00:00 2001
From: Rachel Powers <508861+Ryex@users.noreply.github.com>
Date: Fri, 8 Jul 2022 18:53:01 -0700
Subject: [PATCH 012/246] - ensure `dist_appimage` removed during cleanup -
cleanup
---
setup.py | 30 +++++++++++++++++++-----------
1 file changed, 19 insertions(+), 11 deletions(-)
diff --git a/setup.py b/setup.py
index 15144a99..fdf8e072 100755
--- a/setup.py
+++ b/setup.py
@@ -197,6 +197,7 @@ def cleanBuildDirs():
removeFolder("dist")
removeFolder("dist_deb")
removeFolder("dist_minimal")
+ removeFolder("dist_appimage")
removeFolder("novelWriter.egg-info")
print("")
@@ -863,17 +864,24 @@ def makeAppimage(sysArgs):
plat = platform.machine()
- parser = argparse.ArgumentParser(prog='build_appimage',
- description='Build an Appimage',
- epilog='see https://appimage.org/ for more details')
- parser.add_argument('--linux-tag', nargs='?', default=f"manylinux2010_{plat}",
- help=(
- 'linux compatibility tag (e.g. manylinux1_x86_64) \n'
- 'see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages \n'
- 'and https://github.com/pypa/manylinux for a list of valid tags'
- ))
- parser.add_argument('--python-version', nargs='?', default='3.10',
- help='python version (e.g. 3.10)')
+ parser = argparse.ArgumentParser(
+ prog="build_appimage",
+ description="Build an Appimage",
+ epilog="see https://appimage.org/ for more details",
+ )
+ parser.add_argument(
+ "--linux-tag",
+ nargs="?",
+ default=f"manylinux2010_{plat}",
+ help=(
+ "linux compatibility tag (e.g. manylinux1_x86_64) \n"
+ "see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages \n"
+ "and https://github.com/pypa/manylinux for a list of valid tags"
+ ),
+ )
+ parser.add_argument(
+ "--python-version", nargs="?", default="3.10", help="python version (e.g. 3.10)"
+ )
args, unparsedArgs = parser.parse_known_args(sysArgs)
From 67aff2550bf827543111e1d508f685f3806415db Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 15 Jul 2022 14:38:37 +0200
Subject: [PATCH 013/246] Change look of outline tree
---
novelwriter/gui/noveltree.py | 4 +-
novelwriter/gui/outline.py | 162 +++++++++++++++--------------------
novelwriter/gui/projtree.py | 2 +-
3 files changed, 71 insertions(+), 97 deletions(-)
diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py
index b96f5331..0dec2551 100644
--- a/novelwriter/gui/noveltree.py
+++ b/novelwriter/gui/noveltree.py
@@ -174,7 +174,7 @@ class GuiNovelToolBar(QWidget):
self.mainTheme = novelView.mainGui.mainTheme
iPx = self.mainTheme.baseIconSize
- mPx = self.mainConf.pxInt(3)
+ mPx = self.mainConf.pxInt(2)
self.setContentsMargins(0, 0, 0, 0)
self.setAutoFillBackground(True)
@@ -396,8 +396,6 @@ class GuiNovelTree(QTreeWidget):
self.mainTheme.loadDecoration("deco_doc_h4", pxH=iPx),
]
self._pMore = self.mainTheme.loadDecoration("deco_doc_more", pxH=iPx)
- self._pActive = self.mainTheme.loadDecoration("deco_more_on", pxH=iPx)
- self._pInactive = self.mainTheme.loadDecoration("deco_more_off", pxH=iPx)
# Connect signals
self.clicked.connect(self._treeItemClicked)
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index 0ae253f9..49efe5f0 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -37,16 +37,16 @@ from PyQt5.QtCore import (
Qt, pyqtSignal, pyqtSlot, QSize, QT_TRANSLATE_NOOP
)
from PyQt5.QtWidgets import (
- QAbstractItemView, QAction, QGridLayout, QGroupBox, QHBoxLayout, QLabel,
- QMenu, QScrollArea, QSplitter, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
- QWidget, QFrame, QToolBar, QSizePolicy, QComboBox, QToolButton
+ QAbstractItemView, QAction, QComboBox, QFrame, QGridLayout, QGroupBox,
+ QHBoxLayout, QLabel, QMenu, QScrollArea, QSizePolicy, QSplitter, QToolBar,
+ QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
)
from novelwriter.enum import (
nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline
)
from novelwriter.common import checkInt
-from novelwriter.constants import trConst, nwKeyWords, nwLabels
+from novelwriter.constants import nwHeaders, trConst, nwKeyWords, nwLabels
logger = logging.getLogger(__name__)
@@ -313,6 +313,9 @@ class GuiOutlineTree(QTreeWidget):
nwOutline.SYNOP: False,
}
+ D_HANDLE = Qt.UserRole
+ D_TITLE = Qt.UserRole + 1
+
hiddenStateChanged = pyqtSignal()
activeItemChanged = pyqtSignal(str, str)
@@ -337,11 +340,35 @@ class GuiOutlineTree(QTreeWidget):
iPx = self.mainTheme.baseIconSize
self.setIconSize(QSize(iPx, iPx))
- self.setIndentation(iPx)
+ self.setIndentation(0)
self.treeHead = self.header()
self.treeHead.sectionMoved.connect(self._columnMoved)
+ # Pre-Generate Tree Formatting
+ fH1 = self.font()
+ fH1.setBold(True)
+ fH1.setUnderline(True)
+
+ fH2 = self.font()
+ fH2.setBold(True)
+
+ self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
+ self._pIndent = [
+ self.mainTheme.loadDecoration("deco_doc_h0", pxH=iPx),
+ self.mainTheme.loadDecoration("deco_doc_h1", pxH=iPx),
+ self.mainTheme.loadDecoration("deco_doc_h2", pxH=iPx),
+ self.mainTheme.loadDecoration("deco_doc_h3", pxH=iPx),
+ self.mainTheme.loadDecoration("deco_doc_h4", pxH=iPx),
+ ]
+ self._dIcon = {
+ "H0": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H0"),
+ "H1": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H1"),
+ "H2": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H2"),
+ "H3": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H3"),
+ "H4": self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H4"),
+ }
+
# Internals
self._treeOrder = []
self._colWidth = {}
@@ -449,7 +476,7 @@ class GuiOutlineTree(QTreeWidget):
tHandle = None
tLine = 0
if selItem:
- tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
+ tHandle = selItem[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
tLine = checkInt(selItem[0].text(self._colIdx[nwOutline.LINE]), 1) - 1
return tHandle, tLine
@@ -475,8 +502,8 @@ class GuiOutlineTree(QTreeWidget):
"""
selItems = self.selectedItems()
if selItems:
- tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], Qt.UserRole)
- sTitle = selItems[0].data(self._colIdx[nwOutline.LINE], Qt.UserRole)
+ tHandle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_HANDLE)
+ sTitle = selItems[0].data(self._colIdx[nwOutline.TITLE], self.D_TITLE)
self.activeItemChanged.emit(tHandle, sTitle)
return
@@ -614,8 +641,7 @@ class GuiOutlineTree(QTreeWidget):
self.setColumnWidth(self._colIdx[hItem], self._colWidth[hItem])
self.setColumnHidden(self._colIdx[hItem], self._colHidden[hItem])
- # Make sure title column is always visible,
- # and handle column always hidden
+ # Make sure title column is always visible
self.setColumnHidden(self._colIdx[nwOutline.TITLE], False)
headItem = self.headerItem()
@@ -623,101 +649,51 @@ class GuiOutlineTree(QTreeWidget):
headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
- currTitle = None
- currChapter = None
- currScene = None
-
novStruct = self.theProject.index.novelStructure(rootHandle=rootHandle, skipExcl=True)
for _, tHandle, sTitle, novIdx in novStruct:
- tItem = self._createTreeItem(tHandle, sTitle, novIdx)
+ iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0)
+ dLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
+ if iLevel == 0:
+ continue
- tLevel = novIdx.level
- if tLevel == "H1":
- self.addTopLevelItem(tItem)
- currTitle = tItem
- currChapter = None
- currScene = None
+ trItem = QTreeWidgetItem()
+ nwItem = self.theProject.tree[tHandle]
- elif tLevel == "H2":
- if currTitle is None:
- self.addTopLevelItem(tItem)
- else:
- currTitle.addChild(tItem)
- currChapter = tItem
- currScene = None
+ trItem.setData(self._colIdx[nwOutline.TITLE], Qt.DecorationRole, self._pIndent[iLevel])
+ trItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title)
+ trItem.setData(self._colIdx[nwOutline.TITLE], self.D_HANDLE, tHandle)
+ trItem.setData(self._colIdx[nwOutline.TITLE], self.D_TITLE, sTitle)
+ trItem.setFont(self._colIdx[nwOutline.TITLE], self._hFonts[iLevel])
+ trItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level)
+ trItem.setIcon(self._colIdx[nwOutline.LABEL], self._dIcon[dLevel])
+ trItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName)
+ trItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0"))
+ trItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis)
+ trItem.setText(self._colIdx[nwOutline.CCOUNT], f"{novIdx.charCount:n}")
+ trItem.setText(self._colIdx[nwOutline.WCOUNT], f"{novIdx.wordCount:n}")
+ trItem.setText(self._colIdx[nwOutline.PCOUNT], f"{novIdx.paraCount:n}")
+ trItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
+ trItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
+ trItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
- elif tLevel == "H3":
- if currChapter is None:
- if currTitle is None:
- self.addTopLevelItem(tItem)
- else:
- currTitle.addChild(tItem)
- else:
- currChapter.addChild(tItem)
- currScene = tItem
+ refs = self.theProject.index.getReferences(tHandle, sTitle)
+ trItem.setText(self._colIdx[nwOutline.POV], ", ".join(refs[nwKeyWords.POV_KEY]))
+ trItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(refs[nwKeyWords.FOCUS_KEY]))
+ trItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(refs[nwKeyWords.CHAR_KEY]))
+ trItem.setText(self._colIdx[nwOutline.PLOT], ", ".join(refs[nwKeyWords.PLOT_KEY]))
+ trItem.setText(self._colIdx[nwOutline.TIME], ", ".join(refs[nwKeyWords.TIME_KEY]))
+ trItem.setText(self._colIdx[nwOutline.WORLD], ", ".join(refs[nwKeyWords.WORLD_KEY]))
+ trItem.setText(self._colIdx[nwOutline.OBJECT], ", ".join(refs[nwKeyWords.OBJECT_KEY]))
+ trItem.setText(self._colIdx[nwOutline.ENTITY], ", ".join(refs[nwKeyWords.ENTITY_KEY]))
+ trItem.setText(self._colIdx[nwOutline.CUSTOM], ", ".join(refs[nwKeyWords.CUSTOM_KEY]))
- elif tLevel == "H4":
- if currScene is None:
- if currChapter is None:
- if currTitle is None:
- self.addTopLevelItem(tItem)
- else:
- currTitle.addChild(tItem)
- else:
- currChapter.addChild(tItem)
- else:
- currScene.addChild(tItem)
-
- tItem.setExpanded(True)
+ self.addTopLevelItem(trItem)
self._lastBuild = time()
return
- def _createTreeItem(self, tHandle, sTitle, novIdx):
- """Populate a tree item with all the column values.
- """
- nwItem = self.theProject.tree[tHandle]
- newItem = QTreeWidgetItem()
- hIcon = "doc_%s" % novIdx.level.lower()
-
- hLevel = self.theProject.index.getHandleHeaderLevel(tHandle)
- dIcon = self.mainTheme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, hLevel)
-
- cC = int(novIdx.charCount)
- wC = int(novIdx.wordCount)
- pC = int(novIdx.paraCount)
-
- newItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title)
- newItem.setData(self._colIdx[nwOutline.TITLE], Qt.UserRole, tHandle)
- newItem.setIcon(self._colIdx[nwOutline.TITLE], self.mainTheme.getIcon(hIcon))
- newItem.setText(self._colIdx[nwOutline.LEVEL], novIdx.level)
- newItem.setText(self._colIdx[nwOutline.LABEL], nwItem.itemName)
- newItem.setIcon(self._colIdx[nwOutline.LABEL], dIcon)
- newItem.setText(self._colIdx[nwOutline.LINE], sTitle[1:].lstrip("0"))
- newItem.setData(self._colIdx[nwOutline.LINE], Qt.UserRole, sTitle)
- newItem.setText(self._colIdx[nwOutline.SYNOP], novIdx.synopsis)
- newItem.setText(self._colIdx[nwOutline.CCOUNT], f"{cC:n}")
- newItem.setText(self._colIdx[nwOutline.WCOUNT], f"{wC:n}")
- newItem.setText(self._colIdx[nwOutline.PCOUNT], f"{pC:n}")
- newItem.setTextAlignment(self._colIdx[nwOutline.CCOUNT], Qt.AlignRight)
- newItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
- newItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight)
-
- theRefs = self.theProject.index.getReferences(tHandle, sTitle)
- newItem.setText(self._colIdx[nwOutline.POV], ", ".join(theRefs[nwKeyWords.POV_KEY]))
- newItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(theRefs[nwKeyWords.FOCUS_KEY]))
- newItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY]))
- newItem.setText(self._colIdx[nwOutline.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY]))
- newItem.setText(self._colIdx[nwOutline.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY]))
- newItem.setText(self._colIdx[nwOutline.WORLD], ", ".join(theRefs[nwKeyWords.WORLD_KEY]))
- newItem.setText(self._colIdx[nwOutline.OBJECT], ", ".join(theRefs[nwKeyWords.OBJECT_KEY]))
- newItem.setText(self._colIdx[nwOutline.ENTITY], ", ".join(theRefs[nwKeyWords.ENTITY_KEY]))
- newItem.setText(self._colIdx[nwOutline.CUSTOM], ", ".join(theRefs[nwKeyWords.CUSTOM_KEY]))
-
- return newItem
-
# END Class GuiOutlineTree
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index 13b0b619..acb5e8b4 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -177,7 +177,7 @@ class GuiProjectToolBar(QWidget):
self.mainTheme = projView.mainGui.mainTheme
iPx = self.mainTheme.baseIconSize
- mPx = self.mainConf.pxInt(3)
+ mPx = self.mainConf.pxInt(2)
self.setContentsMargins(0, 0, 0, 0)
self.setAutoFillBackground(True)
From 895beb6aa10457c4f360755141c8b7544d173730 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 15 Jul 2022 16:36:45 +0200
Subject: [PATCH 014/246] Revert change of spelling from UK to US
---
setup/description_short.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup/description_short.txt b/setup/description_short.txt
index d3d30690..ac106560 100644
--- a/setup/description_short.txt
+++ b/setup/description_short.txt
@@ -2,5 +2,5 @@ novelWriter is a plain text editor designed for writing novels assembled from
many smaller text documents. It uses a minimal formatting syntax inspired by
Markdown, and adds a meta data syntax for comments, synopsis, and
cross-referencing. It's designed to be a simple text editor that allows for
-easy organization of text and notes, using human readable text files as
+easy organisation of text and notes, using human readable text files as
storage for robustness.
From 338d158baedaedede1207c988442ba75d5dba401 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 15 Jul 2022 16:39:12 +0200
Subject: [PATCH 015/246] Make some minor changes to the setup script
---
setup.py | 33 +++++++++++++++++----------------
1 file changed, 17 insertions(+), 16 deletions(-)
diff --git a/setup.py b/setup.py
index fdf8e072..3bbefb9f 100755
--- a/setup.py
+++ b/setup.py
@@ -837,19 +837,19 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False):
##
-# Make Appimage (build-appimage)
+# Make AppImage (build-appimage)
##
-def makeAppimage(sysArgs):
+def makeAppImage(sysArgs):
"""Build an Appimage
"""
+ import glob
import argparse
import platform
- import glob
try:
- import python_appimage
+ import python_appimage # noqa F401
except ImportError:
print(
"ERROR: Package 'python-appimage' is missing on this system.\n"
@@ -858,21 +858,19 @@ def makeAppimage(sysArgs):
sys.exit(1)
print("")
- print("Build Appimage")
+ print("Build AppImage")
print("==============")
print("")
- plat = platform.machine()
-
parser = argparse.ArgumentParser(
prog="build_appimage",
- description="Build an Appimage",
+ description="Build an AppImage",
epilog="see https://appimage.org/ for more details",
)
parser.add_argument(
"--linux-tag",
nargs="?",
- default=f"manylinux2010_{plat}",
+ default=f"manylinux2010_{platform.machine()}",
help=(
"linux compatibility tag (e.g. manylinux1_x86_64) \n"
"see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages \n"
@@ -891,7 +889,7 @@ def makeAppimage(sysArgs):
# Version Info
# ============
- numVers, hexVers, relDate = extractVersion()
+ numVers, _, relDate = extractVersion()
pkgVers = compactVersion(numVers)
relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d")
print("")
@@ -928,7 +926,7 @@ def makeAppimage(sysArgs):
outFiles = glob.glob(f"{bldDir}/*.AppImage")
if outFiles:
- print("Removing old Appimages")
+ print("Removing old AppImages")
print("")
for image in outFiles:
try:
@@ -1036,11 +1034,12 @@ def makeAppimage(sysArgs):
# ==============
try:
- subprocess.call(
- ["python", "-m", "python_appimage", "build", "app",
- "-l", linuxTag, "-p", pythonVer, "appimage"], cwd=bldDir)
+ subprocess.call([
+ sys.executable, "-m", "python_appimage", "build", "app",
+ "-l", linuxTag, "-p", pythonVer, "appimage"
+ ], cwd=bldDir)
except Exception as exc:
- print("Appimage build: FAILED")
+ print("AppImage build: FAILED")
print("")
print(str(exc))
print("")
@@ -1804,6 +1803,8 @@ if __name__ == "__main__":
" Add --snapshot to make a snapshot package.",
" build-win-exe Build a setup.exe file with Python embedded for Windows.",
" The package must be built from a minimal windows zip file.",
+ " build-appimage Build an AppImage. Argument --linux-tag defaults to",
+ " manylinux1_x86_64 / i386, and --python-version to 3.10.",
"",
"System Install:",
"",
@@ -1905,7 +1906,7 @@ if __name__ == "__main__":
if "build-appimage" in sys.argv:
sys.argv.remove("build-appimage")
if hostOS == OS_LINUX:
- sys.argv = makeAppimage(sys.argv) # Build appimage and prune it's args
+ sys.argv = makeAppImage(sys.argv) # Build appimage and prune its args
else:
print("ERROR: Command 'build-appimage' can only be used on Linux")
sys.exit(1)
From 069e3baf67814bdc45869c3f3ffb077264857b30 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 31 Jul 2022 18:40:16 +0200
Subject: [PATCH 016/246] Add functionality to load and save last selected
outline novel, and fix a few inconsistencies
---
.../assets/icons/typicons_dark/icons.conf | 1 +
.../assets/icons/typicons_light/icons.conf | 1 +
novelwriter/core/project.py | 21 +++---
novelwriter/gui/outline.py | 69 ++++++++++++++-----
novelwriter/gui/projtree.py | 3 -
novelwriter/guimain.py | 21 ++----
sample/nwProject.nwx | 9 ++-
7 files changed, 68 insertions(+), 57 deletions(-)
diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf
index f372c60c..568c5b24 100644
--- a/novelwriter/assets/icons/typicons_dark/icons.conf
+++ b/novelwriter/assets/icons/typicons_dark/icons.conf
@@ -84,6 +84,7 @@ view_editor = mixed_edit.svg
view_novel = typ_book-grey.svg
view_outline = typ_puzzle-outline.svg
+deco_doc_h0 = nw_deco-h0.svg
deco_doc_h1 = nw_deco-h1.svg
deco_doc_h2 = nw_deco-h2.svg
deco_doc_h3 = nw_deco-h3.svg
diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf
index 4514eab9..1d6a6c0c 100644
--- a/novelwriter/assets/icons/typicons_light/icons.conf
+++ b/novelwriter/assets/icons/typicons_light/icons.conf
@@ -84,6 +84,7 @@ view_editor = mixed_edit.svg
view_novel = typ_book-grey.svg
view_outline = typ_puzzle-outline.svg
+deco_doc_h0 = nw_deco-h0.svg
deco_doc_h1 = nw_deco-h1.svg
deco_doc_h2 = nw_deco-h2.svg
deco_doc_h3 = nw_deco-h3.svg
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index fb81f033..4b470d69 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -97,7 +97,6 @@ class NWProject():
self.autoReplace = {} # Text to auto-replace on exports
self.titleFormat = {} # The formatting of titles for exports
self.spellCheck = False # Controls the spellcheck-as-you-type feature
- self.autoOutline = True # If true, the Project Outline is updated automatically
self.statusItems = None # Novel file progress status values
self.importItems = None # Note file importance values
self.lastEdited = None # The handle of the last file to be edited
@@ -258,7 +257,6 @@ class NWProject():
"section": "",
}
self.spellCheck = False
- self.autoOutline = True
self.statusItems = NWStatus(NWStatus.STATUS)
self.statusItems.write(None, self.tr("New"), (100, 100, 100))
self.statusItems.write(None, self.tr("Note"), (200, 50, 0))
@@ -608,8 +606,6 @@ class NWProject():
self.spellCheck = checkBool(xItem.text, False)
elif xItem.tag == "spellLang":
self.projSpell = checkString(xItem.text, None, True)
- elif xItem.tag == "autoOutline":
- self.autoOutline = checkBool(xItem.text, True)
elif xItem.tag == "lastEdited":
self.lastEdited = checkString(xItem.text, None, True)
elif xItem.tag == "lastViewed":
@@ -735,7 +731,6 @@ class NWProject():
self._packProjectValue(xSettings, "language", self.projLang)
self._packProjectValue(xSettings, "spellCheck", self.spellCheck)
self._packProjectValue(xSettings, "spellLang", self.projSpell)
- self._packProjectValue(xSettings, "autoOutline", self.autoOutline)
self._packProjectValue(xSettings, "lastEdited", self.lastEdited)
self._packProjectValue(xSettings, "lastViewed", self.lastViewed)
self._packProjectValue(xSettings, "lastNovel", self.lastNovel)
@@ -1096,14 +1091,6 @@ class NWProject():
self.setProjectChanged(True)
return True
- def setAutoOutline(self, theMode):
- """Enable/disable automatic update of project outline.
- """
- if self.autoOutline != theMode:
- self.autoOutline = theMode
- self.setProjectChanged(True)
- return self.autoOutline
-
def setTreeOrder(self, newOrder):
"""A list representing the linear/flattened order of project
items in the GUI project tree. The user can rearrange the order
@@ -1139,6 +1126,14 @@ class NWProject():
self.setProjectChanged(True)
return True
+ def setLastOutlineViewed(self, tHandle):
+ """Set last viewed novel root in the outline view.
+ """
+ if self.lastOutline != tHandle:
+ self.lastOutline = tHandle
+ self.setProjectChanged(True)
+ return True
+
def setStatusColours(self, newCols, delCols):
"""Update the list of novel file status flags.
"""
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index 49efe5f0..7c35ebb7 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -59,8 +59,9 @@ class GuiOutlineView(QWidget):
def __init__(self, mainGui):
QWidget.__init__(self, mainGui)
- self.mainConf = novelwriter.CONFIG
- self.mainGui = mainGui
+ self.mainConf = novelwriter.CONFIG
+ self.mainGui = mainGui
+ self.theProject = mainGui.theProject
# Build GUI
self.outlineBar = GuiOutlineToolBar(self)
@@ -96,26 +97,43 @@ class GuiOutlineView(QWidget):
# Methods
##
- def splitSizes(self):
- return self.splitOutline.sizes()
-
- def clearOutline(self):
- self.outlineData.clearDetails()
- return
-
def initOutline(self):
self.outlineTree.initOutline()
self.outlineData.initDetails()
return
+ def refreshTree(self):
+ """Refresh the current tree.
+ """
+ self.outlineTree.refreshTree(rootHandle=self.theProject.lastOutline)
+ return
+
+ def clearProject(self):
+ self.outlineData.clearDetails()
+ return
+
+ def openProjectTasks(self):
+ """Run opening project tasks.
+ """
+ lastOutline = self.theProject.lastOutline
+ if not (lastOutline in self.theProject.tree or lastOutline is None):
+ lastOutline = self.theProject.tree.findRoot(nwItemClass.NOVEL)
+
+ logger.debug("Setting outline tree to root item '%s'", lastOutline)
+
+ self.clearProject()
+ self.outlineBar.populateNovelList()
+ self.outlineBar.setCurrentRoot(lastOutline)
+
+ return
+
def closeOutline(self):
self.outlineTree.closeOutline()
self.outlineData.updateClasses()
return
- def refreshView(self, overRide=False, novelChanged=False):
- self.outlineTree.refreshTree(overRide=overRide, novelChanged=novelChanged)
- return
+ def splitSizes(self):
+ return self.splitOutline.sizes()
def treeHasFocus(self):
return self.outlineTree.hasFocus()
@@ -243,6 +261,17 @@ class GuiOutlineToolBar(QToolBar):
self.novelValue.addItem(tIcon, self.tr("All Novel Folders"), "")
return
+ def setCurrentRoot(self, rootHandle):
+ """Set the current active root handle.
+ """
+ if rootHandle is None:
+ rootIdx = self.novelValue.count() - 1
+ else:
+ rootIdx = self.novelValue.findData(rootHandle)
+ if rootIdx >= 0:
+ self.novelValue.setCurrentIndex(rootIdx)
+ return
+
def setColumnHiddenState(self, hiddenState):
"""Forward the change of column hidden states to the menu.
"""
@@ -379,7 +408,7 @@ class GuiOutlineTree(QTreeWidget):
self._lastBuild = 0
self.initOutline()
- self.clearOutline()
+ self.clearContent()
self.hiddenStateChanged.emit()
@@ -415,7 +444,7 @@ class GuiOutlineTree(QTreeWidget):
return
- def clearOutline(self):
+ def clearContent(self):
"""Clear the tree and header and set the default values for the
columns arrays.
"""
@@ -453,10 +482,12 @@ class GuiOutlineTree(QTreeWidget):
# If the novel index or novel tree has changed since the tree
# was last built, we rebuild the tree from the updated index.
indexChanged = self.theProject.index.rootChangedSince(rootHandle, self._lastBuild)
- doBuild = (novelChanged or indexChanged) and self.theProject.autoOutline
- if doBuild or overRide:
- logger.debug("Rebuilding Project Outline")
- self._populateTree(rootHandle)
+ if not (novelChanged or indexChanged or overRide):
+ logger.verbose("No changes have been made to the novel index")
+ return
+
+ self._populateTree(rootHandle)
+ self.theProject.setLastOutlineViewed(rootHandle or None)
return
@@ -464,7 +495,7 @@ class GuiOutlineTree(QTreeWidget):
"""Called before a project is closed.
"""
self._saveHeaderState()
- self.clearOutline()
+ self.clearContent()
self._firstView = True
return
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index acb5e8b4..a1b5bf8e 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -55,7 +55,6 @@ class GuiProjectView(QWidget):
# Signals triggered when the meta data values of items change
treeItemChanged = pyqtSignal(str)
- novelItemChanged = pyqtSignal(str)
rootFolderChanged = pyqtSignal(str)
wordCountsChanged = pyqtSignal()
@@ -1356,8 +1355,6 @@ class GuiProjectTree(QTreeWidget):
itemType = tItem.itemType
if itemType == nwItemType.ROOT:
self.projView.rootFolderChanged.emit(tHandle)
- elif itemType == nwItemType.FILE and tItem.isNovelLike():
- self.projView.novelItemChanged.emit(tHandle)
self.projView.treeItemChanged.emit(tHandle)
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index e34b873e..6374e768 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -199,7 +199,6 @@ class GuiMain(QMainWindow):
self.projView.selectedItemChanged.connect(self.itemDetails.updateViewBox)
self.projView.openDocumentRequest.connect(self._openDocument)
- self.projView.novelItemChanged.connect(self._treeNovelItemChanged)
self.projView.wordCountsChanged.connect(self._updateStatusWordCount)
self.projView.treeItemChanged.connect(self.docEditor.updateDocInfo)
self.projView.treeItemChanged.connect(self.docViewer.updateDocInfo)
@@ -303,7 +302,7 @@ class GuiMain(QMainWindow):
self.docEditor.clearEditor()
self.docEditor.setDictionaries()
self.closeDocViewer()
- self.outlineView.clearOutline()
+ self.outlineView.clearProject()
# General
self.statusBar.clearStatus()
@@ -365,8 +364,8 @@ class GuiMain(QMainWindow):
self.rebuildTrees()
self.saveProject()
self.docEditor.setDictionaries()
- self.outlineView.updateRootItem(None)
self.novelView.openProjectTasks()
+ self.outlineView.openProjectTasks()
self.rebuildIndex(beQuiet=True)
self.statusBar.setRefTime(self.theProject.projOpened)
self.statusBar.setProjectStatus(nwState.GOOD)
@@ -514,8 +513,8 @@ class GuiMain(QMainWindow):
self.docEditor.setDictionaries()
self.docEditor.toggleSpellCheck(self.theProject.spellCheck)
self.statusBar.setRefTime(self.theProject.projOpened)
- self.outlineView.updateRootItem(None)
self.novelView.openProjectTasks()
+ self.outlineView.openProjectTasks()
self._updateStatusWordCount()
# Restore previously open documents, if any
@@ -1552,18 +1551,6 @@ class GuiMain(QMainWindow):
return
- @pyqtSlot()
- def _treeNovelItemChanged(self):
- """Triggered when there is a change to a novel item in the
- project tree.
- """
- if self.mainStack.currentIndex() == self.idxOutlineView:
- logger.verbose("Novel tree changed while Outline tab active")
- if self.hasProject:
- self.outlineView.refreshView(novelChanged=True)
-
- return
-
@pyqtSlot()
def _keyPressReturn(self):
"""Forward the return/enter keypress to the function that opens
@@ -1593,7 +1580,7 @@ class GuiMain(QMainWindow):
elif stIndex == self.idxOutlineView:
logger.verbose("Outline View activated")
if self.hasProject:
- self.outlineView.refreshView()
+ self.outlineView.refreshTree()
return
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index 6ba4b5f1..b5bf2647 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,24 +1,23 @@
-
+Sample ProjectSample ProjectJane SmithJay Doh
- 1371
+ 1378236
- 69222
+ 69273Falseen_GBTrueNone
- True636b6aa9b697b636b6aa9b697b7031beac91f75
- None
+ 7031beac91f751363954409
From bb2c78c0f6c9eee90d2d20c615d0a9e0c763a4b8 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Sun, 31 Jul 2022 18:50:43 +0200
Subject: [PATCH 017/246] Update tests
---
tests/lipsum/nwProject.nwx | 1 -
tests/minimal/nwProject.nwx | 1 -
tests/reference/coreProject_NewCustomA_nwProject.nwx | 3 +--
tests/reference/coreProject_NewCustomB_nwProject.nwx | 1 -
tests/reference/coreProject_NewFileFolder_nwProject.nwx | 3 +--
tests/reference/coreProject_NewMinimal_nwProject.nwx | 3 +--
tests/reference/coreProject_NewRoot_nwProject.nwx | 3 +--
tests/reference/guiEditor_Main_Final_nwProject.nwx | 5 ++---
tests/reference/guiEditor_Main_Initial_nwProject.nwx | 3 +--
tests/reference/guiProjSettings_Dialog_nwProject.nwx | 3 +--
tests/test_core/test_core_project.py | 6 ------
tests/test_gui/test_gui_guimain.py | 4 +---
tests/test_gui/test_gui_outline.py | 9 ++-------
13 files changed, 11 insertions(+), 34 deletions(-)
diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx
index a8cf6393..4eac7bc6 100644
--- a/tests/lipsum/nwProject.nwx
+++ b/tests/lipsum/nwProject.nwx
@@ -13,7 +13,6 @@
en_GBFalseNone
- True7a992350f3eb6NoneNone
diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx
index 430ecf17..5862d48c 100644
--- a/tests/minimal/nwProject.nwx
+++ b/tests/minimal/nwProject.nwx
@@ -14,7 +14,6 @@
en_GBFalseNone
- TrueNoneNoneNone
diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx
index f046fceb..48ae6363 100644
--- a/tests/reference/coreProject_NewCustomA_nwProject.nwx
+++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx
@@ -1,5 +1,5 @@
-
+Test CustomTest Novel
@@ -14,7 +14,6 @@
NoneFalseNone
- TrueNoneNoneNone
diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx
index 5d02172c..9399dd3f 100644
--- a/tests/reference/coreProject_NewCustomB_nwProject.nwx
+++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx
@@ -14,7 +14,6 @@
NoneFalseNone
- TrueNoneNoneNone
diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
index 20aeb027..b8df1c93 100644
--- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx
+++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx
@@ -1,5 +1,5 @@
-
+New ProjectNew Novel
@@ -13,7 +13,6 @@
NoneFalseNone
- TrueNoneNoneNone
diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx
index ba08600f..633f9f4c 100644
--- a/tests/reference/coreProject_NewMinimal_nwProject.nwx
+++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx
@@ -1,5 +1,5 @@
-
+New Project
@@ -12,7 +12,6 @@
NoneFalseNone
- TrueNoneNoneNone
diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx
index 0a606137..9102601d 100644
--- a/tests/reference/coreProject_NewRoot_nwProject.nwx
+++ b/tests/reference/coreProject_NewRoot_nwProject.nwx
@@ -1,5 +1,5 @@
-
+New ProjectNew Novel
@@ -13,7 +13,6 @@
NoneFalseNone
- TrueNoneNoneNone
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx
index ecc96604..884ab272 100644
--- a/tests/reference/guiEditor_Main_Final_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx
@@ -1,5 +1,5 @@
-
+New ProjectNew Novel
@@ -13,11 +13,10 @@
NoneTrueNone
- True000000000000fNone0000000000008
- None
+ 000000000000812910227
diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
index 1a79d2c2..0563c440 100644
--- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
@@ -1,5 +1,5 @@
-
+New ProjectNew Novel
@@ -13,7 +13,6 @@
NoneFalseNone
- TrueNoneNoneNone
diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx
index 883cb26d..1db9d48c 100644
--- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx
+++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx
@@ -1,5 +1,5 @@
-
+Project NameProject Title
@@ -14,7 +14,6 @@
NoneFalseen
- TrueNoneNoneNone
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index a6409b98..3be812e1 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -934,12 +934,6 @@ def testCoreProject_Methods(monkeypatch, mockGUI, tmpDir, fncDir, mockRnd):
assert theProject.localLookup(1) == "One"
assert theProject.localLookup(10) == "Ten"
- # Automatic outline update
- theProject.projChanged = False
- assert theProject.setAutoOutline(True)
- assert not theProject.setAutoOutline(False)
- assert theProject.projChanged
-
# Last edited
theProject.projChanged = False
assert theProject.setLastEdited("0123456789abc")
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index 6407b84b..a27fc816 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -149,9 +149,7 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, fncProj, mockRnd):
with monkeypatch.context() as mp:
mp.setattr(GuiOutlineView, "treeHasFocus", lambda *a: True)
assert nwGUI.docEditor.docHandle() is None
- actItem = nwGUI.outlineView.outlineTree.topLevelItem(0)
- chpItem = actItem.child(0)
- selItem = chpItem.child(0)
+ selItem = nwGUI.outlineView.outlineTree.topLevelItem(2)
nwGUI.outlineView.outlineTree.setCurrentItem(selItem)
nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle() == sHandle
diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py
index 26ca9803..f530b418 100644
--- a/tests/test_gui/test_gui_outline.py
+++ b/tests/test_gui/test_gui_outline.py
@@ -232,9 +232,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum):
assert outlineData.pCValue.text() == "3"
# Scene One
- actItem = outlineTree.topLevelItem(1)
- chpItem = actItem.child(0)
- selItem = chpItem.child(0)
+ selItem = outlineTree.topLevelItem(4)
outlineTree.setCurrentItem(selItem)
tHandle, tLine = outlineTree.getSelectedHandle()
@@ -252,10 +250,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, nwLipsum):
assert nwGUI.docViewer.docHandle() == "4c4f28287af27"
# Scene One, Section Two
- actItem = outlineTree.topLevelItem(1)
- chpItem = actItem.child(0)
- scnItem = chpItem.child(0)
- selItem = scnItem.child(0)
+ selItem = outlineTree.topLevelItem(5)
outlineTree.setCurrentItem(selItem)
tHandle, tLine = outlineTree.getSelectedHandle()
From cc7ae316ad730ce5bf42aeaa49e37db252fd97ca Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Mon, 15 Aug 2022 16:08:25 +0200
Subject: [PATCH 018/246] Make some minor changes for consistency between
similar classes
---
novelwriter/gui/noveltree.py | 10 +++++----
novelwriter/gui/outline.py | 36 ++++++++++++++++++------------
novelwriter/guimain.py | 6 ++---
tests/test_gui/test_gui_outline.py | 4 ++--
4 files changed, 33 insertions(+), 23 deletions(-)
diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py
index 0dec2551..c8d88b88 100644
--- a/novelwriter/gui/noveltree.py
+++ b/novelwriter/gui/noveltree.py
@@ -69,8 +69,8 @@ class GuiNovelView(QWidget):
self.theProject = mainGui.theProject
# Build GUI
- self.novelTree = GuiNovelTree(self)
self.novelBar = GuiNovelToolBar(self)
+ self.novelTree = GuiNovelTree(self)
# Assemble
self.outerBox = QVBoxLayout()
@@ -93,6 +93,8 @@ class GuiNovelView(QWidget):
##
def initSettings(self):
+ """Initialise GUI elements that depend on specific settings.
+ """
self.novelTree.initSettings()
return
@@ -110,7 +112,7 @@ class GuiNovelView(QWidget):
return
def openProjectTasks(self):
- """Run opening project tasks.
+ """Run open project tasks.
"""
lastNovel = self.theProject.lastNovel
if lastNovel not in self.theProject.tree:
@@ -136,8 +138,8 @@ class GuiNovelView(QWidget):
self.theProject.options.setValue("GuiNovelView", "lastCol", lastColType)
return
- def setFocus(self):
- """Forward the set focus call to the tree widget.
+ def setTreeFocus(self):
+ """Set the focus to the tree widget.
"""
self.novelTree.setFocus()
return
diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py
index 7c35ebb7..e8514c15 100644
--- a/novelwriter/gui/outline.py
+++ b/novelwriter/gui/outline.py
@@ -97,9 +97,11 @@ class GuiOutlineView(QWidget):
# Methods
##
- def initOutline(self):
- self.outlineTree.initOutline()
- self.outlineData.initDetails()
+ def initSettings(self):
+ """Initialise GUI elements that depend on specific settings.
+ """
+ self.outlineTree.initSettings()
+ self.outlineData.initSettings()
return
def refreshTree(self):
@@ -109,11 +111,13 @@ class GuiOutlineView(QWidget):
return
def clearProject(self):
+ """Clear project-related GUI content.
+ """
self.outlineData.clearDetails()
return
def openProjectTasks(self):
- """Run opening project tasks.
+ """Run open project tasks.
"""
lastOutline = self.theProject.lastOutline
if not (lastOutline in self.theProject.tree or lastOutline is None):
@@ -127,20 +131,24 @@ class GuiOutlineView(QWidget):
return
- def closeOutline(self):
- self.outlineTree.closeOutline()
+ def closeProjectTasks(self):
+ self.outlineTree.closeProjectTasks()
self.outlineData.updateClasses()
return
def splitSizes(self):
return self.splitOutline.sizes()
- def treeHasFocus(self):
- return self.outlineTree.hasFocus()
-
def setTreeFocus(self):
+ """Set the focus to the tree widget.
+ """
return self.outlineTree.setFocus()
+ def treeHasFocus(self):
+ """Check if the outline tree has focus.
+ """
+ return self.outlineTree.hasFocus()
+
##
# Public Slots
##
@@ -407,7 +415,7 @@ class GuiOutlineTree(QTreeWidget):
self._firstView = True
self._lastBuild = 0
- self.initOutline()
+ self.initSettings()
self.clearContent()
self.hiddenStateChanged.emit()
@@ -428,7 +436,7 @@ class GuiOutlineTree(QTreeWidget):
# Methods
##
- def initOutline(self):
+ def initSettings(self):
"""Set or update outline settings.
"""
# Scroll bars
@@ -491,7 +499,7 @@ class GuiOutlineTree(QTreeWidget):
return
- def closeOutline(self):
+ def closeProjectTasks(self):
"""Called before a project is closed.
"""
self._saveHeaderState()
@@ -971,13 +979,13 @@ class GuiOutlineDetails(QScrollArea):
self.setWidgetResizable(True)
self.setFrameStyle(QFrame.NoFrame)
- self.initDetails()
+ self.initSettings()
logger.debug("GuiOutlineDetails initialisation complete")
return
- def initDetails(self):
+ def initSettings(self):
"""Set or update outline settings.
"""
# Scroll bars
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index 6374e768..bad8c904 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -421,7 +421,7 @@ class GuiMain(QMainWindow):
if saveOK:
self.closeDocument()
self.docViewer.clearNavHistory()
- self.outlineView.closeOutline()
+ self.outlineView.closeProjectTasks()
self.novelView.closeProjectTasks()
self.theProject.closeProject(self.idleTime)
@@ -930,7 +930,7 @@ class GuiMain(QMainWindow):
self.docViewer.initViewer()
self.projView.initSettings()
self.novelView.initSettings()
- self.outlineView.initOutline()
+ self.outlineView.initSettings()
self._updateStatusWordCount()
return
@@ -1191,7 +1191,7 @@ class GuiMain(QMainWindow):
if tabIdx == self.idxProjView:
self.projView.setFocus()
elif tabIdx == self.idxNovelView:
- self.novelView.setFocus()
+ self.novelView.setTreeFocus()
elif paneNo == nwWidget.EDITOR:
self._changeView(nwView.EDITOR)
self.docEditor.setFocus()
diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py
index f530b418..2f33cf7a 100644
--- a/tests/test_gui/test_gui_outline.py
+++ b/tests/test_gui/test_gui_outline.py
@@ -54,7 +54,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir):
# Toggle scrollbars
nwGUI.mainConf.hideVScroll = True
nwGUI.mainConf.hideHScroll = True
- outlineView.initOutline()
+ outlineView.initSettings()
assert outlineTree.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
assert outlineTree.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
@@ -62,7 +62,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, fncDir):
nwGUI.mainConf.hideVScroll = False
nwGUI.mainConf.hideHScroll = False
- outlineView.initOutline()
+ outlineView.initSettings()
assert outlineTree.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded
assert outlineTree.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded
assert outlineData.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded
From c917fc9c9c104959a175ffcdd55379a4a2d11be8 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 17 Aug 2022 21:49:41 +0200
Subject: [PATCH 019/246] Fix issue #1096 and remove a couple of error ourputs
---
novelwriter/config.py | 2 +-
novelwriter/core/spellcheck.py | 44 +++++++++++++++++++++++-----------
novelwriter/gui/doceditor.py | 2 +-
3 files changed, 32 insertions(+), 16 deletions(-)
diff --git a/novelwriter/config.py b/novelwriter/config.py
index c5159493..e52a7bd7 100644
--- a/novelwriter/config.py
+++ b/novelwriter/config.py
@@ -360,7 +360,7 @@ class Config:
# Check the availability of optional packages
self._checkOptionalPackages()
- if self.spellLanguage is None:
+ if not self.spellLanguage:
self.spellLanguage = "en"
# Look for a PDF version of the manual
diff --git a/novelwriter/core/spellcheck.py b/novelwriter/core/spellcheck.py
index 8f2fcea9..d9c506cd 100644
--- a/novelwriter/core/spellcheck.py
+++ b/novelwriter/core/spellcheck.py
@@ -26,6 +26,8 @@ along with this program. If not, see .
import os
import logging
+from collections import namedtuple
+
from novelwriter.error import logException
logger = logging.getLogger(__name__)
@@ -46,36 +48,47 @@ class NWSpellEnchant():
return
##
- # Getters and Setters
+ # Properties
##
+ @property
def spellLanguage(self):
return self._spellLanguage
+ ##
+ # Setters
+ ##
+
def setLanguage(self, theLang, projectDict=None):
"""Load a dictionary for the language specified in the config.
If that fails, we load a mock dictionary so that lookups don't
- crash.
+ crash. Note that enchant will allow loading an empty string as
+ a tag, but this will fail later on. See issue #1096.
"""
+ self._theBroker = None
+ self._theDict = None
+ self._spellLanguage = None
+
try:
import enchant
- if self._theBroker is not None:
- logger.debug("Deleting old pyenchant broker")
- del self._theBroker
- self._theBroker = enchant.Broker()
- self._theDict = self._theBroker.request_dict(theLang)
- self._spellLanguage = theLang
- logger.debug("Enchant spell checking for language '%s' loaded", theLang)
+ if theLang and enchant.dict_exists(theLang):
+ self._theBroker = enchant.Broker()
+ self._theDict = self._theBroker.request_dict(theLang)
+ self._spellLanguage = theLang
+ logger.debug("Enchant spell checking for language '%s' loaded", theLang)
+ else:
+ logger.warning("Enchant found no dictionary for language '%s'", theLang)
except Exception:
logger.error("Failed to load enchant spell checking for language '%s'", theLang)
- self._theDict = FakeEnchant()
- self._spellLanguage = None
- self._readProjectDictionary(projectDict)
- for pWord in self._projDict:
- self._theDict.add_to_session(pWord)
+ if self._theDict is None:
+ self._theDict = FakeEnchant()
+ else:
+ self._readProjectDictionary(projectDict)
+ for pWord in self._projDict:
+ self._theDict.add_to_session(pWord)
return
@@ -189,6 +202,9 @@ class FakeEnchant:
"""Fallback for when Enchant is selected, but not installed.
"""
def __init__(self):
+ self.tag = ""
+ self.provider = namedtuple("provider", "name")
+ self.provider.name = ""
return
def check(self, theWord):
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 4d35c978..59275295 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -718,7 +718,7 @@ class GuiDocEditor(QTextEdit):
), nwAlert.INFO)
theMode = False
- if self.spEnchant.spellLanguage() is None:
+ if self.spEnchant.spellLanguage is None:
theMode = False
self._spellCheck = theMode
From 2178391bcd793fc72583a87976f7e536881c620b Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 17 Aug 2022 21:49:59 +0200
Subject: [PATCH 020/246] Update tests of spell check classes
---
tests/test_core/test_core_spellcheck.py | 85 +++++++++++++++++++++----
1 file changed, 73 insertions(+), 12 deletions(-)
diff --git a/tests/test_core/test_core_spellcheck.py b/tests/test_core/test_core_spellcheck.py
index 4835e667..01615809 100644
--- a/tests/test_core/test_core_spellcheck.py
+++ b/tests/test_core/test_core_spellcheck.py
@@ -26,29 +26,57 @@ import pytest
from mock import causeOSError
from tools import readFile, writeFile
-from novelwriter.core.spellcheck import NWSpellEnchant
+from novelwriter.core.spellcheck import FakeEnchant, NWSpellEnchant
@pytest.mark.core
-def testCoreSpell_Enchant(monkeypatch, tmpDir):
- """Test the pyenchant spell checker
+def testCoreSpell_FakeEnchant(monkeypatch):
+ """Test the FakeEnchant spell checker fallback.
"""
- wList = os.path.join(tmpDir, "wordlist.txt")
- writeFile(wList, "a_word\nb_word\nc_word\n")
-
- # Block the enchant package (and trigger the default class)
+ # Make package import fail
with monkeypatch.context() as mp:
mp.setitem(sys.modules, "enchant", None)
spChk = NWSpellEnchant()
+ spChk.setLanguage("en", "")
+ assert isinstance(spChk._theDict, FakeEnchant)
- spChk.setLanguage("en", wList)
- assert spChk.setLanguage("", "") is None
- assert spChk.checkWord("") is True
- assert spChk.suggestWords("") == []
+ # Request a non-existent dictionary
+ spChk = NWSpellEnchant()
+ spChk.setLanguage("whatchamajig", "")
+ assert isinstance(spChk._theDict, FakeEnchant)
+
+ # Request an emety language string
+ # See issue https://github.com/vkbo/novelWriter/issues/1096
+ spChk = NWSpellEnchant()
+ spChk.setLanguage("", "")
+ assert isinstance(spChk._theDict, FakeEnchant)
+
+ # FakeEnchant should handle requests
+ fkChk = FakeEnchant()
+ assert fkChk.tag == ""
+ assert fkChk.provider.name == ""
+ assert fkChk.check("whatchamajig") is True
+ assert fkChk.suggest("whatchamajig") == []
+ assert fkChk.add_to_session("whatchamajig") is None
+
+# END Test testCoreSpell_FakeEnchant
+
+
+@pytest.mark.core
+def testCoreSpell_Enchant(monkeypatch, fncDir):
+ """Test the pyenchant spell checker.
+ """
+ wList = os.path.join(fncDir, "wordlist.txt")
+ writeFile(wList, "a_word\nb_word\nc_word\n")
+
+ # Break the enchant package, and check error handling
+ with monkeypatch.context() as mp:
+ mp.setitem(sys.modules, "enchant", None)
+ spChk = NWSpellEnchant()
assert spChk.listDictionaries() == []
assert spChk.describeDict() == ("", "")
- # Break the enchant package, and check error handling
+ # Set the dict to None, and check dictionary call error handling
spChk = NWSpellEnchant()
spChk.theDict = None
assert spChk.checkWord("word") is True
@@ -59,6 +87,7 @@ def testCoreSpell_Enchant(monkeypatch, tmpDir):
spChk = NWSpellEnchant()
spChk.setLanguage("en", wList)
spChk.setLanguage("en", wList)
+ assert spChk.spellLanguage == "en"
# Add a word to the user's dictionary
assert spChk._readProjectDictionary("stuff") is False
@@ -102,3 +131,35 @@ def testCoreSpell_Enchant(monkeypatch, tmpDir):
assert aName != ""
# END Test testCoreSpell_Enchant
+
+
+@pytest.mark.core
+def testCoreSpell_SessionWords(fncDir):
+ """Test the handling of the custom word list in the spell checker.
+ New project sessions should not inherit the project word list from
+ other sessions, so this test checks that they don't bleed through.
+ """
+ wList1 = os.path.join(fncDir, "wordlist1.txt")
+ wList2 = os.path.join(fncDir, "wordlist2.txt")
+ writeFile(wList1, "a_word\nb_word\nc_word\n")
+ writeFile(wList2, "d_word\ne_word\nf_word\n")
+
+ spChk = NWSpellEnchant()
+
+ spChk.setLanguage("en", wList1)
+ assert spChk.checkWord("a_word") is True
+ assert spChk.checkWord("b_word") is True
+ assert spChk.checkWord("c_word") is True
+ assert spChk.checkWord("d_word") is False
+ assert spChk.checkWord("e_word") is False
+ assert spChk.checkWord("f_word") is False
+
+ spChk.setLanguage("en", wList2)
+ assert spChk.checkWord("a_word") is False
+ assert spChk.checkWord("b_word") is False
+ assert spChk.checkWord("c_word") is False
+ assert spChk.checkWord("d_word") is True
+ assert spChk.checkWord("e_word") is True
+ assert spChk.checkWord("f_word") is True
+
+# END Test testCoreSpell_SessionWords
From c23a1d224dd82b8ca4d4d2c68b936b58a315cd22 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 17 Aug 2022 22:19:32 +0200
Subject: [PATCH 021/246] Install a generic English dictionary on Linux CI
---
.github/workflows/test_linux.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/test_linux.yml b/.github/workflows/test_linux.yml
index caf147ef..b95dd745 100644
--- a/.github/workflows/test_linux.yml
+++ b/.github/workflows/test_linux.yml
@@ -25,7 +25,7 @@ jobs:
- name: Install Packages (apt)
run: |
sudo apt update
- sudo apt install libenchant-dev qttools5-dev-tools
+ sudo apt install libenchant-dev qttools5-dev-tools aspell-en
- name: Checkout Source
uses: actions/checkout@v2
- name: Install Dependencies (pip)
From 93194626c7367cd7f1980e61f0d9c3c4c32b7250 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 17 Aug 2022 22:25:02 +0200
Subject: [PATCH 022/246] Add an additional ignore line on main editing test
---
tests/test_gui/test_gui_guimain.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py
index 34cfaffa..cbc37dc7 100644
--- a/tests/test_gui/test_gui_guimain.py
+++ b/tests/test_gui/test_gui_guimain.py
@@ -439,7 +439,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, fncProj, refDir, outDir):
testFile = os.path.join(outDir, "guiEditor_Main_Final_nwProject.nwx")
compFile = os.path.join(refDir, "guiEditor_Main_Final_nwProject.nwx")
copyfile(projFile, testFile)
- assert cmpFiles(testFile, compFile, [2, 6, 7, 8])
+ assert cmpFiles(testFile, compFile, [2, 6, 7, 8, 13])
projFile = os.path.join(fncProj, "content", "031b4af5197ec.nwd")
testFile = os.path.join(outDir, "guiEditor_Main_Final_031b4af5197ec.nwd")
From 3ddbfb061bd40e786f0873953af1606d1ee706fd Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 17 Aug 2022 22:29:27 +0200
Subject: [PATCH 023/246] Make spell ckeck tests run on en_US
---
tests/test_core/test_core_spellcheck.py | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/tests/test_core/test_core_spellcheck.py b/tests/test_core/test_core_spellcheck.py
index 01615809..66dd33b2 100644
--- a/tests/test_core/test_core_spellcheck.py
+++ b/tests/test_core/test_core_spellcheck.py
@@ -37,7 +37,7 @@ def testCoreSpell_FakeEnchant(monkeypatch):
with monkeypatch.context() as mp:
mp.setitem(sys.modules, "enchant", None)
spChk = NWSpellEnchant()
- spChk.setLanguage("en", "")
+ spChk.setLanguage("en_US", "")
assert isinstance(spChk._theDict, FakeEnchant)
# Request a non-existent dictionary
@@ -85,9 +85,9 @@ def testCoreSpell_Enchant(monkeypatch, fncDir):
# Load the proper enchant package (twice)
spChk = NWSpellEnchant()
- spChk.setLanguage("en", wList)
- spChk.setLanguage("en", wList)
- assert spChk.spellLanguage == "en"
+ spChk.setLanguage("en_US", wList)
+ spChk.setLanguage("en_US", wList)
+ assert spChk.spellLanguage == "en_US"
# Add a word to the user's dictionary
assert spChk._readProjectDictionary("stuff") is False
@@ -127,7 +127,7 @@ def testCoreSpell_Enchant(monkeypatch, fncDir):
assert len(dList) > 0
aTag, aName = spChk.describeDict()
- assert aTag == "en"
+ assert aTag == "en_US"
assert aName != ""
# END Test testCoreSpell_Enchant
@@ -146,7 +146,7 @@ def testCoreSpell_SessionWords(fncDir):
spChk = NWSpellEnchant()
- spChk.setLanguage("en", wList1)
+ spChk.setLanguage("en_US", wList1)
assert spChk.checkWord("a_word") is True
assert spChk.checkWord("b_word") is True
assert spChk.checkWord("c_word") is True
@@ -154,7 +154,7 @@ def testCoreSpell_SessionWords(fncDir):
assert spChk.checkWord("e_word") is False
assert spChk.checkWord("f_word") is False
- spChk.setLanguage("en", wList2)
+ spChk.setLanguage("en_US", wList2)
assert spChk.checkWord("a_word") is False
assert spChk.checkWord("b_word") is False
assert spChk.checkWord("c_word") is False
From fcfe7507d69af4e8b4f758c6e274025f4c16221d Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 17 Aug 2022 23:01:03 +0200
Subject: [PATCH 024/246] Block adding spaces before colon (French language
feature) in certain meta data cases
---
novelwriter/gui/doceditor.py | 24 +++++++++++++++++++----
tests/test_gui/test_gui_doceditor.py | 29 +++++++++++++++++++++++++++-
2 files changed, 48 insertions(+), 5 deletions(-)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index 59275295..f93fcab4 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -1982,12 +1982,14 @@ class GuiDocEditor(QTextEdit):
tCheck = tInsert
if tCheck in self.mainConf.fmtPadBefore:
- nDelete = max(nDelete, 1)
- tInsert = self._typPadChar + tInsert
+ if self.allowSpaceBeforeColon(theText, tCheck):
+ nDelete = max(nDelete, 1)
+ tInsert = self._typPadChar + tInsert
if tCheck in self.mainConf.fmtPadAfter:
- nDelete = max(nDelete, 1)
- tInsert = tInsert + self._typPadChar
+ if self.allowSpaceBeforeColon(theText, tCheck):
+ nDelete = max(nDelete, 1)
+ tInsert = tInsert + self._typPadChar
if nDelete > 0:
theCursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, nDelete)
@@ -1995,6 +1997,20 @@ class GuiDocEditor(QTextEdit):
return
+ @staticmethod
+ def _allowSpaceBeforeColon(text, char):
+ """Special checker function only used by the insert space
+ feature for French, Spanish, etc, so it doesn't insert a
+ sapce before colons in meta data lines.
+ """
+ if char == ":" and len(text) > 1:
+ if text[0] == "@":
+ return False
+ if text[0] == "%":
+ if text[1:].lstrip()[:9].lower() == "synopsis:":
+ return False
+ return True
+
def _updateHeaders(self, checkPos=False, checkLevel=False):
"""Update the headers record and return True if anything
changed, if a check flag was provided.
diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py
index 8057ca41..81fb2249 100644
--- a/tests/test_gui/test_gui_doceditor.py
+++ b/tests/test_gui/test_gui_doceditor.py
@@ -1189,7 +1189,7 @@ def testGuiEditor_Tags(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
@pytest.mark.gui
-def testGuiEditor_WordCounters(qtbot, monkeypatch, caplog, nwGUI, nwMinimal, ipsumText):
+def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
"""Test saving text from the editor.
"""
# Block message box
@@ -1488,3 +1488,30 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, nwLipsum):
# qtbot.stopForInteraction()
# END Test testGuiEditor_Search
+
+
+@pytest.mark.gui
+def testGuiEditor_StaticMethods():
+ """Test the document editor's static methods.
+ """
+ # Check the method that decides if it is allowed to insert a space
+ # before a colon using the French, Spanish, etc language feature
+ assert GuiDocEditor._allowSpaceBeforeColon("", "") is True
+ assert GuiDocEditor._allowSpaceBeforeColon("", ":") is True
+ assert GuiDocEditor._allowSpaceBeforeColon("some text", ":") is True
+
+ assert GuiDocEditor._allowSpaceBeforeColon("@:", ":") is False
+ assert GuiDocEditor._allowSpaceBeforeColon("@>", ">") is True
+
+ assert GuiDocEditor._allowSpaceBeforeColon("%", ":") is True
+ assert GuiDocEditor._allowSpaceBeforeColon("%:", ":") is True
+ assert GuiDocEditor._allowSpaceBeforeColon("%synopsis:", ":") is False
+ assert GuiDocEditor._allowSpaceBeforeColon("%Synopsis:", ":") is False
+ assert GuiDocEditor._allowSpaceBeforeColon("% synopsis:", ":") is False
+ assert GuiDocEditor._allowSpaceBeforeColon("% Synopsis:", ":") is False
+ assert GuiDocEditor._allowSpaceBeforeColon("% synopsis:", ":") is False
+ assert GuiDocEditor._allowSpaceBeforeColon("% Synopsis:", ":") is False
+ assert GuiDocEditor._allowSpaceBeforeColon("%synopsis :", ":") is True
+ assert GuiDocEditor._allowSpaceBeforeColon("%Synopsis :", ":") is True
+
+# END Test testGuiEditor_MinorMethods
From 7e82461abaaf68d8fc89349c248f75a711400eeb Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 17 Aug 2022 23:05:26 +0200
Subject: [PATCH 025/246] Fix comment in test
---
tests/test_gui/test_gui_doceditor.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py
index 81fb2249..f0d40808 100644
--- a/tests/test_gui/test_gui_doceditor.py
+++ b/tests/test_gui/test_gui_doceditor.py
@@ -1514,4 +1514,4 @@ def testGuiEditor_StaticMethods():
assert GuiDocEditor._allowSpaceBeforeColon("%synopsis :", ":") is True
assert GuiDocEditor._allowSpaceBeforeColon("%Synopsis :", ":") is True
-# END Test testGuiEditor_MinorMethods
+# END Test testGuiEditor_StaticMethods
From 2407b61830ece297b8667fd275ea0040a3f8c8d5 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 17 Aug 2022 23:24:18 +0200
Subject: [PATCH 026/246] Fix typo in docstring and add reference to issue
---
novelwriter/gui/doceditor.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index c26995ca..b127090f 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -2010,7 +2010,7 @@ class GuiDocEditor(QTextEdit):
def _allowSpaceBeforeColon(text, char):
"""Special checker function only used by the insert space
feature for French, Spanish, etc, so it doesn't insert a
- sapce before colons in meta data lines.
+ space before colons in meta data lines. See issue #1090.
"""
if char == ":" and len(text) > 1:
if text[0] == "@":
From 53c1d1b7ee74a3dde96110a3872c155059267ac3 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 17 Aug 2022 23:49:37 +0200
Subject: [PATCH 027/246] Bumped version and update changelog
---
CHANGELOG.md | 30 +++++++++++++++++++++++
novelwriter/__init__.py | 6 ++---
novelwriter/assets/text/release_notes.htm | 11 +++++++++
3 files changed, 44 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 00509e4e..036c0bc7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,35 @@
# novelWriter Changelog
+## Version 1.6.2 [2022-08-18]
+
+### Release Notes
+
+This is a bugfix release that fixes a rare problem causing novelWriter to crash if the spell
+checker language setting was configured to an empty value.
+
+A few other minor issues have also been fixed: The project language setting is now properly
+exported to ODT documents. Spaces are no longer inserted automatically in front of colons in
+certain meta data settings when the feature is enabled (it is primarily used for French). Lastly,
+the slider splitting the editor and viewer panels can no longer be dragged until the viewer
+disappears. It was not necessarily obvious how the viewer panel could be restored in such cases.
+
+### Detailed Changelog
+
+**Bugfixes**
+
+* Fixed an issue where the project language setting was not exported when building Open Document
+ files. Issue #1073. PR #1087.
+* Fixed an issue where the splitter in the main window could be dragged until it hid the document
+ viewer panel. This is no longer possible. Issue #1085. PR #1087.
+* Fixed an issue where an empty spell check language setting would crash novelWriter. Issue #1096.
+ PR #1098.
+* Added a checker that blocks the automatic insertion of spaces in front of special characters in
+ the cases where the character is a colon in either a meta tag, or as part of the synopsis
+ keyword. This feature is used for certain languages like French and Spanish. Issue #1090.
+ PR #1099.
+
+----
+
## Version 1.6.2 [2022-03-20]
### Release Notes
diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py
index 0afbf118..4428de01 100644
--- a/novelwriter/__init__.py
+++ b/novelwriter/__init__.py
@@ -60,9 +60,9 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
-__version__ = "1.6.2"
-__hexversion__ = "0x010602f0"
-__date__ = "2022-03-20"
+__version__ = "1.6.3"
+__hexversion__ = "0x010603f0"
+__date__ = "2022-08-18"
__status__ = "Stable"
__domain__ = "novelwriter.io"
__url__ = "https://novelwriter.io"
diff --git a/novelwriter/assets/text/release_notes.htm b/novelwriter/assets/text/release_notes.htm
index 0850e30c..a66c4368 100644
--- a/novelwriter/assets/text/release_notes.htm
+++ b/novelwriter/assets/text/release_notes.htm
@@ -53,5 +53,16 @@ empty documents would trigger a rebuild of the index each time the project was o
been fixed. Another fix resolves an error message being written to the console logging output when
a new document was created. Both errors were harmless.
+
Patch 1.6.3 – 18 August 2022
+
+
This is a bugfix release that fixes a rare problem causing novelWriter to crash if the spell
+checker language setting was configured to an empty value.
+
A few other minor issues have also been fixed: The project language setting is now properly
+exported to ODT documents. Spaces are no longer inserted automatically in front of colons in
+certain meta data settings when the feature is enabled (it is primarily used for French). Lastly,
+the slider splitting the editor and viewer panels can no longer be dragged until the viewer
+disappears. It was not necessarily obvious how the viewer panel could be restored in such cases.
+