Merge branch 'main' into features/footnotes
This commit is contained in:
@@ -31,9 +31,9 @@ documentation.
|
||||
|
||||
## Implementation
|
||||
|
||||
novelWriter is written with Python 3 (3.8+) using Qt5 and PyQt5 (5.10+), and is released on Linux,
|
||||
Windows and macOS. It can in principle run on any Operating System that also supports Qt, PyQt and
|
||||
Python.
|
||||
novelWriter is written with Python 3 (3.8+) using Qt5 and PyQt5 (5.15 only), and is released on
|
||||
Linux, Windows and macOS. It can in principle run on any Operating System that also supports Qt,
|
||||
PyQt and Python.
|
||||
|
||||
## Project Contributions
|
||||
|
||||
|
||||
@@ -39,11 +39,9 @@ The following Python packages are needed to run all features of novelWriter:
|
||||
* ``PyQt5`` – needed for connecting with the Qt5 libraries.
|
||||
* ``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,
|
||||
searching using regular expressions with full Unicode support requires 5.13.
|
||||
|
||||
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.
|
||||
PyQt/Qt must be at least 5.15.0. 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
|
||||
source, dependencies can still be installed from PyPi with:
|
||||
@@ -55,7 +53,7 @@ source, dependencies can still be installed from PyPi with:
|
||||
.. note::
|
||||
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
|
||||
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.
|
||||
|
||||
|
||||
|
||||
@@ -47,9 +47,9 @@ __license__ = "GPLv3"
|
||||
__author__ = "Veronica Berglyd Olsen"
|
||||
__maintainer__ = "Veronica Berglyd Olsen"
|
||||
__email__ = "code@vkbo.net"
|
||||
__version__ = "2.5a1"
|
||||
__version__ = "2.5a2"
|
||||
__hexversion__ = "0x020500a1"
|
||||
__date__ = "2024-04-13"
|
||||
__date__ = "2024-04-16"
|
||||
__status__ = "Stable"
|
||||
__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
|
||||
)
|
||||
errorCode |= 0x04
|
||||
if CONFIG.verQtValue < 0x050a00:
|
||||
if CONFIG.verQtValue < 0x050f00:
|
||||
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
|
||||
if CONFIG.verPyQtValue < 0x050a00:
|
||||
if CONFIG.verPyQtValue < 0x050f00:
|
||||
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
|
||||
|
||||
|
||||
@@ -370,17 +370,7 @@ class DocSearch:
|
||||
def _buildPattern(self, search: str) -> str:
|
||||
"""Build the search pattern string."""
|
||||
if self._escape:
|
||||
if CONFIG.verQtValue >= 0x050f00:
|
||||
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
|
||||
search = QRegularExpression.escape(search)
|
||||
if self._words:
|
||||
search = f"(?:^|\\b){search}(?:$|\\b)"
|
||||
return search
|
||||
|
||||
@@ -39,8 +39,8 @@ from time import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import (
|
||||
pyqtSignal, pyqtSlot, QObject, QPoint, QRegExp, QRegularExpression,
|
||||
QRunnable, Qt, QTimer
|
||||
pyqtSignal, pyqtSlot, QObject, QPoint, QRegularExpression, QRunnable, Qt,
|
||||
QTimer
|
||||
)
|
||||
from PyQt5.QtGui import (
|
||||
QColor, QCursor, QFont, QKeyEvent, QKeySequence, QMouseEvent, QPalette,
|
||||
@@ -2568,32 +2568,18 @@ class GuiDocEditSearch(QFrame):
|
||||
# Getters
|
||||
##
|
||||
|
||||
def getSearchObject(self) -> str | QRegularExpression | QRegExp:
|
||||
def getSearchObject(self) -> str | QRegularExpression:
|
||||
"""Return the current search text either as text or as a regular
|
||||
expression object.
|
||||
"""
|
||||
text = self.searchBox.text()
|
||||
if CONFIG.searchRegEx:
|
||||
# Using the Unicode-capable QRegularExpression class was
|
||||
# only added in Qt 5.13. Otherwise, 5.3 and up supports
|
||||
# only the QRegExp class.
|
||||
if CONFIG.verQtValue >= 0x050d00:
|
||||
rxOpt = QRegularExpression.PatternOption.UseUnicodePropertiesOption
|
||||
if not CONFIG.searchCase:
|
||||
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
|
||||
|
||||
rxOpt = QRegularExpression.PatternOption.UseUnicodePropertiesOption
|
||||
if not CONFIG.searchCase:
|
||||
rxOpt |= QRegularExpression.PatternOption.CaseInsensitiveOption
|
||||
regEx = QRegularExpression(text, rxOpt)
|
||||
self._alertSearchValid(regEx.isValid())
|
||||
return regEx
|
||||
return text
|
||||
|
||||
##
|
||||
|
||||
@@ -209,9 +209,8 @@ class GuiDocViewerPanel(QWidget):
|
||||
|
||||
def _updateTabVisibility(self) -> None:
|
||||
"""Hide class tabs with no content."""
|
||||
if CONFIG.verQtValue >= 0x050f00:
|
||||
for tClass, cTab in self.kwTabs.items():
|
||||
self.mainTabs.setTabVisible(self.idTabs[tClass], cTab.countEntries() > 0)
|
||||
for tClass, cTab in self.kwTabs.items():
|
||||
self.mainTabs.setTabVisible(self.idTabs[tClass], cTab.countEntries() > 0)
|
||||
return
|
||||
|
||||
def _loadAllTags(self) -> None:
|
||||
|
||||
+61
-348
@@ -33,6 +33,8 @@ import datetime
|
||||
import subprocess
|
||||
import email.utils
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
OS_NONE = 0
|
||||
OS_LINUX = 1
|
||||
OS_WIN = 2
|
||||
@@ -54,16 +56,15 @@ def extractVersion(beQuiet: bool = False) -> tuple[str, str, str]:
|
||||
numVers = "0"
|
||||
hexVers = "0x0"
|
||||
relDate = "Unknown"
|
||||
initFile = os.path.join("novelwriter", "__init__.py")
|
||||
initFile = Path("novelwriter") / "__init__.py"
|
||||
try:
|
||||
with open(initFile, mode="r", encoding="utf-8") as inFile:
|
||||
for aLine in inFile:
|
||||
if aLine.startswith("__version__"):
|
||||
numVers = getValue((aLine))
|
||||
if aLine.startswith("__hexversion__"):
|
||||
hexVers = getValue((aLine))
|
||||
if aLine.startswith("__date__"):
|
||||
relDate = getValue((aLine))
|
||||
for aLine in initFile.read_text(encoding="utf-8").splitlines():
|
||||
if aLine.startswith("__version__"):
|
||||
numVers = getValue((aLine))
|
||||
if aLine.startswith("__hexversion__"):
|
||||
hexVers = getValue((aLine))
|
||||
if aLine.startswith("__date__"):
|
||||
relDate = getValue((aLine))
|
||||
except Exception as exc:
|
||||
print("Could not read file: %s" % initFile)
|
||||
print(str(exc))
|
||||
@@ -88,26 +89,23 @@ def stripVersion(version: str) -> str:
|
||||
|
||||
def readFile(fileName: str) -> str:
|
||||
"""Read an entire file and return as a string."""
|
||||
with open(fileName, mode="r", encoding="utf-8") as inFile:
|
||||
return inFile.read()
|
||||
return Path(fileName).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def writeFile(fileName: str, writeText: str) -> None:
|
||||
def writeFile(fileName: str, text: str) -> None:
|
||||
"""Write string to file."""
|
||||
with open(fileName, mode="w+", encoding="utf-8") as outFile:
|
||||
outFile.write(writeText)
|
||||
Path(fileName).write_text(text, encoding="utf-8")
|
||||
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
|
||||
directory. The file can optionally be given a new name.
|
||||
"""
|
||||
uplDir = "dist_upload"
|
||||
if not os.path.isdir(uplDir):
|
||||
os.mkdir(uplDir)
|
||||
if dstName is None:
|
||||
dstName = os.path.basename(srcPath)
|
||||
shutil.copyfile(srcPath, os.path.join(uplDir, dstName))
|
||||
uplDir = Path("dist_upload")
|
||||
uplDir.mkdir(exist_ok=True)
|
||||
srcPath = Path(srcPath)
|
||||
shutil.copyfile(srcPath, uplDir / (dstName or srcPath.name))
|
||||
return
|
||||
|
||||
|
||||
@@ -561,43 +559,24 @@ def importI18nUpdates(sysArgs: list[str]) -> None:
|
||||
# Make Minimal Package (minimal-zip)
|
||||
##
|
||||
|
||||
def makeMinimalPackage(targetOS: int) -> None:
|
||||
def makeWindowsZip() -> None:
|
||||
"""Pack the core source file in a single zip file."""
|
||||
from zipfile import ZipFile, ZIP_DEFLATED
|
||||
|
||||
print("")
|
||||
print("Building Minimal ZIP File")
|
||||
print("Building Windows ZIP File")
|
||||
print("=========================")
|
||||
|
||||
bldDir = "dist_minimal"
|
||||
if not os.path.isdir(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():
|
||||
print("ERROR: Missing build assets")
|
||||
sys.exit(1)
|
||||
|
||||
# Build Minimal Zip
|
||||
# =================
|
||||
|
||||
pkgVers, _, _ = extractVersion()
|
||||
zipFile = f"novelwriter-{pkgVers}-minimal{targName}.zip"
|
||||
zipFile = f"novelwriter-{pkgVers}-minimal-win.zip"
|
||||
outFile = os.path.join(bldDir, zipFile)
|
||||
if os.path.isfile(outFile):
|
||||
os.unlink(outFile)
|
||||
@@ -627,26 +606,8 @@ def makeMinimalPackage(targetOS: int) -> None:
|
||||
continue
|
||||
zipObj.write(os.path.join(nRoot, aFile))
|
||||
|
||||
if targetOS == OS_WIN:
|
||||
zipObj.write("novelWriter.py", "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")
|
||||
zipObj.write("novelWriter.py", "novelWriter.pyw")
|
||||
print("Added: novelWriter.pyw")
|
||||
|
||||
for aFile in rootFiles:
|
||||
print("Added: %s" % aFile)
|
||||
@@ -658,14 +619,9 @@ def makeMinimalPackage(targetOS: int) -> None:
|
||||
print("")
|
||||
print("Created File: %s" % outFile)
|
||||
|
||||
# Create Checksum File
|
||||
# ====================
|
||||
|
||||
shaFile = makeCheckSum(zipFile, cwd=bldDir)
|
||||
|
||||
toUpload(outFile)
|
||||
toUpload(shaFile)
|
||||
|
||||
print("")
|
||||
|
||||
return
|
||||
@@ -675,8 +631,10 @@ def makeMinimalPackage(targetOS: int) -> None:
|
||||
# Make Debian Package (build-deb)
|
||||
##
|
||||
|
||||
def makeDebianPackage(signKey: str | None = None, sourceBuild: bool = False,
|
||||
distName: str = "unstable", buildName: str = "") -> str:
|
||||
def makeDebianPackage(
|
||||
signKey: str | None = None, sourceBuild: bool = False,
|
||||
distName: str = "unstable", buildName: str = "", oldSetuptools: bool = False
|
||||
) -> str:
|
||||
"""Build a Debian package."""
|
||||
print("")
|
||||
print("Build Debian Package")
|
||||
@@ -778,11 +736,27 @@ def makeDebianPackage(signKey: str | None = None, sourceBuild: bool = False,
|
||||
))
|
||||
print("Wrote: setup.py")
|
||||
|
||||
setupCfg = readFile("pyproject.toml").replace(
|
||||
"setup/description_pypi.md", "data/description_short.txt"
|
||||
)
|
||||
writeFile(f"{outDir}/pyproject.toml", setupCfg)
|
||||
print("Wrote: pyproject.toml")
|
||||
if oldSetuptools:
|
||||
# This is needed for Ubuntu up to 22.04
|
||||
setupCfg = readFile("setup/launchpad_setup.cfg").replace(
|
||||
"file: setup/description_pypi.md", "file: data/description_short.txt"
|
||||
)
|
||||
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
|
||||
# =======================
|
||||
@@ -860,15 +834,14 @@ def makeForLaunchpad(doSign: bool = False, isFirst: bool = False) -> None:
|
||||
bldNum = "0"
|
||||
|
||||
distLoop = [
|
||||
("20.04", "focal"),
|
||||
("22.04", "jammy"),
|
||||
("23.10", "mantic"),
|
||||
("24.04", "noble"),
|
||||
("22.04", "jammy", True),
|
||||
("23.10", "mantic", False),
|
||||
("24.04", "noble", False),
|
||||
]
|
||||
|
||||
print("Building Ubuntu packages for:")
|
||||
print("")
|
||||
for distNum, codeName in distLoop:
|
||||
for distNum, codeName, _ in distLoop:
|
||||
print(f" * Ubuntu {distNum} {codeName.title()}")
|
||||
print("")
|
||||
|
||||
@@ -881,13 +854,14 @@ def makeForLaunchpad(doSign: bool = False, isFirst: bool = False) -> None:
|
||||
print("")
|
||||
|
||||
dputCmd = []
|
||||
for distNum, codeName in distLoop:
|
||||
for distNum, codeName, oldSetup in distLoop:
|
||||
buildName = f"ubuntu{distNum}.{bldNum}"
|
||||
dCmd = makeDebianPackage(
|
||||
signKey=signKey,
|
||||
sourceBuild=True,
|
||||
distName=codeName,
|
||||
buildName=buildName
|
||||
buildName=buildName,
|
||||
oldSetuptools=oldSetup,
|
||||
)
|
||||
dputCmd.append(dCmd)
|
||||
|
||||
@@ -1555,226 +1529,6 @@ def xdgUninstall() -> None:
|
||||
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
|
||||
# =============================================================================================== #
|
||||
@@ -1795,19 +1549,6 @@ if __name__ == "__main__":
|
||||
|
||||
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
|
||||
if "--sign" in sysArgs:
|
||||
sysArgs.remove("--sign")
|
||||
@@ -1831,10 +1572,6 @@ if __name__ == "__main__":
|
||||
"novelWriter as a package on Linux, Mac and Windows. The available options",
|
||||
"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:",
|
||||
"",
|
||||
" help Print the help message.",
|
||||
@@ -1855,10 +1592,8 @@ if __name__ == "__main__":
|
||||
"Python Packaging:",
|
||||
"",
|
||||
" import-i18n Import updated i18n files from a zip file.",
|
||||
" minimal-zip Creates a minimal zip file of the core application without",
|
||||
" all the other source files. Defaults to tailor the zip file",
|
||||
" for the current OS, but accepts a target OS flag to build",
|
||||
" for another OS.",
|
||||
" windows-zip Creates a minimal zip file of the core application without",
|
||||
" all the other source files. Used for Windows builds.",
|
||||
" build-deb Build a .deb package for Debian and Ubuntu. Add --sign to ",
|
||||
" sign package.",
|
||||
" build-ubuntu Build a .deb packages Launchpad. Add --sign to ",
|
||||
@@ -1870,17 +1605,11 @@ if __name__ == "__main__":
|
||||
"",
|
||||
"System Install:",
|
||||
"",
|
||||
" install Deprecated. Use pip or build.",
|
||||
" xdg-install Install launcher and icons for freedesktop systems. Run as",
|
||||
" root or with sudo for system-wide install, or as user for",
|
||||
" single user install.",
|
||||
" xdg-uninstall Remove the launcher and icons for the current system as",
|
||||
" 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)
|
||||
sys.exit(0) # Don't continue execution
|
||||
|
||||
if "minimal-zip" in sysArgs:
|
||||
sysArgs.remove("minimal-zip")
|
||||
makeMinimalPackage(targetOS)
|
||||
if "windows-zip" in sysArgs:
|
||||
sysArgs.remove("windows-zip")
|
||||
makeWindowsZip()
|
||||
|
||||
if "build-deb" in sysArgs:
|
||||
sysArgs.remove("build-deb")
|
||||
@@ -1997,20 +1726,4 @@ if __name__ == "__main__":
|
||||
else:
|
||||
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
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ classifiers = [
|
||||
]
|
||||
requires-python = ">=3.8"
|
||||
dependencies = [
|
||||
"pyqt5>=5.10",
|
||||
"pyqt5>=5.15",
|
||||
"pyenchant>=3.0.0",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
pyqt5>=5.10
|
||||
pyqt5>=5.15
|
||||
pyenchant>=3.0.0
|
||||
|
||||
@@ -2,14 +2,14 @@ Source: novelwriter
|
||||
Maintainer: Veronica Berglyd Olsen <code@vkbo.net>
|
||||
Section: text
|
||||
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
|
||||
Homepage: https://novelwriter.io
|
||||
X-Python3-Version: >= 3.8
|
||||
|
||||
Package: novelwriter
|
||||
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
|
||||
novelWriter is a plain text editor designed for writing novels assembled from
|
||||
many smaller text documents. It uses a minimal formatting syntax inspired by
|
||||
|
||||
@@ -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
|
||||
@@ -22,12 +22,10 @@ python3 pkgutils.py qtlrelease manual sample
|
||||
deactivate
|
||||
|
||||
echo ""
|
||||
echo " Building Minimal Packages"
|
||||
echo " Building Windows Source Zip"
|
||||
echo "================================================================================"
|
||||
echo ""
|
||||
python3 pkgutils.py minimal-zip --target-win
|
||||
# python3 pkgutils.py minimal-zip --target-linux
|
||||
# python3 pkgutils.py minimal-zip --target-darwin
|
||||
python3 pkgutils.py windows-zip
|
||||
|
||||
echo ""
|
||||
echo " Building Linux Packages"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -427,14 +427,7 @@ def testCoreTools_DocSearch(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText):
|
||||
# ========
|
||||
|
||||
# Escape Using QRegularExpression
|
||||
with monkeypatch.context() as mp:
|
||||
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_\]\+"
|
||||
assert search._buildPattern("[A-Za-z0-9_]+") == r"\[A\-Za\-z0\-9_\]\+"
|
||||
|
||||
# Whole Words
|
||||
search.setWholeWords(True)
|
||||
|
||||
@@ -26,7 +26,7 @@ from tools import C, buildTestProject
|
||||
|
||||
from PyQt5.QtGui import QIcon
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter import SHARED
|
||||
from novelwriter.constants import nwLists
|
||||
from novelwriter.core.item import NWItem
|
||||
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)
|
||||
|
||||
# Check Panel Tab Visibility
|
||||
if CONFIG.verQtValue >= 0x050f00:
|
||||
assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["CHARACTER"]) is True
|
||||
assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["PLOT"]) 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["OBJECT"]) 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["CHARACTER"]) is True
|
||||
assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["PLOT"]) 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["OBJECT"]) is False
|
||||
assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["ENTITY"]) is False
|
||||
assert viewPanel.mainTabs.isTabVisible(viewPanel.idTabs["CUSTOM"]) is False
|
||||
|
||||
# Check Character Tab
|
||||
charTab = viewPanel.kwTabs["CHARACTER"]
|
||||
|
||||
Reference in New Issue
Block a user