Merge branch 'main' into dev

This commit is contained in:
Veronica Berglyd Olsen
2023-11-05 18:54:52 +01:00
29 changed files with 576 additions and 464 deletions
+13
View File
@@ -5,6 +5,8 @@
<h2>Release Notes for 2.1</h2>
<p><i>Released on 17 October 2023</i></p>
<p>Scroll down for <a href="#patch">Patch Notes</a></p>
<p>The primary focus of this release has been a complete redesign of the Build Tool, that is, the
tool that assembles your project into a manuscript document. The new tool, called the "Manuscript
Build Tool" allows you to define multiple build definitions for your project. The build definitions
@@ -30,5 +32,16 @@ a full list of changes, see the detailed changelogs.</p>
<p><i>See also the <a href="https://github.com/vkbo/novelWriter/releases">Releases</a> page.</i></p>
<a name="patch"></a><h2>Patch Notes</h2>
<h3>Patch 2.2.1 &ndash; 5 November 2023</h3>
<p>This is a patch release that fixes a layout issue and internationalisation issues with the new
Manuscript Build tool. It also fixes a number of issues related to bugs in the underlying Qt
framework that affects drag and drop functionality in the project tree. These issues were mostly
only affecting Debian Linux package releases.</p>
<p>Other, minor issues related to updating the editor on colour theme change and project word list
changes have been fixed as well. See the full changelog for more details.</p>
</body>
</html>
+8 -6
View File
@@ -81,10 +81,12 @@ class Config:
# Localisation
# Note that these paths must be strings
self._qLocale = QLocale.system()
self._qtTrans = {}
self._nwLangPath = self._appPath / "assets" / "i18n"
self._qtLangPath = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
self._nwLangPath = str(self._appPath / "assets" / "i18n")
wantedLocale = self._nwLangPath / f"nw_{QLocale.system().name()}.qm"
self._qLocale = QLocale.system() if wantedLocale.exists() else QLocale("en_GB")
self._qtTrans = {}
# PDF Manual
pdfDocs = self._appPath / "assets" / "manual.pdf"
@@ -425,7 +427,7 @@ class Config:
else:
return []
for qmFile in Path(self._nwLangPath).iterdir():
for qmFile in self._nwLangPath.iterdir():
qmName = qmFile.name
if not (qmFile.is_file() and qmName.startswith(fPre) and qmName.endswith(fExt)):
continue
@@ -493,8 +495,8 @@ class Config:
self._qtTrans = {}
langList = [
(self._qtLangPath, "qtbase"), # Qt 5.x
(self._nwLangPath, "nw"), # novelWriter
(self._qtLangPath, "qtbase"), # Qt 5.x
(str(self._nwLangPath), "nw"), # novelWriter
]
for lngPath, lngBase in langList:
for lngCode in self._qLocale.uiLanguages():
+2 -2
View File
@@ -32,7 +32,7 @@ from enum import Enum
from typing import Iterable
from pathlib import Path
from PyQt5.QtCore import QT_TRANSLATE_NOOP
from PyQt5.QtCore import QT_TRANSLATE_NOOP, QCoreApplication
from novelwriter import CONFIG
from novelwriter.enum import nwBuildFmt
@@ -209,7 +209,7 @@ class BuildSettings:
@staticmethod
def getLabel(key: str) -> str:
"""Extract the GUI label for a specific setting."""
return SETTINGS_LABELS.get(key, "ERROR")
return QCoreApplication.translate("Builds", SETTINGS_LABELS.get(key, "ERROR"))
def getStr(self, key: str) -> str:
"""Type safe value access for strings."""
+2 -2
View File
@@ -162,7 +162,7 @@ class ToHtml(Tokenizer):
pStyle = None
lines = []
for tType, tLine, tText, tFormat, tStyle in self._tokens:
for tType, nHead, tText, tFormat, tStyle in self._tokens:
# Replace < and > with HTML entities
if tFormat:
@@ -223,7 +223,7 @@ class ToHtml(Tokenizer):
hStyle = ""
if self._linkHeaders:
aNm = f"<a name='T{tLine:06d}'></a>"
aNm = f"<a name='T{nHead:04d}'></a>"
else:
aNm = ""
+23 -18
View File
@@ -382,7 +382,7 @@ class Tokenizer(ABC):
The format of the token list is an entry with a five-tuple for
each line in the file. The tuple is as follows:
1: The type of the block, self.T_*
2: The line in the file where this block occurred
2: The header number under which the text is placed
3: The text content of the block, without leading tags
4: The internal formatting map of the text, self.FMT_*
5: The style of the block, self.A_*
@@ -396,16 +396,15 @@ class Tokenizer(ABC):
self._tokens = []
tmpMarkdown = []
nLine = 0
nHead = 0
breakNext = False
for aLine in self._text.splitlines():
nLine += 1
sLine = aLine.strip()
# Check for blank lines
if len(sLine) == 0:
self._tokens.append((
self.T_EMPTY, nLine, "", None, self.A_NONE
self.T_EMPTY, nHead, "", None, self.A_NONE
))
if self._keepMarkdown:
tmpMarkdown.append("\n")
@@ -430,7 +429,7 @@ class Tokenizer(ABC):
elif sLine == "[VSPACE]":
self._tokens.append(
(self.T_SKIP, nLine, "", None, sAlign)
(self.T_SKIP, nHead, "", None, sAlign)
)
continue
@@ -438,11 +437,11 @@ class Tokenizer(ABC):
nSkip = checkInt(sLine[8:-1], 0)
if nSkip >= 1:
self._tokens.append(
(self.T_SKIP, nLine, "", None, sAlign)
(self.T_SKIP, nHead, "", None, sAlign)
)
if nSkip > 1:
self._tokens += (nSkip - 1) * [
(self.T_SKIP, nLine, "", None, self.A_NONE)
(self.T_SKIP, nHead, "", None, self.A_NONE)
]
continue
@@ -451,20 +450,20 @@ class Tokenizer(ABC):
synTag = cLine[:9].lower()
if synTag == "synopsis:":
self._tokens.append((
self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, sAlign
self.T_SYNOPSIS, nHead, cLine[9:].strip(), None, sAlign
))
if self._doSynopsis and self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
else:
self._tokens.append((
self.T_COMMENT, nLine, aLine[1:].strip(), None, sAlign
self.T_COMMENT, nHead, aLine[1:].strip(), None, sAlign
))
if self._doComments and self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[0] == "@":
self._tokens.append((
self.T_KEYWORD, nLine, aLine[1:].strip(), None, sAlign
self.T_KEYWORD, nHead, aLine[1:].strip(), None, sAlign
))
if self._doKeywords and self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
@@ -474,8 +473,9 @@ class Tokenizer(ABC):
sAlign |= self.A_CENTRE
sAlign |= self.A_PBB
nHead += 1
self._tokens.append((
self.T_HEAD1, nLine, aLine[2:].strip(), None, sAlign
self.T_HEAD1, nHead, aLine[2:].strip(), None, sAlign
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
@@ -484,39 +484,44 @@ class Tokenizer(ABC):
if self._isNovel:
sAlign |= self.A_PBB
nHead += 1
self._tokens.append((
self.T_HEAD2, nLine, aLine[3:].strip(), None, sAlign
self.T_HEAD2, nHead, aLine[3:].strip(), None, sAlign
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "### ":
nHead += 1
self._tokens.append((
self.T_HEAD3, nLine, aLine[4:].strip(), None, sAlign
self.T_HEAD3, nHead, aLine[4:].strip(), None, sAlign
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:5] == "#### ":
nHead += 1
self._tokens.append((
self.T_HEAD4, nLine, aLine[5:].strip(), None, sAlign
self.T_HEAD4, nHead, aLine[5:].strip(), None, sAlign
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "#! ":
nHead += 1
if self._isNovel:
tStyle = self.T_TITLE
else:
tStyle = self.T_HEAD1
self._tokens.append((
tStyle, nLine, aLine[3:].strip(), None, sAlign | self.A_CENTRE
tStyle, nHead, aLine[3:].strip(), None, sAlign | self.A_CENTRE
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "##! ":
nHead += 1
if self._isNovel:
tStyle = self.T_UNNUM
sAlign |= self.A_PBB
@@ -524,7 +529,7 @@ class Tokenizer(ABC):
tStyle = self.T_HEAD2
self._tokens.append((
tStyle, nLine, aLine[4:].strip(), None, sAlign
tStyle, nHead, aLine[4:].strip(), None, sAlign
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
@@ -581,7 +586,7 @@ class Tokenizer(ABC):
# sorted by position
fmtPos = sorted(fmtPos, key=itemgetter(0))
self._tokens.append((
self.T_TEXT, nLine, aLine, fmtPos, sAlign
self.T_TEXT, nHead, aLine, fmtPos, sAlign
))
if self._keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
@@ -600,7 +605,7 @@ class Tokenizer(ABC):
# Always add an empty line at the end of the file
self._tokens.append((
self.T_EMPTY, nLine, "", None, self.A_NONE
self.T_EMPTY, nHead, "", None, self.A_NONE
))
if self._keepMarkdown:
tmpMarkdown.append("\n")
+2
View File
@@ -176,6 +176,8 @@ class GuiPreferencesGeneral(QWidget):
for lang, langName in theLangs:
self.guiLocale.addItem(langName, lang)
langIdx = self.guiLocale.findData(CONFIG.guiLocale)
if langIdx < 0:
langIdx = self.guiLocale.findData("en_GB")
if langIdx != -1:
self.guiLocale.setCurrentIndex(langIdx)
+3 -1
View File
@@ -339,6 +339,8 @@ class GuiDocEditor(QPlainTextEdit):
self.clearEditor()
else:
self.redrawText()
if not self._bigDoc:
self.highLight.rehighlight()
return
@@ -980,7 +982,7 @@ class GuiDocEditor(QPlainTextEdit):
uCursor = self.textCursor()
pCursor = self.cursorForPosition(pos)
ctxMenu = QMenu()
ctxMenu = QMenu(self)
# Follow
if self._followTag(cursor=pCursor, loadTag=False):
+1 -1
View File
@@ -381,7 +381,7 @@ class GuiDocViewer(QTextBrowser):
userCursor = self.textCursor()
userSelection = userCursor.hasSelection()
mnuContext = QMenu()
mnuContext = QMenu(self)
# Cut, Copy and Paste
# ===================
+1 -1
View File
@@ -228,7 +228,7 @@ class GuiNovelToolBar(QWidget):
self.tbRefresh.clicked.connect(self._refreshNovelTree)
# More Options Menu
self.mMore = QMenu()
self.mMore = QMenu(self)
self.mLastCol = self.mMore.addMenu(self.tr("Last Column"))
self.gLastCol = QActionGroup(self.mMore)
+54 -7
View File
@@ -31,8 +31,8 @@ from enum import Enum
from time import time
from typing import TYPE_CHECKING
from PyQt5.QtGui import QDropEvent, QMouseEvent, QPalette
from PyQt5.QtCore import QPoint, Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QDragMoveEvent, QDropEvent, QMouseEvent, QPalette
from PyQt5.QtCore import QPoint, QTimer, Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel,
QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem,
@@ -248,7 +248,7 @@ class GuiProjectToolBar(QWidget):
self.viewLabel.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
# Quick Links
self.mQuick = QMenu()
self.mQuick = QMenu(self)
self.tbQuick = QToolButton(self)
self.tbQuick.setToolTip("%s [Ctrl+L]" % self.tr("Quick Links"))
@@ -269,7 +269,7 @@ class GuiProjectToolBar(QWidget):
self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1))
# Add Item Menu
self.mAdd = QMenu()
self.mAdd = QMenu(self)
self.aAddEmpty = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["document"]))
self.aAddEmpty.triggered.connect(
@@ -307,7 +307,7 @@ class GuiProjectToolBar(QWidget):
self.tbAdd.setPopupMode(QToolButton.InstantPopup)
# More Options Menu
self.mMore = QMenu()
self.mMore = QMenu(self)
self.aExpand = self.mMore.addAction(self.tr("Expand All"))
self.aExpand.triggered.connect(lambda: self.projTree.setExpandedFromHandle(None, True))
@@ -507,8 +507,15 @@ class GuiProjectTree(QTreeWidget):
# Allow Move by Drag & Drop
self.setDragEnabled(True)
self.setDragDropMode(QAbstractItemView.InternalMove)
self.setDropIndicatorShown(True)
# Disable built-in autoscroll as it isn't working in some Qt
# releases (see #1561) and instead use our own implementation
self.setAutoScroll(False)
# But don't allow drop on root level
# Due to a bug, this stops working somewhere between Qt 5.15.3
# and 5.15.8, so this is also blocked in dropEvent (see #1569)
trRoot = self.invisibleRootItem()
trRoot.setFlags(trRoot.flags() ^ Qt.ItemIsDropEnabled)
@@ -524,6 +531,13 @@ class GuiProjectTree(QTreeWidget):
self.itemDoubleClicked.connect(self._treeDoubleClick)
self.itemSelectionChanged.connect(self._treeSelectionChange)
# Autoscroll
self._scrollMargin = SHARED.theme.baseIconSize
self._scrollDirection = 0
self._scrollTimer = QTimer()
self._scrollTimer.timeout.connect(self._doAutoScroll)
self._scrollTimer.setInterval(250)
# Set custom settings
self.initSettings()
@@ -1196,7 +1210,7 @@ class GuiProjectTree(QTreeWidget):
logger.debug("No item found")
return False
ctxMenu = QMenu()
ctxMenu = QMenu(self)
# Trash Folder
# ============
@@ -1343,6 +1357,17 @@ class GuiProjectTree(QTreeWidget):
return True
@pyqtSlot()
def _doAutoScroll(self) -> None:
"""Scroll one item up or down based on direction value."""
if self._scrollDirection == -1:
self.scrollToItem(self.itemAbove(self.itemAt(1, 1)))
elif self._scrollDirection == 1:
self.scrollToItem(self.itemBelow(self.itemAt(1, self.height() - 1)))
self._scrollDirection = 0
self._scrollTimer.stop()
return
##
# Events
##
@@ -1374,14 +1399,36 @@ class GuiProjectTree(QTreeWidget):
return
def dragMoveEvent(self, event: QDragMoveEvent) -> None:
"""Capture the drag move event to enable edge autoscroll."""
y = event.pos().y()
if y < self._scrollMargin:
if not self._scrollTimer.isActive():
self._scrollDirection = -1
self._scrollTimer.start()
elif y > self.height() - self._scrollMargin:
if not self._scrollTimer.isActive():
self._scrollDirection = 1
self._scrollTimer.start()
super().dragMoveEvent(event)
return
def dropEvent(self, event: QDropEvent) -> None:
"""Overload the drop item event to ensure relevant data has been
updated.
"""
sHandle = self.getSelectedHandle()
sItem = self._getTreeItem(sHandle) if sHandle else None
if sHandle is None or sItem is None:
if sHandle is None or sItem is None or sItem.parent() is None:
logger.error("Invalid drag and drop event")
event.ignore()
return
if not self.indexAt(event.pos()).isValid():
# Needed due to a bug somewhere around Qt 5.15.8 that
# ignores the invisible root item flags
logger.error("Invalid drop location")
event.ignore()
return
logger.debug("Drag'n'drop of item '%s' accepted", sHandle)
+1
View File
@@ -991,6 +991,7 @@ class GuiMain(QMainWindow):
if dlgWords.result() == QDialog.Accepted:
logger.debug("Reloading word list")
SHARED.updateSpellCheckLanguage(reload=True)
self.docEditor.spellCheckDocument()
return True
+8 -6
View File
@@ -332,14 +332,16 @@ class _FilterTab(QWidget):
treeHeader = self.optTree.header()
treeHeader.setStretchLastSection(False)
treeHeader.setMinimumSectionSize(iPx + cMg) # See Issue #1551
treeHeader.setSectionResizeMode(self.C_NAME, QHeaderView.Stretch)
treeHeader.setSectionResizeMode(self.C_ACTIVE, QHeaderView.Fixed)
treeHeader.setSectionResizeMode(self.C_STATUS, QHeaderView.Fixed)
treeHeader.resizeSection(self.C_ACTIVE, iPx + cMg)
treeHeader.resizeSection(self.C_STATUS, iPx + cMg)
self.optTree.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.optTree.setDragDropMode(QAbstractItemView.NoDragDrop)
self.optTree.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.optTree.setSelectionBehavior(QAbstractItemView.SelectRows)
# Filters
# =======
@@ -390,11 +392,11 @@ class _FilterTab(QWidget):
self.mainSplit.addWidget(self.filterOpt)
self.mainSplit.setCollapsible(0, False)
self.mainSplit.setCollapsible(1, False)
self.mainSplit.setStretchFactor(0, 0)
self.mainSplit.setStretchFactor(1, 1)
self.mainSplit.setStretchFactor(0, 1)
self.mainSplit.setStretchFactor(1, 0)
self.mainSplit.setSizes([
CONFIG.pxInt(pOptions.getInt("GuiBuildSettings", "treeWidth", 1)),
CONFIG.pxInt(pOptions.getInt("GuiBuildSettings", "filterWidth", 1))
CONFIG.pxInt(pOptions.getInt("GuiBuildSettings", "treeWidth", 300)),
CONFIG.pxInt(pOptions.getInt("GuiBuildSettings", "filterWidth", 300))
])
self.outerBox = QHBoxLayout()
@@ -718,7 +720,7 @@ class _HeadingsTab(QWidget):
self.formSyntax = _HeadingSyntaxHighlighter(self.editTextBox.document())
self.menuInsert = QMenu()
self.menuInsert = QMenu(self)
self.aInsTitle = self.menuInsert.addAction(self.tr("Title"))
self.aInsChNum = self.menuInsert.addAction(self.tr("Chapter Number"))
self.aInsChWord = self.menuInsert.addAction(self.tr("Chapter Number (Word)"))