diff --git a/novelwriter/common.py b/novelwriter/common.py index bf150794..92b304e7 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -436,7 +436,7 @@ def describeFont(font: QFont) -> str: def qtLambda(func: Callable, *args: Any, **kwargs: Any) -> Callable: """A replacement for Python lambdas that works for Qt slots.""" - def wrapper(*args_: Any, **kwargs_: Any) -> None: + def wrapper(*a_: Any) -> None: func(*args, **kwargs) return wrapper diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py index 9f813abc..419e4ec2 100644 --- a/novelwriter/core/options.py +++ b/novelwriter/core/options.py @@ -73,6 +73,9 @@ VALID_MAP: dict[str, set[str]] = { "widthCol3", "widthCol4", "wordsPerPage", "countFrom", "clearDouble", "novelRoot", }, + "GuiOutlineDetails": { + "detailsWidth", "tagsWidth", + } } diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index 7e7b4f4a..6cbff7d2 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -35,7 +35,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.common import simplified +from novelwriter.common import qtLambda, simplified from novelwriter.constants import nwLabels, trConst from novelwriter.core.status import NWStatus, StatusEntry from novelwriter.enum import nwStatusShape @@ -365,10 +365,10 @@ class _StatusPage(NFixedPage): self.delButton.clicked.connect(self._delItem) self.upButton = NIconToolButton(self, iSz, "up") - self.upButton.clicked.connect(lambda: self._moveItem(-1)) + self.upButton.clicked.connect(qtLambda(self._moveItem, -1)) self.dnButton = NIconToolButton(self, iSz, "down") - self.dnButton.clicked.connect(lambda: self._moveItem(1)) + self.dnButton.clicked.connect(qtLambda(self._moveItem, 1)) # Edit Form self.editName = QLineEdit(self) diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index a304b12e..aa6248e2 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -38,6 +38,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED +from novelwriter.common import qtLambda from novelwriter.constants import nwStyles, nwUnicode from novelwriter.enum import nwDocAction, nwDocMode, nwItemType from novelwriter.error import logException @@ -396,25 +397,25 @@ class GuiDocViewer(QTextBrowser): if userSelection: mnuCopy = QAction(self.tr("Copy"), ctxMenu) - mnuCopy.triggered.connect(lambda: self.docAction(nwDocAction.COPY)) + mnuCopy.triggered.connect(qtLambda(self.docAction, nwDocAction.COPY)) ctxMenu.addAction(mnuCopy) ctxMenu.addSeparator() mnuSelAll = QAction(self.tr("Select All"), ctxMenu) - mnuSelAll.triggered.connect(lambda: self.docAction(nwDocAction.SEL_ALL)) + mnuSelAll.triggered.connect(qtLambda(self.docAction, nwDocAction.SEL_ALL)) ctxMenu.addAction(mnuSelAll) mnuSelWord = QAction(self.tr("Select Word"), ctxMenu) - mnuSelWord.triggered.connect( - lambda: self._makePosSelection(QTextCursor.SelectionType.WordUnderCursor, point) - ) + mnuSelWord.triggered.connect(qtLambda( + self._makePosSelection, QTextCursor.SelectionType.WordUnderCursor, point + )) ctxMenu.addAction(mnuSelWord) mnuSelPara = QAction(self.tr("Select Paragraph"), ctxMenu) - mnuSelPara.triggered.connect( - lambda: self._makePosSelection(QTextCursor.SelectionType.BlockUnderCursor, point) - ) + mnuSelPara.triggered.connect(qtLambda( + self._makePosSelection, QTextCursor.SelectionType.BlockUnderCursor, point + )) ctxMenu.addAction(mnuSelPara) # Open the context menu diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index 73542889..6d67d74c 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -30,7 +30,7 @@ from PyQt5.QtWidgets import QGridLayout, QLabel, QWidget from novelwriter import CONFIG, SHARED from novelwriter.common import elide -from novelwriter.constants import nwLabels, trConst +from novelwriter.constants import nwLabels, nwStats, trConst from novelwriter.types import ( QtAlignLeft, QtAlignLeftBase, QtAlignRight, QtAlignRightBase, QtAlignRightMiddle @@ -62,6 +62,10 @@ class GuiItemDetails(QWidget): fntValue = self.font() fntValue.setPointSizeF(0.9*fPt) + trStats1 = trConst(nwLabels.STATS_NAME[nwStats.CHARS_ALL]) + trStats2 = trConst(nwLabels.STATS_NAME[nwStats.WORDS_ALL]) + trStats3 = trConst(nwLabels.STATS_NAME[nwStats.PARAGRAPHS]) + # Label self.labelName = QLabel(self.tr("Label"), self) self.labelName.setFont(fntLabel) @@ -113,7 +117,7 @@ class GuiItemDetails(QWidget): self.usageData.setWordWrap(True) # Character Count - self.cCountName = QLabel(" "+self.tr("Characters"), self) + self.cCountName = QLabel(trStats1, self) self.cCountName.setFont(fntLabel) self.cCountName.setAlignment(QtAlignRight) @@ -122,7 +126,7 @@ class GuiItemDetails(QWidget): self.cCountData.setAlignment(QtAlignRight) # Word Count - self.wCountName = QLabel(" "+self.tr("Words"), self) + self.wCountName = QLabel(trStats2, self) self.wCountName.setFont(fntLabel) self.wCountName.setAlignment(QtAlignRight) @@ -131,7 +135,7 @@ class GuiItemDetails(QWidget): self.wCountData.setAlignment(QtAlignRight) # Paragraph Count - self.pCountName = QLabel(" "+self.tr("Paragraphs"), self) + self.pCountName = QLabel(trStats3, self) self.pCountName.setFont(fntLabel) self.pCountName.setAlignment(QtAlignRight) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 8db8a1e0..795622c0 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -39,7 +39,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.common import minmax +from novelwriter.common import minmax, qtLambda from novelwriter.constants import nwKeyWords, nwLabels, nwStyles, trConst from novelwriter.core.index import IndexHeading from novelwriter.enum import nwDocMode, nwItemClass, nwOutline @@ -346,7 +346,7 @@ class GuiNovelToolBar(QWidget): aLast = self.mLastCol.addAction(actionLabel) aLast.setCheckable(True) aLast.setActionGroup(self.gLastCol) - aLast.triggered.connect(lambda: self.setLastColType(colType)) + aLast.triggered.connect(qtLambda(self.setLastColType, colType)) self.aLastCol[colType] = aLast return @@ -484,16 +484,9 @@ class GuiNovelTree(QTreeWidget): if rootHandle is None: rootHandle = SHARED.project.tree.findRoot(nwItemClass.NOVEL) - treeChanged = SHARED.mainGui.projView.changedSince(self._lastBuild) - indexChanged = SHARED.project.index.rootChangedSince(rootHandle, self._lastBuild) - if not (treeChanged or indexChanged or overRide): - logger.debug("No changes have been made to the novel index") - return - - selItem = self.selectedItems() titleKey = None - if selItem: - titleKey = selItem[0].data(self.C_DATA, self.D_KEY) + if selItems := self.selectedItems(): + titleKey = selItems[0].data(self.C_DATA, self.D_KEY) self._populateTree(rootHandle) SHARED.project.data.setLastHandle(rootHandle, "novelTree") diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 54b8102e..ee0e1c9f 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -42,7 +42,7 @@ from PyQt5.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.common import checkInt, formatFileFilter, makeFileNameSafe -from novelwriter.constants import nwKeyWords, nwLabels, nwStyles, trConst +from novelwriter.constants import nwKeyWords, nwLabels, nwStats, nwStyles, trConst from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline from novelwriter.error import logException from novelwriter.extensions.configlayout import NColourLabel @@ -135,12 +135,14 @@ class GuiOutlineView(QWidget): self.outlineBar.populateNovelList() self.outlineBar.setCurrentRoot(lastOutline) self.outlineBar.setEnabled(True) + self.outlineData.loadGuiSettings() return def closeProjectTasks(self) -> None: """Run closing project tasks.""" self.outlineTree.closeProjectTasks() + self.outlineData.saveGuiSettings() self.outlineData.updateClasses() self.clearOutline() return @@ -799,6 +801,10 @@ class GuiOutlineDetails(QScrollArea): bFont = SHARED.theme.guiFontB + trStats1 = trConst(nwLabels.STATS_NAME[nwStats.CHARS_ALL]) + trStats2 = trConst(nwLabels.STATS_NAME[nwStats.WORDS_ALL]) + trStats3 = trConst(nwLabels.STATS_NAME[nwStats.PARAGRAPHS]) + # Details Area self.titleLabel = QLabel(self.tr("Title"), self) self.fileLabel = QLabel(self.tr("Document"), self) @@ -819,9 +825,9 @@ class GuiOutlineDetails(QScrollArea): self.itemValue.setMaximumWidth(maxTitle) # Stats Area - self.cCLabel = QLabel(self.tr("Characters"), self) - self.wCLabel = QLabel(self.tr("Words"), self) - self.pCLabel = QLabel(self.tr("Paragraphs"), self) + self.cCLabel = QLabel(trStats1, self) + self.wCLabel = QLabel(trStats2, self) + self.pCLabel = QLabel(trStats3, self) self.cCValue = QLabel("", self) self.wCValue = QLabel("", self) self.pCValue = QLabel("", self) @@ -848,94 +854,9 @@ class GuiOutlineDetails(QScrollArea): self.synopLWrap = QHBoxLayout() self.synopLWrap.addWidget(self.synopValue, 1) - # Tags - self.povKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.POV_KEY]), self) - self.focKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY]), self) - self.chrKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY]), self) - self.pltKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY]), self) - self.timKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.TIME_KEY]), self) - self.wldKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.WORLD_KEY]), self) - self.objKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.OBJECT_KEY]), self) - self.entKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.ENTITY_KEY]), self) - self.cstKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY]), self) - self.styKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.STORY_KEY]), self) - self.mntKeyLabel = QLabel(trConst(nwLabels.KEY_NAME[nwKeyWords.MENTION_KEY]), self) - - self.povKeyLabel.setFont(bFont) - self.focKeyLabel.setFont(bFont) - self.chrKeyLabel.setFont(bFont) - self.pltKeyLabel.setFont(bFont) - self.timKeyLabel.setFont(bFont) - self.wldKeyLabel.setFont(bFont) - self.objKeyLabel.setFont(bFont) - self.entKeyLabel.setFont(bFont) - self.cstKeyLabel.setFont(bFont) - self.styKeyLabel.setFont(bFont) - self.mntKeyLabel.setFont(bFont) - - self.povKeyLWrap = QHBoxLayout() - self.focKeyLWrap = QHBoxLayout() - self.chrKeyLWrap = QHBoxLayout() - self.pltKeyLWrap = QHBoxLayout() - self.timKeyLWrap = QHBoxLayout() - self.wldKeyLWrap = QHBoxLayout() - self.objKeyLWrap = QHBoxLayout() - self.entKeyLWrap = QHBoxLayout() - self.cstKeyLWrap = QHBoxLayout() - self.styKeyLWrap = QHBoxLayout() - self.mntKeyLWrap = QHBoxLayout() - - self.povKeyValue = QLabel("", self) - self.focKeyValue = QLabel("", self) - self.chrKeyValue = QLabel("", self) - self.pltKeyValue = QLabel("", self) - self.timKeyValue = QLabel("", self) - self.wldKeyValue = QLabel("", self) - self.objKeyValue = QLabel("", self) - self.entKeyValue = QLabel("", self) - self.cstKeyValue = QLabel("", self) - self.styKeyValue = QLabel("", self) - self.mntKeyValue = QLabel("", self) - - self.povKeyValue.setWordWrap(True) - self.focKeyValue.setWordWrap(True) - self.chrKeyValue.setWordWrap(True) - self.pltKeyValue.setWordWrap(True) - self.timKeyValue.setWordWrap(True) - self.wldKeyValue.setWordWrap(True) - self.objKeyValue.setWordWrap(True) - self.entKeyValue.setWordWrap(True) - self.cstKeyValue.setWordWrap(True) - self.styKeyValue.setWordWrap(True) - self.mntKeyValue.setWordWrap(True) - - self.povKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) - self.focKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) - self.chrKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) - self.pltKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) - self.timKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) - self.wldKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) - self.objKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) - self.entKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) - self.cstKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) - self.styKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) - self.mntKeyValue.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) - - self.povKeyLWrap.addWidget(self.povKeyValue, 1) - self.focKeyLWrap.addWidget(self.focKeyValue, 1) - self.chrKeyLWrap.addWidget(self.chrKeyValue, 1) - self.pltKeyLWrap.addWidget(self.pltKeyValue, 1) - self.timKeyLWrap.addWidget(self.timKeyValue, 1) - self.wldKeyLWrap.addWidget(self.wldKeyValue, 1) - self.objKeyLWrap.addWidget(self.objKeyValue, 1) - self.entKeyLWrap.addWidget(self.entKeyValue, 1) - self.cstKeyLWrap.addWidget(self.cstKeyValue, 1) - self.styKeyLWrap.addWidget(self.styKeyValue, 1) - self.mntKeyLWrap.addWidget(self.mntKeyValue, 1) - # Selected Item Details - self.mainGroup = QGroupBox(self.tr("Title Details"), self) self.mainForm = QGridLayout() + self.mainGroup = QGroupBox(self.tr("Title Details"), self) self.mainGroup.setLayout(self.mainForm) self.mainForm.addWidget(self.titleLabel, 0, 0, 1, 1, QtAlignLeftTop) @@ -959,46 +880,37 @@ class GuiOutlineDetails(QScrollArea): self.mainForm.setVerticalSpacing(vSpace) # Selected Item Tags - self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self) self.tagsForm = QGridLayout() + self.tagsGroup = QGroupBox(self.tr("Reference Tags"), self) self.tagsGroup.setLayout(self.tagsForm) - self.tagsForm.addWidget(self.povKeyLabel, 0, 0, QtAlignLeftTop) - self.tagsForm.addLayout(self.povKeyLWrap, 0, 1, QtAlignLeftTop) - self.tagsForm.addWidget(self.focKeyLabel, 1, 0, QtAlignLeftTop) - self.tagsForm.addLayout(self.focKeyLWrap, 1, 1, QtAlignLeftTop) - self.tagsForm.addWidget(self.chrKeyLabel, 2, 0, QtAlignLeftTop) - self.tagsForm.addLayout(self.chrKeyLWrap, 2, 1, QtAlignLeftTop) - self.tagsForm.addWidget(self.pltKeyLabel, 3, 0, QtAlignLeftTop) - self.tagsForm.addLayout(self.pltKeyLWrap, 3, 1, QtAlignLeftTop) - self.tagsForm.addWidget(self.timKeyLabel, 4, 0, QtAlignLeftTop) - self.tagsForm.addLayout(self.timKeyLWrap, 4, 1, QtAlignLeftTop) - self.tagsForm.addWidget(self.wldKeyLabel, 5, 0, QtAlignLeftTop) - self.tagsForm.addLayout(self.wldKeyLWrap, 5, 1, QtAlignLeftTop) - self.tagsForm.addWidget(self.objKeyLabel, 6, 0, QtAlignLeftTop) - self.tagsForm.addLayout(self.objKeyLWrap, 6, 1, QtAlignLeftTop) - self.tagsForm.addWidget(self.entKeyLabel, 7, 0, QtAlignLeftTop) - self.tagsForm.addLayout(self.entKeyLWrap, 7, 1, QtAlignLeftTop) - self.tagsForm.addWidget(self.cstKeyLabel, 8, 0, QtAlignLeftTop) - self.tagsForm.addLayout(self.cstKeyLWrap, 8, 1, QtAlignLeftTop) - self.tagsForm.addWidget(self.styKeyLabel, 9, 0, QtAlignLeftTop) - self.tagsForm.addLayout(self.styKeyLWrap, 9, 1, QtAlignLeftTop) - self.tagsForm.addWidget(self.mntKeyLabel, 10, 0, QtAlignLeftTop) - self.tagsForm.addLayout(self.mntKeyLWrap, 10, 1, QtAlignLeftTop) + self.tagValues: dict[str, tuple[QLabel, QLabel]] = {} + for key in nwKeyWords.ALL_KEYS: + label = QLabel(trConst(nwLabels.KEY_NAME[key]), self) + label.setFont(bFont) + value = QLabel("", self) + value.setWordWrap(True) + value.linkActivated.connect(lambda x: self.itemTagClicked.emit(x)) + layout = QHBoxLayout() + layout.addWidget(value, 1) + n = len(self.tagValues) + self.tagsForm.addWidget(label, n, 0, QtAlignLeftTop) + self.tagsForm.addLayout(layout, n, 1, QtAlignLeftTop) + self.tagValues[key] = (label, value) self.tagsForm.setColumnStretch(1, 1) - self.tagsForm.setRowStretch(10, 1) + self.tagsForm.setRowStretch(len(self.tagValues), 1) self.tagsForm.setHorizontalSpacing(hSpace) self.tagsForm.setVerticalSpacing(vSpace) # Assemble - self.outerWidget = QWidget(self) - self.outerBox = QHBoxLayout() - self.outerBox.addWidget(self.mainGroup, 0) - self.outerBox.addWidget(self.tagsGroup, 1) + self.mainSplit = QSplitter(Qt.Orientation.Horizontal) + self.mainSplit.addWidget(self.mainGroup) + self.mainSplit.addWidget(self.tagsGroup) + self.mainSplit.setCollapsible(0, False) + self.mainSplit.setCollapsible(1, False) - self.outerWidget.setLayout(self.outerBox) - self.setWidget(self.outerWidget) + self.setWidget(self.mainSplit) self.setHorizontalScrollBarPolicy(QtScrollAsNeeded) self.setVerticalScrollBarPolicy(QtScrollAsNeeded) @@ -1024,27 +936,42 @@ class GuiOutlineDetails(QScrollArea): self.updateClasses() return + def loadGuiSettings(self) -> None: + """Run open project tasks.""" + half = self.width() // 2 + pOptions = SHARED.project.options + self.mainSplit.setSizes([ + CONFIG.pxInt(pOptions.getInt("GuiOutlineDetails", "detailsWidth", half)), + CONFIG.pxInt(pOptions.getInt("GuiOutlineDetails", "tagsWidth", half)) + ]) + return + + def saveGuiSettings(self) -> None: + """Run close project tasks.""" + mainSplit = self.mainSplit.sizes() + detailsWidth = CONFIG.rpxInt(mainSplit[0]) + tagsWidth = CONFIG.rpxInt(mainSplit[1]) + + logger.debug("Saving State: GuiOutlineDetails") + pOptions = SHARED.project.options + pOptions.setValue("GuiOutlineDetails", "detailsWidth", detailsWidth) + pOptions.setValue("GuiOutlineDetails", "tagsWidth", tagsWidth) + return + def clearDetails(self) -> None: """Clear all the data labels.""" self.titleLabel.setText(self.tr("Title")) - self.titleValue.setText("") - self.fileValue.setText("") - self.itemValue.setText("") - self.cCValue.setText("") - self.wCValue.setText("") - self.pCValue.setText("") - self.synopValue.setText("") - self.povKeyValue.setText("") - self.focKeyValue.setText("") - self.chrKeyValue.setText("") - self.pltKeyValue.setText("") - self.timKeyValue.setText("") - self.wldKeyValue.setText("") - self.objKeyValue.setText("") - self.entKeyValue.setText("") - self.cstKeyValue.setText("") - self.styKeyValue.setText("") - self.mntKeyValue.setText("") + self.titleValue.clear() + self.fileValue.clear() + self.itemValue.clear() + self.cCValue.clear() + self.wCValue.clear() + self.pCValue.clear() + self.synopValue.clear() + + for _, value in self.tagValues.values(): + value.clear() + self.updateClasses() return @@ -1076,17 +1003,8 @@ class GuiOutlineDetails(QScrollArea): self.synopValue.setText(novIdx.synopsis) - self.povKeyValue.setText(self._formatTags(novRefs, nwKeyWords.POV_KEY)) - self.focKeyValue.setText(self._formatTags(novRefs, nwKeyWords.FOCUS_KEY)) - self.chrKeyValue.setText(self._formatTags(novRefs, nwKeyWords.CHAR_KEY)) - self.pltKeyValue.setText(self._formatTags(novRefs, nwKeyWords.PLOT_KEY)) - self.timKeyValue.setText(self._formatTags(novRefs, nwKeyWords.TIME_KEY)) - self.wldKeyValue.setText(self._formatTags(novRefs, nwKeyWords.WORLD_KEY)) - self.objKeyValue.setText(self._formatTags(novRefs, nwKeyWords.OBJECT_KEY)) - self.entKeyValue.setText(self._formatTags(novRefs, nwKeyWords.ENTITY_KEY)) - self.cstKeyValue.setText(self._formatTags(novRefs, nwKeyWords.CUSTOM_KEY)) - self.styKeyValue.setText(self._formatTags(novRefs, nwKeyWords.STORY_KEY)) - self.mntKeyValue.setText(self._formatTags(novRefs, nwKeyWords.MENTION_KEY)) + for key, (_, value) in self.tagValues.items(): + value.setText(self._formatTags(novRefs, key)) return @@ -1094,27 +1012,12 @@ class GuiOutlineDetails(QScrollArea): def updateClasses(self) -> None: """Update the visibility status of class details.""" usedClasses = SHARED.project.tree.rootClasses() - - pltVisible = nwItemClass.PLOT in usedClasses - timVisible = nwItemClass.TIMELINE in usedClasses - wldVisible = nwItemClass.WORLD in usedClasses - objVisible = nwItemClass.OBJECT in usedClasses - entVisible = nwItemClass.ENTITY in usedClasses - cstVisible = nwItemClass.CUSTOM in usedClasses - - self.pltKeyLabel.setVisible(pltVisible) - self.pltKeyValue.setVisible(pltVisible) - self.timKeyLabel.setVisible(timVisible) - self.timKeyValue.setVisible(timVisible) - self.wldKeyLabel.setVisible(wldVisible) - self.wldKeyValue.setVisible(wldVisible) - self.objKeyLabel.setVisible(objVisible) - self.objKeyValue.setVisible(objVisible) - self.entKeyLabel.setVisible(entVisible) - self.entKeyValue.setVisible(entVisible) - self.cstKeyLabel.setVisible(cstVisible) - self.cstKeyValue.setVisible(cstVisible) - + for key, itemClass in nwKeyWords.KEY_CLASS.items(): + visible = itemClass in usedClasses + if key in self.tagValues: + label, value = self.tagValues[key] + label.setVisible(visible) + value.setVisible(visible) return @staticmethod diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 6bcae09d..843eb687 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -29,7 +29,6 @@ from __future__ import annotations import logging from enum import Enum -from time import time from PyQt5.QtCore import QPoint, Qt, QTimer, pyqtSignal, pyqtSlot from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon, QMouseEvent, QPalette @@ -39,7 +38,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.common import minmax +from novelwriter.common import minmax, qtLambda from novelwriter.constants import nwLabels, nwStyles, nwUnicode, trConst from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter from novelwriter.core.item import NWItem @@ -97,37 +96,37 @@ class GuiProjectView(QWidget): self.keyMoveUp = QShortcut(self.projTree) self.keyMoveUp.setKey("Ctrl+Up") self.keyMoveUp.setContext(Qt.ShortcutContext.WidgetShortcut) - self.keyMoveUp.activated.connect(lambda: self.projTree.moveTreeItem(-1)) + self.keyMoveUp.activated.connect(qtLambda(self.projTree.moveTreeItem, -1)) self.keyMoveDn = QShortcut(self.projTree) self.keyMoveDn.setKey("Ctrl+Down") self.keyMoveDn.setContext(Qt.ShortcutContext.WidgetShortcut) - self.keyMoveDn.activated.connect(lambda: self.projTree.moveTreeItem(1)) + self.keyMoveDn.activated.connect(qtLambda(self.projTree.moveTreeItem, 1)) self.keyGoPrev = QShortcut(self.projTree) self.keyGoPrev.setKey("Alt+Up") self.keyGoPrev.setContext(Qt.ShortcutContext.WidgetShortcut) - self.keyGoPrev.activated.connect(lambda: self.projTree.moveToNextItem(-1)) + self.keyGoPrev.activated.connect(qtLambda(self.projTree.moveToNextItem, -1)) self.keyGoNext = QShortcut(self.projTree) self.keyGoNext.setKey("Alt+Down") self.keyGoNext.setContext(Qt.ShortcutContext.WidgetShortcut) - self.keyGoNext.activated.connect(lambda: self.projTree.moveToNextItem(1)) + self.keyGoNext.activated.connect(qtLambda(self.projTree.moveToNextItem, 1)) self.keyGoUp = QShortcut(self.projTree) self.keyGoUp.setKey("Alt+Left") self.keyGoUp.setContext(Qt.ShortcutContext.WidgetShortcut) - self.keyGoUp.activated.connect(lambda: self.projTree.moveToLevel(-1)) + self.keyGoUp.activated.connect(qtLambda(self.projTree.moveToLevel, -1)) self.keyGoDown = QShortcut(self.projTree) self.keyGoDown.setKey("Alt+Right") self.keyGoDown.setContext(Qt.ShortcutContext.WidgetShortcut) - self.keyGoDown.activated.connect(lambda: self.projTree.moveToLevel(1)) + self.keyGoDown.activated.connect(qtLambda(self.projTree.moveToLevel, 1)) self.keyContext = QShortcut(self.projTree) self.keyContext.setKey("Ctrl+.") self.keyContext.setContext(Qt.ShortcutContext.WidgetShortcut) - self.keyContext.activated.connect(lambda: self.projTree.openContextOnSelected()) + self.keyContext.activated.connect(self.projTree.openContextMenu) # Signals self.selectedItemChanged.connect(self.projBar.treeSelectionChanged) @@ -138,7 +137,6 @@ class GuiProjectView(QWidget): self.emptyTrash = self.projTree.emptyTrash self.requestDeleteItem = self.projTree.requestDeleteItem self.getSelectedHandle = self.projTree.getSelectedHandle - self.changedSince = self.projTree.changedSince return @@ -292,38 +290,38 @@ class GuiProjectToolBar(QWidget): # Move Buttons self.tbMoveU = NIconToolButton(self, iSz) self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up")) - self.tbMoveU.clicked.connect(lambda: self.projTree.moveTreeItem(-1)) + self.tbMoveU.clicked.connect(qtLambda(self.projTree.moveTreeItem, -1)) self.tbMoveD = NIconToolButton(self, iSz) self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down")) - self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1)) + self.tbMoveD.clicked.connect(qtLambda(self.projTree.moveTreeItem, 1)) # Add Item Menu self.mAdd = QMenu(self) self.aAddEmpty = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["document"])) self.aAddEmpty.triggered.connect( - lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) + qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=0, isNote=False) ) self.aAddChap = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h2"])) self.aAddChap.triggered.connect( - lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) + qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=2, isNote=False) ) self.aAddScene = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h3"])) self.aAddScene.triggered.connect( - lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) + qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=3, isNote=False) ) self.aAddNote = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["note"])) self.aAddNote.triggered.connect( - lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) + qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=1, isNote=True) ) self.aAddFolder = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["folder"])) self.aAddFolder.triggered.connect( - lambda: self.projTree.newTreeItem(nwItemType.FOLDER) + qtLambda(self.projTree.newTreeItem, nwItemType.FOLDER) ) self.mTemplates = _UpdatableMenu(self.mAdd) @@ -343,13 +341,17 @@ class GuiProjectToolBar(QWidget): self.mMore = QMenu(self) self.aExpand = self.mMore.addAction(self.tr("Expand All")) - self.aExpand.triggered.connect(lambda: self.projTree.setExpandedFromHandle(None, True)) + self.aExpand.triggered.connect( + qtLambda(self.projTree.setExpandedFromHandle, None, True) + ) self.aCollapse = self.mMore.addAction(self.tr("Collapse All")) - self.aCollapse.triggered.connect(lambda: self.projTree.setExpandedFromHandle(None, False)) + self.aCollapse.triggered.connect( + qtLambda(self.projTree.setExpandedFromHandle, None, False) + ) self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash")) - self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash()) + self.aEmptyTrash.triggered.connect(qtLambda(self.projTree.emptyTrash)) self.tbMore = NIconToolButton(self, iSz) self.tbMore.setToolTip(self.tr("More Options")) @@ -422,7 +424,7 @@ class GuiProjectToolBar(QWidget): action.setData(tHandle) action.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass])) action.triggered.connect( - lambda _, tHandle=tHandle: self.projView.setSelectedHandle(tHandle, doScroll=True) + qtLambda(self.projView.setSelectedHandle, tHandle, doScroll=True) ) return @@ -461,7 +463,9 @@ class GuiProjectToolBar(QWidget): def addClass(itemClass: nwItemClass) -> None: aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass])) aNew.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass])) - aNew.triggered.connect(lambda: self.projTree.newTreeItem(nwItemType.ROOT, itemClass)) + aNew.triggered.connect( + qtLambda(self.projTree.newTreeItem, nwItemType.ROOT, itemClass) + ) self.mAddRoot.addAction(aNew) return @@ -504,7 +508,6 @@ class GuiProjectTree(QTreeWidget): # Internal Variables self._treeMap: dict[str, QTreeWidgetItem] = {} - self._timeChanged = 0.0 self._popAlert = None self._actHandle = None @@ -518,7 +521,7 @@ class GuiProjectTree(QTreeWidget): # Context Menu self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self.customContextMenuRequested.connect(self._openContextMenu) + self.customContextMenuRequested.connect(self.openContextMenu) # Tree Settings iPx = SHARED.theme.baseIconHeight @@ -602,7 +605,6 @@ class GuiProjectTree(QTreeWidget): """Clear the GUI content and the related map.""" self.clear() self._treeMap = {} - self._timeChanged = 0.0 return def createNewNote(self, tag: str, itemClass: nwItemClass) -> None: @@ -1121,12 +1123,10 @@ class GuiProjectTree(QTreeWidget): """ logger.debug("Building the project tree ...") self.clearTree() - count = 0 for nwItem in SHARED.project.iterProjectItems(): - count += 1 self._addTreeItem(nwItem) - if count > 0: - logger.info("%d item(s) added to the project tree", count) + self.setActiveHandle(self._actHandle) + logger.info("%d item(s) added to the project tree", len(self._treeMap)) return def getSelectedHandle(self) -> str | None: @@ -1137,19 +1137,13 @@ class GuiProjectTree(QTreeWidget): return items[0].data(self.C_DATA, self.D_HANDLE) return None - def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> bool: + def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> None: """Set a specific handle as the selected item.""" - tItem = self._getTreeItem(tHandle) - if tItem is None: - return False - if tHandle in self._treeMap: self.setCurrentItem(self._treeMap[tHandle]) - - if (indexes := self.selectedIndexes()) and doScroll: - self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter) - - return True + if (indexes := self.selectedIndexes()) and doScroll: + self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter) + return def setActiveHandle(self, tHandle: str | None) -> None: """Highlight the rows associated with a given handle.""" @@ -1169,19 +1163,52 @@ class GuiProjectTree(QTreeWidget): status for all child items. If tHandle is None, it affects the entire tree. """ - trItem = self._getTreeItem(tHandle) or self.invisibleRootItem() - self._recursiveSetExpanded(trItem, isExpanded) + item = self._getTreeItem(tHandle) or self.invisibleRootItem() + self._recursiveSetExpanded(item, isExpanded) return - def openContextOnSelected(self) -> bool: - """Open the context menu on the current selected item.""" - if items := self.selectedItems(): - return self._openContextMenu(self.visualItemRect(items[0]).center()) - return False + ## + # Public Slots + ## - def changedSince(self, checkTime: float) -> bool: - """Check if the tree has changed since a given time.""" - return self._timeChanged > checkTime + @pyqtSlot() + @pyqtSlot("QPoint") + def openContextMenu(self, clickPos: QPoint | None = None) -> None: + """The user right clicked an element in the project tree, so we + open a context menu in-place. + """ + if clickPos is None and (items := self.selectedItems()): + clickPos = self.visualItemRect(items[0]).center() + + if clickPos is not None: + tItem = None + tHandle = None + hasChild = False + sItem = self.itemAt(clickPos) + sItems = self.selectedItems() + if isinstance(sItem, QTreeWidgetItem): + tHandle = sItem.data(self.C_DATA, self.D_HANDLE) + tItem = SHARED.project.tree[tHandle] + hasChild = sItem.childCount() > 0 + + if tItem is None or tHandle is None: + logger.debug("No item found") + return + + ctxMenu = _TreeContextMenu(self, tItem) + trashHandle = SHARED.project.tree.trashRoot + if trashHandle and tHandle == trashHandle: + ctxMenu.buildTrashMenu() + elif len(sItems) > 1: + handles = [str(x.data(self.C_DATA, self.D_HANDLE)) for x in sItems] + ctxMenu.buildMultiSelectMenu(handles) + else: + ctxMenu.buildSingleSelectMenu(hasChild) + + ctxMenu.exec(self.viewport().mapToGlobal(clickPos)) + ctxMenu.deleteLater() + + return ## # Private Slots @@ -1224,40 +1251,6 @@ class GuiProjectTree(QTreeWidget): return - @pyqtSlot("QPoint") - def _openContextMenu(self, clickPos: QPoint) -> bool: - """The user right clicked an element in the project tree, so we - open a context menu in-place. - """ - tItem = None - tHandle = None - hasChild = False - sItem = self.itemAt(clickPos) - sItems = self.selectedItems() - if isinstance(sItem, QTreeWidgetItem): - tHandle = sItem.data(self.C_DATA, self.D_HANDLE) - tItem = SHARED.project.tree[tHandle] - hasChild = sItem.childCount() > 0 - - if tItem is None or tHandle is None: - logger.debug("No item found") - return False - - ctxMenu = _TreeContextMenu(self, tItem) - trashHandle = SHARED.project.tree.trashRoot - if trashHandle and tHandle == trashHandle: - ctxMenu.buildTrashMenu() - elif len(sItems) > 1: - handles = [str(x.data(self.C_DATA, self.D_HANDLE)) for x in sItems] - ctxMenu.buildMultiSelectMenu(handles) - else: - ctxMenu.buildSingleSelectMenu(hasChild) - - ctxMenu.exec(self.viewport().mapToGlobal(clickPos)) - ctxMenu.deleteLater() - - return True - @pyqtSlot() def _doAutoScroll(self) -> None: """Scroll one item up or down based on direction value.""" @@ -1397,11 +1390,6 @@ class GuiProjectTree(QTreeWidget): return - def _getItemWordCount(self, tHandle: str) -> int: - """Return the word count of a given item handle.""" - tItem = self._getTreeItem(tHandle) - return int(tItem.data(self.C_DATA, self.D_WORDS)) if tItem else 0 - def _getTreeItem(self, tHandle: str | None) -> QTreeWidgetItem | None: """Return the QTreeWidgetItem of a given item handle.""" return self._treeMap.get(tHandle, None) if tHandle else None @@ -1658,7 +1646,6 @@ class GuiProjectTree(QTreeWidget): signals. A flush is only needed if an item is moved, created or deleted. """ - self._timeChanged = time() SHARED.project.setProjectChanged(True) if flush: self.saveTreeOrder() @@ -1785,7 +1772,7 @@ class _TreeContextMenu(QMenu): # Edit Item Settings action = self.addAction(self.tr("Rename")) - action.triggered.connect(lambda: self.projTree.renameTreeItem(self._handle)) + action.triggered.connect(qtLambda(self.projTree.renameTreeItem, self._handle)) if isFile: self._itemHeader() self._itemActive(False) @@ -1821,13 +1808,15 @@ class _TreeContextMenu(QMenu): def _docActions(self) -> None: """Add document actions.""" action = self.addAction(self.tr("Open Document")) - action.triggered.connect( - lambda: self.projView.openDocumentRequest.emit(self._handle, nwDocMode.EDIT, "", True) - ) + action.triggered.connect(qtLambda( + self.projView.openDocumentRequest.emit, + self._handle, nwDocMode.EDIT, "", True + )) action = self.addAction(self.tr("View Document")) - action.triggered.connect( - lambda: self.projView.openDocumentRequest.emit(self._handle, nwDocMode.VIEW, "", False) - ) + action.triggered.connect(qtLambda( + self.projView.openDocumentRequest.emit, + self._handle, nwDocMode.VIEW, "", False + )) return def _itemCreation(self) -> None: @@ -1846,7 +1835,7 @@ class _TreeContextMenu(QMenu): if hItem := SHARED.project.index.getItemHeading(self._handle, "T0001"): action = self.addAction(self.tr("Rename to Heading")) action.triggered.connect( - lambda: self.projTree.renameTreeItem(self._handle, hItem.title) + qtLambda(self.projTree.renameTreeItem, self._handle, hItem.title) ) return @@ -1855,9 +1844,9 @@ class _TreeContextMenu(QMenu): if multi: mSub = self.addMenu(self.tr("Set Active to ...")) aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.projTree.trActive) - aOne.triggered.connect(lambda: self._iterItemActive(True)) + aOne.triggered.connect(qtLambda(self._iterItemActive, True)) aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.projTree.trInactive) - aTwo.triggered.connect(lambda: self._iterItemActive(False)) + aTwo.triggered.connect(qtLambda(self._iterItemActive, False)) else: action = self.addAction(self.tr("Toggle Active")) action.triggered.connect(self._toggleItemActive) @@ -1868,37 +1857,39 @@ class _TreeContextMenu(QMenu): if self._item.isNovelLike(): menu = self.addMenu(self.tr("Set Status to ...")) current = self._item.itemStatus - for n, (key, entry) in enumerate(SHARED.project.data.itemStatus.iterItems()): + for key, entry in SHARED.project.data.itemStatus.iterItems(): name = entry.name if not multi and current == key: name += f" ({nwUnicode.U_CHECK})" action = menu.addAction(entry.icon, name) if multi: - action.triggered.connect(lambda n, key=key: self._iterSetItemStatus(key)) + action.triggered.connect(qtLambda(self._iterSetItemStatus, key)) else: - action.triggered.connect(lambda n, key=key: self._changeItemStatus(key)) + action.triggered.connect(qtLambda(self._changeItemStatus, key)) menu.addSeparator() action = menu.addAction(self.tr("Manage Labels ...")) - action.triggered.connect( - lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_STATUS) - ) + action.triggered.connect(qtLambda( + self.projView.projectSettingsRequest.emit, + GuiProjectSettings.PAGE_STATUS + )) else: menu = self.addMenu(self.tr("Set Importance to ...")) current = self._item.itemImport - for n, (key, entry) in enumerate(SHARED.project.data.itemImport.iterItems()): + for key, entry in SHARED.project.data.itemImport.iterItems(): name = entry.name if not multi and current == key: name += f" ({nwUnicode.U_CHECK})" action = menu.addAction(entry.icon, name) if multi: - action.triggered.connect(lambda n, key=key: self._iterSetItemImport(key)) + action.triggered.connect(qtLambda(self._iterSetItemImport, key)) else: - action.triggered.connect(lambda n, key=key: self._changeItemImport(key)) + action.triggered.connect(qtLambda(self._changeItemImport, key)) menu.addSeparator() action = menu.addAction(self.tr("Manage Labels ...")) - action.triggered.connect( - lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_IMPORT) - ) + action.triggered.connect(qtLambda( + self.projView.projectSettingsRequest.emit, + GuiProjectSettings.PAGE_IMPORT + )) return def _itemTransform(self, isFile: bool, isFolder: bool, hasChild: bool) -> None: @@ -1917,33 +1908,33 @@ class _TreeContextMenu(QMenu): if isNoteFile and self._item.documentAllowed(): action = menu.addAction(self.tr("Convert to {0}").format(trDoc)) - action.triggered.connect(lambda: self._changeItemLayout(loDoc)) + action.triggered.connect(qtLambda(self._changeItemLayout, loDoc)) if isDocFile: action = menu.addAction(self.tr("Convert to {0}").format(trNote)) - action.triggered.connect(lambda: self._changeItemLayout(loNote)) + action.triggered.connect(qtLambda(self._changeItemLayout, loNote)) if isFolder and self._item.documentAllowed(): action = menu.addAction(self.tr("Convert to {0}").format(trDoc)) - action.triggered.connect(lambda: self._covertFolderToFile(loDoc)) + action.triggered.connect(qtLambda(self._covertFolderToFile, loDoc)) if isFolder: action = menu.addAction(self.tr("Convert to {0}").format(trNote)) - action.triggered.connect(lambda: self._covertFolderToFile(loNote)) + action.triggered.connect(qtLambda(self._covertFolderToFile, loNote)) if hasChild and isFile: action = menu.addAction(self.tr("Merge Child Items into Self")) - action.triggered.connect(lambda: tree._mergeDocuments(tHandle, False)) + action.triggered.connect(qtLambda(tree._mergeDocuments, tHandle, False)) action = menu.addAction(self.tr("Merge Child Items into New")) - action.triggered.connect(lambda: tree._mergeDocuments(tHandle, True)) + action.triggered.connect(qtLambda(tree._mergeDocuments, tHandle, True)) if hasChild and isFolder: action = menu.addAction(self.tr("Merge Documents in Folder")) - action.triggered.connect(lambda: tree._mergeDocuments(tHandle, True)) + action.triggered.connect(qtLambda(tree._mergeDocuments, tHandle, True)) if isFile: action = menu.addAction(self.tr("Split Document by Headings")) - action.triggered.connect(lambda: tree._splitDocument(tHandle)) + action.triggered.connect(qtLambda(tree._splitDocument, tHandle)) return @@ -1953,19 +1944,19 @@ class _TreeContextMenu(QMenu): tHandle = self._handle if hasChild: action = self.addAction(self.tr("Expand All")) - action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, True)) + action.triggered.connect(qtLambda(tree.setExpandedFromHandle, tHandle, True)) action = self.addAction(self.tr("Collapse All")) - action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, False)) + action.triggered.connect(qtLambda(tree.setExpandedFromHandle, tHandle, False)) action = self.addAction(self.tr("Duplicate")) - action.triggered.connect(lambda: tree._duplicateFromHandle(tHandle)) + action.triggered.connect(qtLambda(tree._duplicateFromHandle, tHandle)) if self._item.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild): action = self.addAction(self.tr("Delete Permanently")) - action.triggered.connect(lambda: tree.permDeleteItem(tHandle)) + action.triggered.connect(qtLambda(tree.permDeleteItem, tHandle)) else: action = self.addAction(self.tr("Move to Trash")) - action.triggered.connect(lambda: tree.moveItemToTrash(tHandle)) + action.triggered.connect(qtLambda(tree.moveItemToTrash, tHandle)) return diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index 6f4669c7..7fccd311 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -32,6 +32,7 @@ from PyQt5.QtGui import QPalette from PyQt5.QtWidgets import QMenu, QVBoxLayout, QWidget from novelwriter import CONFIG, SHARED +from novelwriter.common import qtLambda from novelwriter.enum import nwView from novelwriter.extensions.eventfilters import StatusTipFilter from novelwriter.extensions.modified import NIconToolButton @@ -54,7 +55,7 @@ class GuiSideBar(QWidget): self.mainGui = mainGui - iPx = int(1.2*SHARED.theme.baseButtonHeight) + iPx = int(1.25*SHARED.theme.baseButtonHeight) iSz = QSize(iPx, iPx) self.setContentsMargins(0, 0, 0, 0) @@ -63,19 +64,19 @@ class GuiSideBar(QWidget): # Buttons self.tbProject = NIconToolButton(self, iSz) self.tbProject.setToolTip("{0} [Ctrl+T]".format(self.tr("Project Tree View"))) - self.tbProject.clicked.connect(lambda: self.requestViewChange.emit(nwView.PROJECT)) + self.tbProject.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.PROJECT)) self.tbNovel = NIconToolButton(self, iSz) self.tbNovel.setToolTip("{0} [Ctrl+T]".format(self.tr("Novel Tree View"))) - self.tbNovel.clicked.connect(lambda: self.requestViewChange.emit(nwView.NOVEL)) + self.tbNovel.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.NOVEL)) self.tbSearch = NIconToolButton(self, iSz) self.tbSearch.setToolTip("{0} [Ctrl+Shift+F]".format(self.tr("Project Search"))) - self.tbSearch.clicked.connect(lambda: self.requestViewChange.emit(nwView.SEARCH)) + self.tbSearch.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.SEARCH)) self.tbOutline = NIconToolButton(self, iSz) self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View"))) - self.tbOutline.clicked.connect(lambda: self.requestViewChange.emit(nwView.OUTLINE)) + self.tbOutline.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.OUTLINE)) self.tbBuild = NIconToolButton(self, iSz) self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript"))) @@ -113,7 +114,7 @@ class GuiSideBar(QWidget): self.outerBox.addWidget(self.tbStats) self.outerBox.addWidget(self.tbSettings) self.outerBox.setContentsMargins(0, 0, 0, 0) - self.outerBox.setSpacing(CONFIG.pxInt(4)) + self.outerBox.setSpacing(CONFIG.pxInt(6)) self.setLayout(self.outerBox) self.updateTheme() diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 350e48a4..098b1ce7 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -37,7 +37,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.common import describeFont +from novelwriter.common import describeFont, qtLambda from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwStyles, trConst from novelwriter.core.buildsettings import BuildSettings, FilterMode from novelwriter.extensions.configlayout import ( @@ -329,16 +329,16 @@ class _FilterTab(NFixedPage): self.includedButton = NIconToolButton(self, iSz) self.includedButton.setToolTip(self.tr("Always included")) self.includedButton.setIcon(self._statusFlags[self.F_INCLUDED]) - self.includedButton.clicked.connect(lambda: self._setSelectedMode(self.F_INCLUDED)) + self.includedButton.clicked.connect(qtLambda(self._setSelectedMode, self.F_INCLUDED)) self.excludedButton = NIconToolButton(self, iSz) self.excludedButton.setToolTip(self.tr("Always excluded")) self.excludedButton.setIcon(self._statusFlags[self.F_EXCLUDED]) - self.excludedButton.clicked.connect(lambda: self._setSelectedMode(self.F_EXCLUDED)) + self.excludedButton.clicked.connect(qtLambda(self._setSelectedMode, self.F_EXCLUDED)) self.resetButton = NIconToolButton(self, iSz, "revert") self.resetButton.setToolTip(self.tr("Reset to default")) - self.resetButton.clicked.connect(lambda: self._setSelectedMode(self.F_FILTERED)) + self.resetButton.clicked.connect(qtLambda(self._setSelectedMode, self.F_FILTERED)) self.modeBox = QHBoxLayout() self.modeBox.addWidget(QLabel(self.tr("Mark selection as"), self)) @@ -589,7 +589,7 @@ class _HeadingsTab(NScrollablePage): self.fmtPart = QLineEdit("", self) self.fmtPart.setReadOnly(True) self.btnPart = NIconToolButton(self, iSz, "edit") - self.btnPart.clicked.connect(lambda: self._editHeading(self.EDIT_TITLE)) + self.btnPart.clicked.connect(qtLambda(self._editHeading, self.EDIT_TITLE)) self.hdePart = QLabel(trHide, self) self.hdePart.setIndent(bSp) self.swtPart = NSwitch(self, height=iPx) @@ -605,7 +605,7 @@ class _HeadingsTab(NScrollablePage): self.fmtChapter = QLineEdit("", self) self.fmtChapter.setReadOnly(True) self.btnChapter = NIconToolButton(self, iSz, "edit") - self.btnChapter.clicked.connect(lambda: self._editHeading(self.EDIT_CHAPTER)) + self.btnChapter.clicked.connect(qtLambda(self._editHeading, self.EDIT_CHAPTER)) self.hdeChapter = QLabel(trHide, self) self.hdeChapter.setIndent(bSp) self.swtChapter = NSwitch(self, height=iPx) @@ -621,7 +621,7 @@ class _HeadingsTab(NScrollablePage): self.fmtUnnumbered = QLineEdit("", self) self.fmtUnnumbered.setReadOnly(True) self.btnUnnumbered = NIconToolButton(self, iSz, "edit") - self.btnUnnumbered.clicked.connect(lambda: self._editHeading(self.EDIT_UNNUM)) + self.btnUnnumbered.clicked.connect(qtLambda(self._editHeading, self.EDIT_UNNUM)) self.hdeUnnumbered = QLabel(trHide, self) self.hdeUnnumbered.setIndent(bSp) self.swtUnnumbered = NSwitch(self, height=iPx) @@ -637,7 +637,7 @@ class _HeadingsTab(NScrollablePage): self.fmtScene = QLineEdit("", self) self.fmtScene.setReadOnly(True) self.btnScene = NIconToolButton(self, iSz, "edit") - self.btnScene.clicked.connect(lambda: self._editHeading(self.EDIT_SCENE)) + self.btnScene.clicked.connect(qtLambda(self._editHeading, self.EDIT_SCENE)) self.hdeScene = QLabel(trHide, self) self.hdeScene.setIndent(bSp) self.swtScene = NSwitch(self, height=iPx) @@ -653,7 +653,7 @@ class _HeadingsTab(NScrollablePage): self.fmtAScene = QLineEdit("", self) self.fmtAScene.setReadOnly(True) self.btnAScene = NIconToolButton(self, iSz, "edit") - self.btnAScene.clicked.connect(lambda: self._editHeading(self.EDIT_HSCENE)) + self.btnAScene.clicked.connect(qtLambda(self._editHeading, self.EDIT_HSCENE)) self.hdeAScene = QLabel(trHide, self) self.hdeAScene.setIndent(bSp) self.swtAScene = NSwitch(self, height=iPx) @@ -669,7 +669,7 @@ class _HeadingsTab(NScrollablePage): self.fmtSection = QLineEdit("", self) self.fmtSection.setReadOnly(True) self.btnSection = NIconToolButton(self, iSz, "edit") - self.btnSection.clicked.connect(lambda: self._editHeading(self.EDIT_SECTION)) + self.btnSection.clicked.connect(qtLambda(self._editHeading, self.EDIT_SECTION)) self.hdeSection = QLabel(trHide, self) self.hdeSection.setIndent(bSp) self.swtSection = NSwitch(self, height=iPx) @@ -702,15 +702,15 @@ class _HeadingsTab(NScrollablePage): self.aInsCharPOV = self.menuInsert.addAction(self.tr("Point of View Character")) self.aInsCharFocus = self.menuInsert.addAction(self.tr("Focus Character")) - self.aInsTitle.triggered.connect(lambda: self._insertIntoForm(nwHeadFmt.TITLE)) - self.aInsChNum.triggered.connect(lambda: self._insertIntoForm(nwHeadFmt.CH_NUM)) - self.aInsChWord.triggered.connect(lambda: self._insertIntoForm(nwHeadFmt.CH_WORD)) - self.aInsChRomU.triggered.connect(lambda: self._insertIntoForm(nwHeadFmt.CH_ROMU)) - self.aInsChRomL.triggered.connect(lambda: self._insertIntoForm(nwHeadFmt.CH_ROML)) - self.aInsScNum.triggered.connect(lambda: self._insertIntoForm(nwHeadFmt.SC_NUM)) - self.aInsScAbs.triggered.connect(lambda: self._insertIntoForm(nwHeadFmt.SC_ABS)) - self.aInsCharPOV.triggered.connect(lambda: self._insertIntoForm(nwHeadFmt.CHAR_POV)) - self.aInsCharFocus.triggered.connect(lambda: self._insertIntoForm(nwHeadFmt.CHAR_FOCUS)) + self.aInsTitle.triggered.connect(qtLambda(self._insertIntoForm, nwHeadFmt.TITLE)) + self.aInsChNum.triggered.connect(qtLambda(self._insertIntoForm, nwHeadFmt.CH_NUM)) + self.aInsChWord.triggered.connect(qtLambda(self._insertIntoForm, nwHeadFmt.CH_WORD)) + self.aInsChRomU.triggered.connect(qtLambda(self._insertIntoForm, nwHeadFmt.CH_ROMU)) + self.aInsChRomL.triggered.connect(qtLambda(self._insertIntoForm, nwHeadFmt.CH_ROML)) + self.aInsScNum.triggered.connect(qtLambda(self._insertIntoForm, nwHeadFmt.SC_NUM)) + self.aInsScAbs.triggered.connect(qtLambda(self._insertIntoForm, nwHeadFmt.SC_ABS)) + self.aInsCharPOV.triggered.connect(qtLambda(self._insertIntoForm, nwHeadFmt.CHAR_POV)) + self.aInsCharFocus.triggered.connect(qtLambda(self._insertIntoForm, nwHeadFmt.CHAR_FOCUS)) self.btnInsert = QPushButton(self.tr("Insert"), self) self.btnInsert.setMenu(self.menuInsert) diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index 864ad177..0ba1d52f 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -38,7 +38,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED -from novelwriter.common import checkInt, checkIntTuple, formatTime, minmax +from novelwriter.common import checkInt, checkIntTuple, formatTime, minmax, qtLambda from novelwriter.constants import nwConst from novelwriter.error import formatException from novelwriter.extensions.modified import NToolDialog @@ -277,11 +277,11 @@ class GuiWritingStats(NToolDialog): self.btnSave.setMenu(self.saveMenu) self.saveJSON = QAction(self.tr("JSON Data File (.json)"), self) - self.saveJSON.triggered.connect(lambda: self._saveData(self.FMT_JSON)) + self.saveJSON.triggered.connect(qtLambda(self._saveData, self.FMT_JSON)) self.saveMenu.addAction(self.saveJSON) self.saveCSV = QAction(self.tr("CSV Data File (.csv)"), self) - self.saveCSV.triggered.connect(lambda: self._saveData(self.FMT_CSV)) + self.saveCSV.triggered.connect(qtLambda(self._saveData, self.FMT_CSV)) self.saveMenu.addAction(self.saveCSV) # Assemble diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 14082922..30b0f89e 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -134,7 +134,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): assert docViewer.docAction(nwDocAction.COPY) is False # Open again via menu - assert nwGUI.projView.projTree.setSelectedHandle("88243afbe5ed8") + nwGUI.projView.projTree.setSelectedHandle("88243afbe5ed8") nwGUI.mainMenu.aViewDoc.activate(QAction.ActionEvent.Trigger) # Open context menu diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index 3dc6b32b..dabd9cf0 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -29,6 +29,7 @@ import pytest from PyQt5.QtWidgets import QAction, QFileDialog, QWidget from novelwriter import CONFIG, SHARED +from novelwriter.constants import nwKeyWords from novelwriter.enum import nwItemClass, nwOutline, nwView from novelwriter.types import QtScrollAlwaysOff, QtScrollAsNeeded @@ -261,7 +262,7 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, prjLipsum, fncPath, tstPat assert outlineData.itemValue.text() == "Finished" # Click POV Link - assert outlineData.povKeyValue.text() == "Bod" + assert outlineData.tagValues[nwKeyWords.POV_KEY][1].text() == "Bod" outlineView._tagClicked("Bod") assert nwGUI.docViewer.docHandle == "4c4f28287af27" diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 7f663698..af14b26c 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -21,6 +21,7 @@ along with this program. If not, see . from __future__ import annotations from pathlib import Path +from unittest.mock import MagicMock import pytest @@ -107,7 +108,6 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn assert project.tree["0000000000013"].itemClass == nwItemClass.NOVEL # type: ignore assert nwGUI.openDocument("0000000000013") assert nwGUI.docEditor.getText() == "## New Chapter\n\n" - assert projTree._getItemWordCount("0000000000013") == 2 # Add a new scene next to the other new file projView.setSelectedHandle("0000000000012") @@ -117,7 +117,6 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn assert project.tree["0000000000014"].itemClass == nwItemClass.NOVEL # type: ignore assert nwGUI.openDocument("0000000000014") assert nwGUI.docEditor.getText() == "### New Scene\n\n" - assert projTree._getItemWordCount("0000000000014") == 2 # Add a new scene with the content copied from the previous assert nwGUI.openDocument("0000000000014") @@ -130,7 +129,6 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn assert project.tree["0000000000015"].itemClass == nwItemClass.NOVEL # type: ignore assert nwGUI.openDocument("0000000000015") assert nwGUI.docEditor.getText() == "### New Scene\n\nWith Stuff\n\n" - assert projTree._getItemWordCount("0000000000015") == 4 # Add a new file to the characters folder projView.setSelectedHandle(C.hCharRoot) @@ -140,7 +138,6 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn assert project.tree["0000000000016"].itemClass == nwItemClass.CHARACTER # type: ignore assert nwGUI.openDocument("0000000000016") assert nwGUI.docEditor.getText() == "# New Note\n\n" - assert projTree._getItemWordCount("0000000000016") == 2 # Make sure the sibling folder bug trap works projView.setSelectedHandle("0000000000013") @@ -1146,30 +1143,37 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): def itemPos(tHandle): return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center() - # Pop the menu + # Pop the menu in various positions and check for success with monkeypatch.context() as mp: - mp.setattr(QMenu, "exec", lambda *a: None) + mockMenu = MagicMock() + mp.setattr(QMenu, "exec", mockMenu) projTree.clearSelection() # No item under menu - assert projTree._openContextMenu(projTree.viewport().rect().bottomRight()) is False + projTree.openContextMenu(projTree.viewport().rect().bottomRight()) + assert mockMenu.call_count == 0 # Open Trash Menu - assert projTree._openContextMenu(itemPos(hTrashRoot)) is True + projTree.openContextMenu(itemPos(hTrashRoot)) + assert mockMenu.call_count == 1 # Open Single Select Menu - assert projTree._openContextMenu(itemPos(C.hNovelRoot)) is True + projTree.openContextMenu(itemPos(C.hNovelRoot)) + assert mockMenu.call_count == 2 # Open Multi-Select Menu projTree._getTreeItem(hNovelNote).setSelected(True) projTree._getTreeItem(hSubNote).setSelected(True) - assert projTree._openContextMenu(itemPos(hCharNote)) is True + projTree.openContextMenu(itemPos(hCharNote)) + assert mockMenu.call_count == 3 # Check the keyboard shortcut handler as well projTree.setSelectedHandle(C.hNovelRoot) - assert projTree.openContextOnSelected() is True + projTree.openContextMenu(None) + assert mockMenu.call_count == 4 projTree.clearSelection() - assert projTree.openContextOnSelected() is False + projTree.openContextMenu(None) + assert mockMenu.call_count == 4 # Menu Builders # =============