From ce666d9d7aa44176724d7948121ad08cad46d541 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 29 Nov 2023 20:55:05 +0100 Subject: [PATCH 1/4] Scroll novel view when looping through documents (#1555) --- novelwriter/gui/noveltree.py | 14 ++++++-------- novelwriter/guimain.py | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index d59a668a3..8da01e7a6 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -567,27 +567,25 @@ def setLastColSize(self, colSize: int) -> None: self._lastColSize = minmax(colSize, 15, 75)/100.0 return - def setActiveHandle(self, tHandle: str | None) -> None: + def setActiveHandle(self, tHandle: str | None, doScroll: bool = False) -> None: """Highlight the rows associated with a given handle.""" - tStart = time() - + didScroll = False self._actHandle = tHandle for i in range(self.topLevelItemCount()): - tItem = self.topLevelItem(i) - if tItem is not None: + if tItem := self.topLevelItem(i): if tItem.data(self.C_DATA, self.D_HANDLE) == tHandle: tItem.setBackground(self.C_TITLE, self.palette().alternateBase()) tItem.setBackground(self.C_WORDS, self.palette().alternateBase()) tItem.setBackground(self.C_EXTRA, self.palette().alternateBase()) tItem.setBackground(self.C_MORE, self.palette().alternateBase()) + if doScroll and not didScroll: + self.scrollToItem(tItem, QAbstractItemView.ScrollHint.PositionAtCenter) + didScroll = True else: tItem.setBackground(self.C_TITLE, self.palette().base()) tItem.setBackground(self.C_WORDS, self.palette().base()) tItem.setBackground(self.C_EXTRA, self.palette().base()) tItem.setBackground(self.C_MORE, self.palette().base()) - - logger.debug("Highlighted Novel Tree in %.3f ms", (time() - tStart)*1000) - return ## diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 04ebab1a2..17f91f4fe 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -606,7 +606,7 @@ def openDocument(self, tHandle: str | None, tLine: int | None = None, if self.docEditor.loadText(tHandle, tLine): SHARED.project.data.setLastHandle(tHandle, "editor") self.projView.setSelectedHandle(tHandle, doScroll=doScroll) - self.novelView.setActiveHandle(tHandle) + self.novelView.setActiveHandle(tHandle, doScroll=doScroll) if changeFocus: self.docEditor.setFocus() else: From a31f42b04c2732cf9fda3646b884ae9d6d1edf4a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 29 Nov 2023 21:24:07 +0100 Subject: [PATCH 2/4] Clean up novel view and improve coverage --- novelwriter/gui/noveltree.py | 80 +++++++++++++--------------- tests/test_gui/test_gui_noveltree.py | 24 ++++++++- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 8da01e7a6..588e4897b 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -206,18 +206,18 @@ def __init__(self, novelView: GuiNovelView) -> None: # Novel Selector selFont = self.font() - selFont.setWeight(QFont.Bold) + selFont.setWeight(QFont.Weight.Bold) self.novelPrefix = self.tr("Outline of {0}") self.novelValue = NovelSelector(self) self.novelValue.setFont(selFont) self.novelValue.setMinimumWidth(CONFIG.pxInt(150)) - self.novelValue.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.novelValue.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.novelValue.novelSelectionChanged.connect(self.setCurrentRoot) self.tbNovel = QToolButton(self) self.tbNovel.setToolTip(self.tr("Novel Root")) self.tbNovel.setIconSize(QSize(iPx, iPx)) - self.tbNovel.clicked.connect(self._openNovelSelector) + self.tbNovel.clicked.connect(self.novelValue.showPopup) # Refresh Button self.tbRefresh = QToolButton(self) @@ -244,7 +244,7 @@ def __init__(self, novelView: GuiNovelView) -> None: self.tbMore.setToolTip(self.tr("More Options")) self.tbMore.setIconSize(QSize(iPx, iPx)) self.tbMore.setMenu(self.mMore) - self.tbMore.setPopupMode(QToolButton.InstantPopup) + self.tbMore.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) # Assemble self.outerBox = QHBoxLayout() @@ -275,7 +275,7 @@ def updateTheme(self) -> None: self.tbMore.setIcon(SHARED.theme.getIcon("menu")) qPalette = self.palette() - qPalette.setBrush(QPalette.Window, qPalette.base()) + qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base()) self.setPalette(qPalette) # StyleSheets @@ -327,12 +327,6 @@ def setLastColType(self, colType: NovelTreeColumn, doRefresh: bool = True) -> No # Private Slots ## - @pyqtSlot() - def _openNovelSelector(self) -> None: - """Trigger the dropdown list of the novel selector.""" - self.novelValue.showPopup() - return - @pyqtSlot() def _refreshNovelTree(self) -> None: """Rebuild the current tree.""" @@ -408,14 +402,14 @@ def __init__(self, novelView: GuiNovelView) -> None: cMg = CONFIG.pxInt(6) self.setIconSize(QSize(iPx, iPx)) - self.setFrameStyle(QFrame.NoFrame) + self.setFrameStyle(QFrame.Shape.NoFrame) self.setUniformRowHeights(True) self.setAllColumnsShowFocus(True) self.setHeaderHidden(True) self.setIndentation(0) self.setColumnCount(4) - self.setSelectionBehavior(QAbstractItemView.SelectRows) - self.setSelectionMode(QAbstractItemView.SingleSelection) + self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self.setExpandsOnDoubleClick(False) self.setDragEnabled(False) @@ -423,10 +417,10 @@ def __init__(self, novelView: GuiNovelView) -> None: treeHeader = self.header() treeHeader.setStretchLastSection(False) treeHeader.setMinimumSectionSize(iPx + cMg) - treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.Stretch) - treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeToContents) - treeHeader.setSectionResizeMode(self.C_EXTRA, QHeaderView.ResizeToContents) - treeHeader.setSectionResizeMode(self.C_MORE, QHeaderView.ResizeToContents) + treeHeader.setSectionResizeMode(self.C_TITLE, QHeaderView.ResizeMode.Stretch) + treeHeader.setSectionResizeMode(self.C_WORDS, QHeaderView.ResizeMode.ResizeToContents) + treeHeader.setSectionResizeMode(self.C_EXTRA, QHeaderView.ResizeMode.ResizeToContents) + treeHeader.setSectionResizeMode(self.C_MORE, QHeaderView.ResizeMode.ResizeToContents) # Pre-Generate Tree Formatting fH1 = self.font() @@ -455,14 +449,14 @@ def initSettings(self) -> None: """Set or update tree widget settings.""" # Scroll bars if CONFIG.hideVScroll: - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) else: - self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) if CONFIG.hideHScroll: - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) else: - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) return @@ -522,21 +516,16 @@ def refreshTree(self, rootHandle: str | None = None, overRide: bool = False) -> def refreshHandle(self, tHandle: str) -> None: """Refresh the data for a given handle.""" - idxData = SHARED.project.index.getItemData(tHandle) - if idxData is None: - return - - logger.debug("Refreshing meta data for item '%s'", tHandle) - for sTitle, tHeading in idxData.items(): - sKey = f"{tHandle}:{sTitle}" - trItem = self._treeMap.get(sKey, None) - if trItem is None: - logger.debug("Heading '%s' not in novel tree", sKey) - self.refreshTree() - return - - self._updateTreeItemValues(trItem, tHeading, tHandle, sTitle) - + if idxData := SHARED.project.index.getItemData(tHandle): + logger.debug("Refreshing meta data for item '%s'", tHandle) + for sTitle, tHeading in idxData.items(): + sKey = f"{tHandle}:{sTitle}" + if trItem := self._treeMap.get(sKey, None): + self._updateTreeItemValues(trItem, tHeading, tHandle, sTitle) + else: + logger.debug("Heading '%s' not in novel tree", sKey) + self.refreshTree() + return return def getSelectedHandle(self) -> tuple[str | None, str | None]: @@ -599,12 +588,12 @@ def mousePressEvent(self, event: QMouseEvent) -> None: """ super().mousePressEvent(event) - if event.button() == Qt.LeftButton: + if event.button() == Qt.MouseButton.LeftButton: selItem = self.indexAt(event.pos()) if not selItem.isValid(): self.clearSelection() - elif event.button() == Qt.MiddleButton: + elif event.button() == Qt.MouseButton.MiddleButton: selItem = self.itemAt(event.pos()) if not isinstance(selItem, QTreeWidgetItem): return @@ -635,7 +624,10 @@ def resizeEvent(self, event: QResizeEvent) -> None: trItem = self.topLevelItem(i) if isinstance(trItem, QTreeWidgetItem): lastText = trItem.data(self.C_DATA, self.D_EXTRA) - trItem.setText(self.C_EXTRA, fMetric.elidedText(lastText, Qt.ElideRight, eliW)) + trItem.setText( + self.C_EXTRA, + fMetric.elidedText(lastText, Qt.TextElideMode.ElideRight, eliW) + ) return ## @@ -691,7 +683,7 @@ def _populateTree(self, rootHandle: str | None) -> None: newItem.setData(self.C_DATA, self.D_HANDLE, tHandle) newItem.setData(self.C_DATA, self.D_TITLE, sTitle) newItem.setData(self.C_DATA, self.D_KEY, tKey) - newItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) + newItem.setTextAlignment(self.C_WORDS, Qt.AlignmentFlag.AlignRight) self._updateTreeItemValues(newItem, novIdx, tHandle, sTitle) self._treeMap[tKey] = newItem @@ -710,16 +702,16 @@ def _updateTreeItemValues(self, trItem: QTreeWidgetItem, idxItem: IndexHeading, iLevel = nwHeaders.H_LEVEL.get(idxItem.level, 0) hDec = SHARED.theme.getHeaderDecoration(iLevel) - trItem.setData(self.C_TITLE, Qt.DecorationRole, hDec) + trItem.setData(self.C_TITLE, Qt.ItemDataRole.DecorationRole, hDec) trItem.setText(self.C_TITLE, idxItem.title) trItem.setFont(self.C_TITLE, self._hFonts[iLevel]) trItem.setText(self.C_WORDS, f"{idxItem.wordCount:n}") - trItem.setData(self.C_MORE, Qt.DecorationRole, self._pMore) + trItem.setData(self.C_MORE, Qt.ItemDataRole.DecorationRole, self._pMore) # Custom column mW = int(self._lastColSize * self.viewport().width()) lastText, toolTip = self._getLastColumnText(tHandle, sTitle) - elideText = self.fontMetrics().elidedText(lastText, Qt.ElideRight, mW) + elideText = self.fontMetrics().elidedText(lastText, Qt.TextElideMode.ElideRight, mW) trItem.setText(self.C_EXTRA, elideText) trItem.setData(self.C_DATA, self.D_EXTRA, lastText) trItem.setToolTip(self.C_EXTRA, toolTip) diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index 7cd0eb185..f968dbb3b 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -27,12 +27,12 @@ from tools import C, buildTestProject from PyQt5.QtGui import QFocusEvent -from PyQt5.QtCore import Qt, QEvent +from PyQt5.QtCore import QPoint, Qt, QEvent from PyQt5.QtWidgets import QInputDialog, QToolTip from novelwriter import CONFIG, SHARED from novelwriter.enum import nwWidget, nwItemType -from novelwriter.gui.noveltree import NovelTreeColumn +from novelwriter.gui.noveltree import GuiNovelTree, NovelTreeColumn from novelwriter.dialogs.editlabel import GuiEditLabel @@ -192,12 +192,32 @@ def showText(pos, text): mIndex = novelTree.model().index(2, novelTree.C_MORE) with monkeypatch.context() as mp: mp.setattr(QToolTip, "showText", showText) + + ttText = "" novelTree._treeItemClicked(mIndex) assert ttText == ( "
Point of View: Jane
Focus: Jane
Synopsis: This is a scene.
" ) + ttText = "" + novelTree._popMetaBox(QPoint(1, 1), C.hInvalid, "T0001") + assert ttText == "" + + # Set Default Root + # ================ + SHARED.project.data.setLastHandle(C.hInvalid, "novelTree") + novelView.openProjectTasks() + assert novelBar.novelValue.handle == C.hNovelRoot + + # Tree Focus + # ========== + with monkeypatch.context() as mp: + mp.setattr(GuiNovelTree, "hasFocus", lambda *a: False) + assert novelView.treeHasFocus() is False + mp.setattr(GuiNovelTree, "hasFocus", lambda *a: True) + assert novelView.treeHasFocus() is True + # Other Checks # ============ From b203bcdcff113c0723aa18020d05111a2ea72f18 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 29 Nov 2023 21:48:09 +0100 Subject: [PATCH 3/4] Include apostrophes when auto selecting in editor (#1624) --- novelwriter/constants.py | 3 +++ novelwriter/gui/doceditor.py | 13 +++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 4afa77810..461647de4 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -349,6 +349,9 @@ class nwUnicode: # Unicode Constants # ================= + # Lookup + APOS_TYPE = "\u0027\u2019\u02bc" + # Quotation Marks U_QUOT = "\u0022" # Quotation mark U_APOS = "\u0027" # Apostrophe diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 8ea95769a..5a5ed1840 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -2012,20 +2012,25 @@ def _autoSelect(self) -> QTextCursor: cPos = cursor.position() bPos = cursor.block().position() bLen = cursor.block().length() + apos = nwUnicode.APOS_TYPE - # Scan backwards + # Scan backward sPos = cPos for i in range(cPos - bPos): sPos = cPos - i - 1 - if not self._qDocument.characterAt(sPos).isalnum(): + cOne = self._qDocument.characterAt(sPos) + cTwo = self._qDocument.characterAt(sPos - 1) + if not (cOne.isalnum() or cOne in apos and cTwo.isalnum()): sPos += 1 break - # Scan forwards + # Scan forward ePos = cPos for i in range(bPos + bLen - cPos): ePos = cPos + i - if not self._qDocument.characterAt(ePos).isalnum(): + cOne = self._qDocument.characterAt(ePos) + cTwo = self._qDocument.characterAt(ePos + 1) + if not (cOne.isalnum() or cOne in apos and cTwo.isalnum()): break if ePos - sPos <= 0: From f59317c2b5bae50c40d37824e107a08e6190923d Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 29 Nov 2023 21:55:07 +0100 Subject: [PATCH 4/4] Remove alternative apostrophe from auto select logic because it is already a letter --- novelwriter/constants.py | 3 --- novelwriter/gui/doceditor.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 461647de4..4afa77810 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -349,9 +349,6 @@ class nwUnicode: # Unicode Constants # ================= - # Lookup - APOS_TYPE = "\u0027\u2019\u02bc" - # Quotation Marks U_QUOT = "\u0022" # Quotation mark U_APOS = "\u0027" # Apostrophe diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 5a5ed1840..64a0e7a60 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -2012,7 +2012,7 @@ def _autoSelect(self) -> QTextCursor: cPos = cursor.position() bPos = cursor.block().position() bLen = cursor.block().length() - apos = nwUnicode.APOS_TYPE + apos = nwUnicode.U_APOS + nwUnicode.U_RSQUO # Scan backward sPos = cPos