Merge branch 'main' into features/footnotes

This commit is contained in:
Veronica Berglyd Olsen
2024-04-18 00:01:48 +02:00
16 changed files with 149 additions and 533 deletions
+3 -3
View File
@@ -31,9 +31,9 @@ documentation.
## Implementation ## Implementation
novelWriter is written with Python 3 (3.8+) using Qt5 and PyQt5 (5.10+), and is released on Linux, novelWriter is written with Python 3 (3.8+) using Qt5 and PyQt5 (5.15 only), and is released on
Windows and macOS. It can in principle run on any Operating System that also supports Qt, PyQt and Linux, Windows and macOS. It can in principle run on any Operating System that also supports Qt,
Python. PyQt and Python.
## Project Contributions ## Project Contributions
+4 -6
View File
@@ -39,11 +39,9 @@ The following Python packages are needed to run all features of novelWriter:
* ``PyQt5`` needed for connecting with the Qt5 libraries. * ``PyQt5`` needed for connecting with the Qt5 libraries.
* ``PyEnchant`` needed for spell checking (optional). * ``PyEnchant`` needed for spell checking (optional).
PyQt/Qt should be at least 5.10, but ideally 5.13 or higher for all features to work. For instance, PyQt/Qt must be at least 5.15.0. If you want spell checking, you must install the ``PyEnchant``
searching using regular expressions with full Unicode support requires 5.13. package. The spell check library must be at least 3.0 to work with Windows. On Linux, 2.0 also
works fine.
If you want spell checking, you must install the ``PyEnchant`` package. The spell check library
must be at least 3.0 to work with Windows. On Linux, 2.0 also works fine.
If you install from PyPi, these dependencies should be installed automatically. If you install from If you install from PyPi, these dependencies should be installed automatically. If you install from
source, dependencies can still be installed from PyPi with: source, dependencies can still be installed from PyPi with:
@@ -55,7 +53,7 @@ source, dependencies can still be installed from PyPi with:
.. note:: .. note::
On Linux distros, the Qt library is usually split up into multiple packages. In some cases, On Linux distros, the Qt library is usually split up into multiple packages. In some cases,
secondary dependencies may not be installed automatically. For novelWriter, the library files secondary dependencies may not be installed automatically. For novelWriter, the library files
for renderring the SVG icons may be left out and needs to be installed manually. This is the for rendering the SVG icons may be left out and needs to be installed manually. This is the
case on for instance Arch Linux. case on for instance Arch Linux.
+6 -6
View File
@@ -47,9 +47,9 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen" __author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen" __maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net" __email__ = "code@vkbo.net"
__version__ = "2.5a1" __version__ = "2.5a2"
__hexversion__ = "0x020500a1" __hexversion__ = "0x020500a1"
__date__ = "2024-04-13" __date__ = "2024-04-16"
__status__ = "Stable" __status__ = "Stable"
__domain__ = "novelwriter.io" __domain__ = "novelwriter.io"
@@ -167,14 +167,14 @@ def main(sysArgs: list | None = None) -> GuiMain | None:
"At least Python 3.8 is required, found %s" % CONFIG.verPyString "At least Python 3.8 is required, found %s" % CONFIG.verPyString
) )
errorCode |= 0x04 errorCode |= 0x04
if CONFIG.verQtValue < 0x050a00: if CONFIG.verQtValue < 0x050f00:
errorData.append( errorData.append(
"At least Qt5 version 5.10 is required, found %s" % CONFIG.verQtString "At least Qt5 version 5.15.0 is required, found %s" % CONFIG.verQtString
) )
errorCode |= 0x08 errorCode |= 0x08
if CONFIG.verPyQtValue < 0x050a00: if CONFIG.verPyQtValue < 0x050f00:
errorData.append( errorData.append(
"At least PyQt5 version 5.10 is required, found %s" % CONFIG.verPyQtString "At least PyQt5 version 5.15.0 is required, found %s" % CONFIG.verPyQtString
) )
errorCode |= 0x10 errorCode |= 0x10
+1 -11
View File
@@ -370,17 +370,7 @@ class DocSearch:
def _buildPattern(self, search: str) -> str: def _buildPattern(self, search: str) -> str:
"""Build the search pattern string.""" """Build the search pattern string."""
if self._escape: if self._escape:
if CONFIG.verQtValue >= 0x050f00: search = QRegularExpression.escape(search)
search = QRegularExpression.escape(search)
else:
# For older Qt versions, we escape manually
escaped = ""
for c in search:
if c.isalnum() or c == "_":
escaped += c
else:
escaped += f"\\{c}"
search = escaped
if self._words: if self._words:
search = f"(?:^|\\b){search}(?:$|\\b)" search = f"(?:^|\\b){search}(?:$|\\b)"
return search return search
+9 -23
View File
@@ -39,8 +39,8 @@ from time import time
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from PyQt5.QtCore import ( from PyQt5.QtCore import (
pyqtSignal, pyqtSlot, QObject, QPoint, QRegExp, QRegularExpression, pyqtSignal, pyqtSlot, QObject, QPoint, QRegularExpression, QRunnable, Qt,
QRunnable, Qt, QTimer QTimer
) )
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QColor, QCursor, QFont, QKeyEvent, QKeySequence, QMouseEvent, QPalette, QColor, QCursor, QFont, QKeyEvent, QKeySequence, QMouseEvent, QPalette,
@@ -2568,32 +2568,18 @@ class GuiDocEditSearch(QFrame):
# Getters # Getters
## ##
def getSearchObject(self) -> str | QRegularExpression | QRegExp: def getSearchObject(self) -> str | QRegularExpression:
"""Return the current search text either as text or as a regular """Return the current search text either as text or as a regular
expression object. expression object.
""" """
text = self.searchBox.text() text = self.searchBox.text()
if CONFIG.searchRegEx: if CONFIG.searchRegEx:
# Using the Unicode-capable QRegularExpression class was rxOpt = QRegularExpression.PatternOption.UseUnicodePropertiesOption
# only added in Qt 5.13. Otherwise, 5.3 and up supports if not CONFIG.searchCase:
# only the QRegExp class. rxOpt |= QRegularExpression.PatternOption.CaseInsensitiveOption
if CONFIG.verQtValue >= 0x050d00: regEx = QRegularExpression(text, rxOpt)
rxOpt = QRegularExpression.PatternOption.UseUnicodePropertiesOption self._alertSearchValid(regEx.isValid())
if not CONFIG.searchCase: return regEx
rxOpt |= QRegularExpression.PatternOption.CaseInsensitiveOption
regEx = QRegularExpression(text, rxOpt)
self._alertSearchValid(regEx.isValid())
return regEx
else: # pragma: no cover
# >= 50300 to < 51300
if CONFIG.searchCase:
rxOpt = Qt.CaseSensitivity.CaseSensitive
else:
rxOpt = Qt.CaseSensitivity.CaseInsensitive
regEx = QRegExp(text, rxOpt)
self._alertSearchValid(regEx.isValid())
return regEx
return text return text
## ##
+2 -3
View File
@@ -209,9 +209,8 @@ class GuiDocViewerPanel(QWidget):
def _updateTabVisibility(self) -> None: def _updateTabVisibility(self) -> None:
"""Hide class tabs with no content.""" """Hide class tabs with no content."""
if CONFIG.verQtValue >= 0x050f00: for tClass, cTab in self.kwTabs.items():
for tClass, cTab in self.kwTabs.items(): self.mainTabs.setTabVisible(self.idTabs[tClass], cTab.countEntries() > 0)
self.mainTabs.setTabVisible(self.idTabs[tClass], cTab.countEntries() > 0)
return return
def _loadAllTags(self) -> None: def _loadAllTags(self) -> None:
+61 -348
View File
@@ -33,6 +33,8 @@ import datetime
import subprocess import subprocess
import email.utils import email.utils
from pathlib import Path
OS_NONE = 0 OS_NONE = 0
OS_LINUX = 1 OS_LINUX = 1
OS_WIN = 2 OS_WIN = 2
@@ -54,16 +56,15 @@ def extractVersion(beQuiet: bool = False) -> tuple[str, str, str]:
numVers = "0" numVers = "0"
hexVers = "0x0" hexVers = "0x0"
relDate = "Unknown" relDate = "Unknown"
initFile = os.path.join("novelwriter", "__init__.py") initFile = Path("novelwriter") / "__init__.py"
try: try:
with open(initFile, mode="r", encoding="utf-8") as inFile: for aLine in initFile.read_text(encoding="utf-8").splitlines():
for aLine in inFile: if aLine.startswith("__version__"):
if aLine.startswith("__version__"): numVers = getValue((aLine))
numVers = getValue((aLine)) if aLine.startswith("__hexversion__"):
if aLine.startswith("__hexversion__"): hexVers = getValue((aLine))
hexVers = getValue((aLine)) if aLine.startswith("__date__"):
if aLine.startswith("__date__"): relDate = getValue((aLine))
relDate = getValue((aLine))
except Exception as exc: except Exception as exc:
print("Could not read file: %s" % initFile) print("Could not read file: %s" % initFile)
print(str(exc)) print(str(exc))
@@ -88,26 +89,23 @@ def stripVersion(version: str) -> str:
def readFile(fileName: str) -> str: def readFile(fileName: str) -> str:
"""Read an entire file and return as a string.""" """Read an entire file and return as a string."""
with open(fileName, mode="r", encoding="utf-8") as inFile: return Path(fileName).read_text(encoding="utf-8")
return inFile.read()
def writeFile(fileName: str, writeText: str) -> None: def writeFile(fileName: str, text: str) -> None:
"""Write string to file.""" """Write string to file."""
with open(fileName, mode="w+", encoding="utf-8") as outFile: Path(fileName).write_text(text, encoding="utf-8")
outFile.write(writeText) return
def toUpload(srcPath: str, dstName: str | None = None) -> None: def toUpload(srcPath: str | Path, dstName: str | None = None) -> None:
"""Copy a file produced by one of the build functions to the upload """Copy a file produced by one of the build functions to the upload
directory. The file can optionally be given a new name. directory. The file can optionally be given a new name.
""" """
uplDir = "dist_upload" uplDir = Path("dist_upload")
if not os.path.isdir(uplDir): uplDir.mkdir(exist_ok=True)
os.mkdir(uplDir) srcPath = Path(srcPath)
if dstName is None: shutil.copyfile(srcPath, uplDir / (dstName or srcPath.name))
dstName = os.path.basename(srcPath)
shutil.copyfile(srcPath, os.path.join(uplDir, dstName))
return return
@@ -561,43 +559,24 @@ def importI18nUpdates(sysArgs: list[str]) -> None:
# Make Minimal Package (minimal-zip) # Make Minimal Package (minimal-zip)
## ##
def makeMinimalPackage(targetOS: int) -> None: def makeWindowsZip() -> None:
"""Pack the core source file in a single zip file.""" """Pack the core source file in a single zip file."""
from zipfile import ZipFile, ZIP_DEFLATED from zipfile import ZipFile, ZIP_DEFLATED
print("") print("")
print("Building Minimal ZIP File") print("Building Windows ZIP File")
print("=========================") print("=========================")
bldDir = "dist_minimal" bldDir = "dist_minimal"
if not os.path.isdir(bldDir): if not os.path.isdir(bldDir):
os.mkdir(bldDir) os.mkdir(bldDir)
if targetOS == OS_LINUX:
targName = "-linux"
print("Target OS: Linux")
elif targetOS == OS_DARWIN:
targName = "-darwin"
print("Target OS: Darwin")
elif targetOS == OS_WIN:
targName = "-win"
print("Target OS: Windows")
else:
targName = ""
print("")
# Check Additional Assets
# =======================
if not checkAssetsExist(): if not checkAssetsExist():
print("ERROR: Missing build assets") print("ERROR: Missing build assets")
sys.exit(1) sys.exit(1)
# Build Minimal Zip
# =================
pkgVers, _, _ = extractVersion() pkgVers, _, _ = extractVersion()
zipFile = f"novelwriter-{pkgVers}-minimal{targName}.zip" zipFile = f"novelwriter-{pkgVers}-minimal-win.zip"
outFile = os.path.join(bldDir, zipFile) outFile = os.path.join(bldDir, zipFile)
if os.path.isfile(outFile): if os.path.isfile(outFile):
os.unlink(outFile) os.unlink(outFile)
@@ -627,26 +606,8 @@ def makeMinimalPackage(targetOS: int) -> None:
continue continue
zipObj.write(os.path.join(nRoot, aFile)) zipObj.write(os.path.join(nRoot, aFile))
if targetOS == OS_WIN: zipObj.write("novelWriter.py", "novelWriter.pyw")
zipObj.write("novelWriter.py", "novelWriter.pyw") print("Added: novelWriter.pyw")
print("Added: novelWriter.pyw")
zipObj.write(os.path.join("setup", "windows_install.bat"), "windows_install.bat")
print("Added: windows_install.bat")
zipObj.write(os.path.join("setup", "windows_uninstall.bat"), "windows_uninstall.bat")
print("Added: windows_uninstall.bat")
else: # Linux and Mac
# Add icons
for nRoot, _, nFiles in os.walk(os.path.join("setup", "data")):
print("Added: %s/* [Files: %d]" % (nRoot, len(nFiles)))
for aFile in nFiles:
zipObj.write(os.path.join(nRoot, aFile))
zipObj.write(os.path.join("setup", "description_pypi.md"))
print("Added: setup/description_pypi.md")
zipObj.write("novelWriter.py")
print("Added: novelWriter.py")
for aFile in rootFiles: for aFile in rootFiles:
print("Added: %s" % aFile) print("Added: %s" % aFile)
@@ -658,14 +619,9 @@ def makeMinimalPackage(targetOS: int) -> None:
print("") print("")
print("Created File: %s" % outFile) print("Created File: %s" % outFile)
# Create Checksum File
# ====================
shaFile = makeCheckSum(zipFile, cwd=bldDir) shaFile = makeCheckSum(zipFile, cwd=bldDir)
toUpload(outFile) toUpload(outFile)
toUpload(shaFile) toUpload(shaFile)
print("") print("")
return return
@@ -675,8 +631,10 @@ def makeMinimalPackage(targetOS: int) -> None:
# Make Debian Package (build-deb) # Make Debian Package (build-deb)
## ##
def makeDebianPackage(signKey: str | None = None, sourceBuild: bool = False, def makeDebianPackage(
distName: str = "unstable", buildName: str = "") -> str: signKey: str | None = None, sourceBuild: bool = False,
distName: str = "unstable", buildName: str = "", oldSetuptools: bool = False
) -> str:
"""Build a Debian package.""" """Build a Debian package."""
print("") print("")
print("Build Debian Package") print("Build Debian Package")
@@ -778,11 +736,27 @@ def makeDebianPackage(signKey: str | None = None, sourceBuild: bool = False,
)) ))
print("Wrote: setup.py") print("Wrote: setup.py")
setupCfg = readFile("pyproject.toml").replace( if oldSetuptools:
"setup/description_pypi.md", "data/description_short.txt" # This is needed for Ubuntu up to 22.04
) setupCfg = readFile("setup/launchpad_setup.cfg").replace(
writeFile(f"{outDir}/pyproject.toml", setupCfg) "file: setup/description_pypi.md", "file: data/description_short.txt"
print("Wrote: pyproject.toml") )
writeFile(f"{outDir}/setup.cfg", setupCfg)
print("Wrote: setup.cfg")
writeFile(f"{outDir}/pyproject.toml", (
"[build-system]\n"
"requires = [\"setuptools\"]\n"
"build-backend = \"setuptools.build_meta\"\n"
))
print("Wrote: pyproject.toml")
else:
pyProject = readFile("pyproject.toml").replace(
"setup/description_pypi.md", "data/description_short.txt"
)
writeFile(f"{outDir}/pyproject.toml", pyProject)
print("Wrote: pyproject.toml")
# Copy/Write Debian Files # Copy/Write Debian Files
# ======================= # =======================
@@ -860,15 +834,14 @@ def makeForLaunchpad(doSign: bool = False, isFirst: bool = False) -> None:
bldNum = "0" bldNum = "0"
distLoop = [ distLoop = [
("20.04", "focal"), ("22.04", "jammy", True),
("22.04", "jammy"), ("23.10", "mantic", False),
("23.10", "mantic"), ("24.04", "noble", False),
("24.04", "noble"),
] ]
print("Building Ubuntu packages for:") print("Building Ubuntu packages for:")
print("") print("")
for distNum, codeName in distLoop: for distNum, codeName, _ in distLoop:
print(f" * Ubuntu {distNum} {codeName.title()}") print(f" * Ubuntu {distNum} {codeName.title()}")
print("") print("")
@@ -881,13 +854,14 @@ def makeForLaunchpad(doSign: bool = False, isFirst: bool = False) -> None:
print("") print("")
dputCmd = [] dputCmd = []
for distNum, codeName in distLoop: for distNum, codeName, oldSetup in distLoop:
buildName = f"ubuntu{distNum}.{bldNum}" buildName = f"ubuntu{distNum}.{bldNum}"
dCmd = makeDebianPackage( dCmd = makeDebianPackage(
signKey=signKey, signKey=signKey,
sourceBuild=True, sourceBuild=True,
distName=codeName, distName=codeName,
buildName=buildName buildName=buildName,
oldSetuptools=oldSetup,
) )
dputCmd.append(dCmd) dputCmd.append(dCmd)
@@ -1555,226 +1529,6 @@ def xdgUninstall() -> None:
return return
##
# WIN Installation (win-install)
##
def winInstall() -> None:
"""Will attempt to install icons and make a launcher for Windows."""
if sys.platform != "win32":
raise Exception("This method only runs on Windows")
import winreg
try:
import win32com.client
except ImportError:
print(
"ERROR: Package 'pywin32' is missing on this system.\n"
" Please run 'pkgutils.py pip' to automatically install\n"
" dependecies, or run 'pip install --user pywin32'."
)
sys.exit(1)
print("")
print("Windows Install")
print("===============")
print("")
numVers, hexVers, _ = extractVersion()
nwTesting = not hexVers[-2] == "f"
wShell = win32com.client.Dispatch("WScript.Shell")
if nwTesting:
linkName = "novelWriter Testing %s.lnk" % numVers
else:
linkName = "novelWriter %s.lnk" % numVers
desktopDir = wShell.SpecialFolders("Desktop")
desktopIcon = os.path.join(desktopDir, linkName)
startMenuDir = wShell.SpecialFolders("StartMenu")
startMenuProg = os.path.join(startMenuDir, "Programs", "novelWriter")
startMenuIcon = os.path.join(startMenuProg, linkName)
pythonDir = os.path.dirname(sys.executable)
pythonExe = os.path.join(pythonDir, "pythonw.exe")
targetDir = os.path.abspath(os.path.dirname(__file__))
targetPy = os.path.join(targetDir, "novelWriter.pyw")
targetIcon = os.path.join(targetDir, "novelwriter", "assets", "icons", "novelwriter.ico")
if not os.path.isfile(targetPy):
shutil.copy2(os.path.join(targetDir, "novelWriter.py"), targetPy)
print("")
print("Collecting Info ...")
print("Desktop Folder: %s" % desktopDir)
print("Start Menu Folder: %s" % startMenuDir)
print("Python Executable: %s" % pythonExe)
print("Target Executable: %s" % targetPy)
print("Target Icon: %s" % targetIcon)
print("")
print("Creating Links ...")
if os.path.isfile(desktopIcon):
os.unlink(desktopIcon)
print("Deleted: %s" % desktopIcon)
if os.path.isdir(startMenuProg):
for oldIcon in os.listdir(startMenuProg):
oldPath = os.path.join(startMenuProg, oldIcon)
if not oldIcon.startswith("novelWriter"):
continue
isTesting = oldIcon.startswith("novelWriter Testing")
if isTesting and nwTesting:
os.unlink(oldPath)
print("Deleted: %s" % oldPath)
if not isTesting and not nwTesting:
os.unlink(oldPath)
print("Deleted: %s" % oldPath)
else:
os.mkdir(startMenuProg)
print("Created: %s" % startMenuProg)
wShortcut = wShell.CreateShortCut(desktopIcon)
wShortcut.TargetPath = targetPy
wShortcut.WorkingDirectory = targetDir
wShortcut.IconLocation = targetIcon
wShortcut.WindowStyle = 1
wShortcut.save()
print("Created: %s" % desktopIcon)
wShortcut = wShell.CreateShortCut(startMenuIcon)
wShortcut.TargetPath = targetPy
wShortcut.WorkingDirectory = targetDir
wShortcut.IconLocation = targetIcon
wShortcut.WindowStyle = 1
wShortcut.save()
print("Created: %s" % startMenuIcon)
print("")
print("Creating registry keys ...")
def setKey(kPath: str, kName: str, kVal: str) -> None:
winreg.CreateKey(winreg.HKEY_CURRENT_USER, kPath)
regKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, kPath, 0, winreg.KEY_WRITE)
winreg.SetValueEx(regKey, kName, 0, winreg.REG_SZ, kVal)
winreg.CloseKey(regKey)
mimeIcon = os.path.join(
targetDir, "novelwriter", "assets", "icons", "x-novelwriter-project.ico"
)
mimeExec = '"%s" "%s" "%%1"' % (pythonExe, targetPy)
try:
setKey(r"Software\Classes\.nwx\OpenWithProgids", "novelWriterProject.nwx", "")
setKey(r"Software\Classes\novelWriterProject.nwx", "", "novelWriter Project File")
setKey(r"Software\Classes\novelWriterProject.nwx\DefaultIcon", "", mimeIcon)
setKey(r"Software\Classes\novelWriterProject.nwx\shell\open\command", "", mimeExec)
setKey(r"Software\Classes\Applications\novelWriter.pyw\SupportedTypes", ".nwx", "")
except WindowsError:
print("ERROR: Failed to set registry keys.")
print("")
print("")
print("Done!")
print("")
return
##
# WIN Uninstallation (win-uninstall)
##
def winUninstall() -> None:
"""Will attempt to uninstall icons previously installed."""
if sys.platform != "win32":
raise Exception("This method only runs on Windows")
import winreg
try:
import win32com.client
except ImportError:
print(
"ERROR: Package 'pywin32' is missing on this system.\n"
" Please run 'pip install --user pywin32' to install it."
)
sys.exit(1)
print("")
print("Windows Uninstall")
print("=================")
print("")
numVers, hexVers, _ = extractVersion()
nwTesting = not hexVers[-2] == "f"
wShell = win32com.client.Dispatch("WScript.Shell")
if nwTesting:
linkName = "novelWriter Testing %s.lnk" % numVers
else:
linkName = "novelWriter %s.lnk" % numVers
desktopDir = wShell.SpecialFolders("Desktop")
desktopIcon = os.path.join(desktopDir, linkName)
startMenuDir = wShell.SpecialFolders("StartMenu")
startMenuProg = os.path.join(startMenuDir, "Programs", "novelWriter")
startMenuIcon = os.path.join(startMenuProg, linkName)
print("")
print("Deleting Links ...")
if os.path.isfile(desktopIcon):
os.unlink(desktopIcon)
print("Deleted: %s" % desktopIcon)
else:
print("Not Found: %s" % desktopIcon)
if os.path.isfile(startMenuIcon):
os.unlink(startMenuIcon)
print("Deleted: %s" % startMenuIcon)
else:
print("Not Found: %s" % startMenuIcon)
if os.path.isdir(startMenuProg):
if not os.listdir(startMenuProg):
os.rmdir(startMenuProg)
print("Deleted: %s" % startMenuProg)
else:
print("Not Empty: %s" % startMenuProg)
print("")
print("Removing registry keys ...")
keys = [
r"Software\Classes\novelWriterProject.nwx\shell\open\command",
r"Software\Classes\novelWriterProject.nwx\shell\open",
r"Software\Classes\novelWriterProject.nwx\shell",
r"Software\Classes\novelWriterProject.nwx\DefaultIcon",
r"Software\Classes\novelWriterProject.nwx",
r"Software\Classes\.nwx\OpenWithProgids",
r"Software\Classes\.nwx",
r"Software\Classes\Applications\novelWriter.pyw\SupportedTypes",
r"Software\Classes\Applications\novelWriter.pyw",
]
for aKey in keys:
try:
winreg.DeleteKey(winreg.HKEY_CURRENT_USER, aKey)
print("Deleted: HKEY_CURRENT_USER\\%s" % aKey)
except WindowsError:
print("Not Found: HKEY_CURRENT_USER\\%s" % aKey)
print("")
print("Done!")
print("")
return
# =============================================================================================== # # =============================================================================================== #
# Process Command Line # Process Command Line
# =============================================================================================== # # =============================================================================================== #
@@ -1795,19 +1549,6 @@ if __name__ == "__main__":
sysArgs = sys.argv.copy() sysArgs = sys.argv.copy()
# Set Target OS
if "--target-linux" in sysArgs:
sysArgs.remove("--target-linux")
targetOS = OS_LINUX
elif "--target-darwin" in sysArgs:
sysArgs.remove("--target-darwin")
targetOS = OS_DARWIN
elif "--target-win" in sysArgs:
sysArgs.remove("--target-win")
targetOS = OS_WIN
else:
targetOS = hostOS
# Sign package # Sign package
if "--sign" in sysArgs: if "--sign" in sysArgs:
sysArgs.remove("--sign") sysArgs.remove("--sign")
@@ -1831,10 +1572,6 @@ if __name__ == "__main__":
"novelWriter as a package on Linux, Mac and Windows. The available options", "novelWriter as a package on Linux, Mac and Windows. The available options",
"are as follows:", "are as follows:",
"", "",
"Some of the commands can be targeted towards a different OS than the host OS.",
"To target the command, add one of '--target-linux', '--target-darwin' or",
"'--target-win'.",
"",
"General:", "General:",
"", "",
" help Print the help message.", " help Print the help message.",
@@ -1855,10 +1592,8 @@ if __name__ == "__main__":
"Python Packaging:", "Python Packaging:",
"", "",
" import-i18n Import updated i18n files from a zip file.", " import-i18n Import updated i18n files from a zip file.",
" minimal-zip Creates a minimal zip file of the core application without", " windows-zip Creates a minimal zip file of the core application without",
" all the other source files. Defaults to tailor the zip file", " all the other source files. Used for Windows builds.",
" for the current OS, but accepts a target OS flag to build",
" for another OS.",
" build-deb Build a .deb package for Debian and Ubuntu. Add --sign to ", " build-deb Build a .deb package for Debian and Ubuntu. Add --sign to ",
" sign package.", " sign package.",
" build-ubuntu Build a .deb packages Launchpad. Add --sign to ", " build-ubuntu Build a .deb packages Launchpad. Add --sign to ",
@@ -1870,17 +1605,11 @@ if __name__ == "__main__":
"", "",
"System Install:", "System Install:",
"", "",
" install Deprecated. Use pip or build.",
" xdg-install Install launcher and icons for freedesktop systems. Run as", " xdg-install Install launcher and icons for freedesktop systems. Run as",
" root or with sudo for system-wide install, or as user for", " root or with sudo for system-wide install, or as user for",
" single user install.", " single user install.",
" xdg-uninstall Remove the launcher and icons for the current system as", " xdg-uninstall Remove the launcher and icons for the current system as",
" installed by the 'xdg-install' command.", " installed by the 'xdg-install' command.",
" win-install Install desktop icon, start menu icon, and registry entries",
" for file association with .nwx files for Windows systems.",
" win-uninstall Remove desktop icon, start menu icon, and registry keys,",
" for the current system. Note that it only removes icons for",
" the version number of the package the command is run from.",
"", "",
] ]
@@ -1941,9 +1670,9 @@ if __name__ == "__main__":
importI18nUpdates(sysArgs) importI18nUpdates(sysArgs)
sys.exit(0) # Don't continue execution sys.exit(0) # Don't continue execution
if "minimal-zip" in sysArgs: if "windows-zip" in sysArgs:
sysArgs.remove("minimal-zip") sysArgs.remove("windows-zip")
makeMinimalPackage(targetOS) makeWindowsZip()
if "build-deb" in sysArgs: if "build-deb" in sysArgs:
sysArgs.remove("build-deb") sysArgs.remove("build-deb")
@@ -1997,20 +1726,4 @@ if __name__ == "__main__":
else: else:
xdgUninstall() xdgUninstall()
if "win-install" in sysArgs:
sysArgs.remove("win-install")
if hostOS == OS_WIN:
winInstall()
else:
print("ERROR: Command 'win-install' can only be used on Windows")
sys.exit(1)
if "win-uninstall" in sysArgs:
sysArgs.remove("win-uninstall")
if hostOS == OS_WIN:
winUninstall()
else:
print("ERROR: Command 'win-uninstall' can only be used on Windows")
sys.exit(1)
# END Main # END Main
+1 -1
View File
@@ -27,7 +27,7 @@ classifiers = [
] ]
requires-python = ">=3.8" requires-python = ">=3.8"
dependencies = [ dependencies = [
"pyqt5>=5.10", "pyqt5>=5.15",
"pyenchant>=3.0.0", "pyenchant>=3.0.0",
] ]
dynamic = ["version"] dynamic = ["version"]
+1 -1
View File
@@ -1,2 +1,2 @@
pyqt5>=5.10 pyqt5>=5.15
pyenchant>=3.0.0 pyenchant>=3.0.0
+2 -2
View File
@@ -2,14 +2,14 @@ Source: novelwriter
Maintainer: Veronica Berglyd Olsen <code@vkbo.net> Maintainer: Veronica Berglyd Olsen <code@vkbo.net>
Section: text Section: text
Priority: optional Priority: optional
Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9), python3 (>=3.8), python3-pyqt5 (>= 5.10), python3-enchant (>= 2.0) Build-Depends: dh-python, python3-setuptools, python3-all, debhelper (>= 9), python3 (>=3.8), python3-pyqt5 (>= 5.15), python3-enchant (>= 2.0)
Standards-Version: 4.5.1 Standards-Version: 4.5.1
Homepage: https://novelwriter.io Homepage: https://novelwriter.io
X-Python3-Version: >= 3.8 X-Python3-Version: >= 3.8
Package: novelwriter Package: novelwriter
Architecture: all Architecture: all
Depends: ${misc:Depends}, ${python3:Depends}, python3 (>=3.8), python3-pyqt5 (>= 5.10), python3-enchant (>= 2.0) Depends: ${misc:Depends}, ${python3:Depends}, python3 (>=3.8), python3-pyqt5 (>= 5.15), python3-enchant (>= 2.0)
Description: A markdown-like text editor for planning and writing novels Description: A markdown-like text editor for planning and writing novels
novelWriter is a plain text editor designed for writing novels assembled from novelWriter is a plain text editor designed for writing novels assembled from
many smaller text documents. It uses a minimal formatting syntax inspired by many smaller text documents. It uses a minimal formatting syntax inspired by
+48
View File
@@ -0,0 +1,48 @@
[metadata]
name = novelWriter
version = attr: novelwriter.__version__
author = Veronica Berglyd Olsen
author_email = code@vkbo.net
description = A markdown-like text editor for planning and writing novels
url = https://novelwriter.io
long_description = file: setup/description_pypi.md
long_description_content_type = text/markdown
license_files = LICENSE.md
license = GNU General Public License v3
classifiers =
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Programming Language :: Python :: Implementation :: CPython
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
Development Status :: 5 - Production/Stable
Operating System :: OS Independent
Intended Audience :: End Users/Desktop
Natural Language :: English
Topic :: Text Editors
project_urls =
Bug Tracker = https://github.com/vkbo/novelWriter/issues
Documentation = https://docs.novelwriter.io
Source Code = https://github.com/vkbo/novelWriter
[options]
zip_safe = False
python_requires = >=3.8
include_package_data = True
packages = find_namespace:
install_requires =
pyqt5>=5.15
pyenchant>=3.0.0
[options.packages.find]
include = novelwriter*
[options.entry_points]
gui_scripts =
novelwriter = novelwriter:main
[bdist_wheel]
universal = 0
+2 -4
View File
@@ -22,12 +22,10 @@ python3 pkgutils.py qtlrelease manual sample
deactivate deactivate
echo "" echo ""
echo " Building Minimal Packages" echo " Building Windows Source Zip"
echo "================================================================================" echo "================================================================================"
echo "" echo ""
python3 pkgutils.py minimal-zip --target-win python3 pkgutils.py windows-zip
# python3 pkgutils.py minimal-zip --target-linux
# python3 pkgutils.py minimal-zip --target-darwin
echo "" echo ""
echo " Building Linux Packages" echo " Building Linux Packages"
-49
View File
@@ -1,49 +0,0 @@
@echo off
:: Check for Python Installation
echo.
echo Looking for Python
python --version 2>NUL
if errorlevel 1 goto errorNoPython
echo Python found. OK.
if exist pkgutils.py (
echo pkgutils.py found. OK.
) else (
cd ..
if exist pkgutils.py (
echo pkgutils.py found. OK.
) else (
goto errorNoSetup
)
)
echo.
:: Install the PyQt5 and pyenchant dependencies
pip install --user pywin32 -r requirements.txt
:: Create the desktop and start menu icons
python pkgutils.py win-install
pause
goto:eof
:errorNoPython
echo.
echo ERROR^: Python is not installed on your system, or cannot be found.
echo.
echo Please download and install it from https://www.python.org/downloads/
echo Also make sure the "Add Python to PATH" option is selected during installation.
echo.
pause
goto:eof
:errorNoSetup
echo.
echo ERROR^: Could not find the pkgutils.py script.
echo.
echo Make sure you run pkgutils.py from the novelWriter root folder.
echo.
pause
-59
View File
@@ -1,59 +0,0 @@
@echo off
:: Check for Python Installation
echo.
echo Looking for Python
python --version 2>NUL
if errorlevel 1 goto errorNoPython
echo Python found. OK.
if exist pkgutils.py (
echo pkgutils.py found. OK.
) else (
cd ..
if exist pkgutils.py (
echo pkgutils.py found. OK.
) else (
goto errorNoSetup
)
)
echo.
echo Ready to remove the novelWriter icons, registry keys, and package dependencies.
set /P c=Are you sure you want to continue [y/n]?
if /I "%c%" EQU "y" goto doUninst
goto afterUninst
:doUninst
:: Remove the desktop and start menu icons
python pkgutils.py win-uninstall
:: Remove the PyQt5 and pyenchant dependencies
pip uninstall --yes -r requirements.txt
:afterUninst
echo.
pause
goto:eof
:errorNoPython
echo.
echo ERROR^: Python is not installed on your system, or cannot be found.
echo.
echo Please download and install it from https://www.python.org/downloads/
echo Also make sure the "Add Python to PATH" option is selected during installation.
echo.
pause
goto:eof
:errorNoSetup
echo.
echo ERROR^: Could not find the pkgutils.py script.
echo.
echo Make sure you run pkgutils.py from the novelWriter root folder.
echo.
pause
+1 -8
View File
@@ -427,14 +427,7 @@ def testCoreTools_DocSearch(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText):
# ======== # ========
# Escape Using QRegularExpression # Escape Using QRegularExpression
with monkeypatch.context() as mp: assert search._buildPattern("[A-Za-z0-9_]+") == r"\[A\-Za\-z0\-9_\]\+"
mp.setattr(CONFIG, "verQtValue", 0x050f00)
assert search._buildPattern("[A-Za-z0-9_]+") == r"\[A\-Za\-z0\-9_\]\+"
# Escape Using Custom Implementation
with monkeypatch.context() as mp:
mp.setattr(CONFIG, "verQtValue", 0x050d00)
assert search._buildPattern("[A-Za-z0-9_]+") == r"\[A\-Za\-z0\-9_\]\+"
# Whole Words # Whole Words
search.setWholeWords(True) search.setWholeWords(True)
+8 -9
View File
@@ -26,7 +26,7 @@ from tools import C, buildTestProject
from PyQt5.QtGui import QIcon from PyQt5.QtGui import QIcon
from novelwriter import CONFIG, SHARED from novelwriter import SHARED
from novelwriter.constants import nwLists from novelwriter.constants import nwLists
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.editlabel import GuiEditLabel
@@ -151,14 +151,13 @@ def testGuiViewerPanel_Tags(qtbot, monkeypatch, caplog, nwGUI, projPath, mockRnd
nwGUI.docEditor._processTag(cursor, create=True) nwGUI.docEditor._processTag(cursor, create=True)
# Check Panel Tab Visibility # Check Panel Tab Visibility
if CONFIG.verQtValue >= 0x050f00: assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["CHARACTER"]) is True
assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["CHARACTER"]) is True assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["PLOT"]) is False
assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["PLOT"]) is False assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["WORLD"]) is False
assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["WORLD"]) is False assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["TIMELINE"]) is False
assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["TIMELINE"]) is False assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["OBJECT"]) is False
assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["OBJECT"]) is False assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["ENTITY"]) is False
assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["ENTITY"]) is False assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["CUSTOM"]) is False
assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["CUSTOM"]) is False
# Check Character Tab # Check Character Tab
charTab = viewPanel.kwTabs["CHARACTER"] charTab = viewPanel.kwTabs["CHARACTER"]