From 6560594c4941d256dd555508b276d2a76541e6d2 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 15 Nov 2022 16:48:46 +0100 Subject: [PATCH 1/3] Fix scrolling when moving to a specific line in editor (#1239) --- novelwriter/gui/doceditor.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 3b40b05a..8eded68e 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -52,7 +52,7 @@ from PyQt5.QtWidgets import ( from novelwriter.core import NWSpellEnchant, countWords from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode, nwItemClass -from novelwriter.common import transferCase +from novelwriter.common import minmax, transferCase from novelwriter.constants import nwConst, nwFiles, nwKeyWords, nwUnicode from novelwriter.gui.dochighlight import GuiDocHighlighter @@ -654,17 +654,25 @@ class GuiDocEditor(QTextEdit): self.docEditedStatusChanged.emit(self._docChanged) return self._docChanged - def setCursorPosition(self, thePosition): + def setCursorPosition(self, position): """Move the cursor to a given position in the document. """ - if not isinstance(thePosition, int): + if not isinstance(position, int): return False nChars = self.document().characterCount() if nChars > 1: theCursor = self.textCursor() - theCursor.setPosition(min(max(thePosition, 0), nChars-1)) + theCursor.setPosition(minmax(position, 0, nChars-1)) self.setTextCursor(theCursor) + + # The editor scrolls so the cursor is on the last line, so we must correct + vPos = self.verticalScrollBar().value() # Current scrollbar position + cPos = self.cursorRect().topLeft().y() # Cursor position to scroll to + dMrg = int(self.document().documentMargin()) # Document margin to subtract + mPos = int(self.viewport().height()*0.1) # Distance from top to adjust for (10%) + self.verticalScrollBar().setValue(max(0, vPos + cPos - dMrg - mPos)) + self.docFooter.updateLineCount() return True @@ -677,18 +685,17 @@ class GuiDocEditor(QTextEdit): self._nwItem.setCursorPos(cursPos) return - def setCursorLine(self, theLine): + def setCursorLine(self, lineNo): """Move the cursor to a given line in the document. """ - if not isinstance(theLine, int): + if not isinstance(lineNo, int): return False - if theLine >= 0: - theBlock = self.document().findBlockByLineNumber(theLine) + if lineNo >= 0: + theBlock = self.document().findBlockByLineNumber(lineNo) if theBlock: self.setCursorPosition(theBlock.position()) - self.docFooter.updateLineCount() - logger.debug("Cursor moved to line %d", theLine) + logger.debug("Cursor moved to line %d", lineNo) return True From a2ea7b40e23f9a28a97f6c9011b7f236c297b8bf Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 15 Nov 2022 16:49:59 +0100 Subject: [PATCH 2/3] Don't reload document if opening the same (#1242) --- novelwriter/gui/doceditor.py | 7 ++++--- novelwriter/gui/noveltree.py | 6 +++--- novelwriter/gui/outline.py | 4 ++-- novelwriter/gui/projtree.py | 10 +++++----- novelwriter/guimain.py | 17 ++++++++++++----- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 8eded68e..b2641ce6 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -417,7 +417,7 @@ class GuiDocEditor(QTextEdit): else: self.setCursorPosition(self._nwItem.cursorPos) elif isinstance(tLine, int): - self.setCursorLine(tLine - 1) + self.setCursorLine(tLine) if self.mainConf.scrollPastEnd > 0: fSize = QFontMetrics(self.font()).lineSpacing() @@ -691,8 +691,9 @@ class GuiDocEditor(QTextEdit): if not isinstance(lineNo, int): return False - if lineNo >= 0: - theBlock = self.document().findBlockByLineNumber(lineNo) + lineIdx = lineNo - 1 # Block index is 0 offset, lineNo is 1 offset + if lineIdx >= 0: + theBlock = self.document().findBlockByLineNumber(lineIdx) if theBlock: self.setCursorPosition(theBlock.position()) logger.debug("Cursor moved to line %d", lineNo) diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 2a022bb0..12ff53ad 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -59,7 +59,7 @@ class GuiNovelView(QWidget): # Signals for user interaction with the novel tree selectedItemChanged = pyqtSignal(str) - openDocumentRequest = pyqtSignal(str, Enum, str) + openDocumentRequest = pyqtSignal(str, Enum, str, bool) def __init__(self, mainGui): super().__init__(parent=mainGui) @@ -594,7 +594,7 @@ class GuiNovelTree(QTreeWidget): if tHandle is None: return - self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle or "") + self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle or "", False) return @@ -637,7 +637,7 @@ class GuiNovelTree(QTreeWidget): document editor. """ tHandle, sTitle = self.getSelectedHandle() - self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "") + self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True) return ## diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index bc2763d7..98d3b36e 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -55,7 +55,7 @@ logger = logging.getLogger(__name__) class GuiOutlineView(QWidget): loadDocumentTagRequest = pyqtSignal(str, Enum) - openDocumentRequest = pyqtSignal(str, Enum, str) + openDocumentRequest = pyqtSignal(str, Enum, str, bool) def __init__(self, mainGui): super().__init__(parent=mainGui) @@ -545,7 +545,7 @@ class GuiOutlineTree(QTreeWidget): tHandle, sTitle = self.getSelectedHandle() if tHandle is None: return - self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "") + self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True) return @pyqtSlot() diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 92bdea27..3b6a38f0 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -60,7 +60,7 @@ class GuiProjectView(QWidget): # Signals for user interaction with the project tree selectedItemChanged = pyqtSignal(str) - openDocumentRequest = pyqtSignal(str, Enum, str) + openDocumentRequest = pyqtSignal(str, Enum, str, bool) # Requests for the main GUI projectSettingsRequest = pyqtSignal(int) @@ -1144,7 +1144,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.isFileType(): - self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "") + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True) else: trItem.setExpanded(not trItem.isExpanded()) @@ -1190,11 +1190,11 @@ class GuiProjectTree(QTreeWidget): if isFile: aOpenDoc = ctxMenu.addAction(self.tr("Open Document")) aOpenDoc.triggered.connect( - lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "") + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True) ) aViewDoc = ctxMenu.addAction(self.tr("View Document")) aViewDoc.triggered.connect( - lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "") + lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False) ) ctxMenu.addSeparator() @@ -1324,7 +1324,7 @@ class GuiProjectTree(QTreeWidget): return if tItem.isFileType(): - self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "") + self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False) return diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 92b7c9cf..3a5d278d 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -596,14 +596,21 @@ class GuiMain(QMainWindow): logger.debug("Requested item '%s' is not a document", tHandle) return False + cHandle = self.docEditor.docHandle() + if cHandle == tHandle: + self.docEditor.setCursorLine(tLine) + if changeFocus: + self.docEditor.setFocus() + return True + self.closeDocument(beforeOpen=True) self._changeView(nwView.EDITOR) if self.docEditor.loadText(tHandle, tLine): - if changeFocus: - self.docEditor.setFocus() self.theProject.data.setLastHandle(tHandle, "editor") self.projView.setSelectedHandle(tHandle, doScroll=doScroll) self.novelView.setActiveHandle(tHandle) + if changeFocus: + self.docEditor.setFocus() else: return False @@ -1476,8 +1483,8 @@ class GuiMain(QMainWindow): self.viewDocument(tHandle=tHandle, sTitle=sTitle) return - @pyqtSlot(str, Enum, str) - def _openDocument(self, tHandle, tMode, sTitle): + @pyqtSlot(str, Enum, str, bool) + def _openDocument(self, tHandle, tMode, sTitle, setFocus): """Handle an open document request from one of the tree views. """ if tHandle is not None: @@ -1486,7 +1493,7 @@ class GuiMain(QMainWindow): hItem = self.theProject.index.getItemHeader(tHandle, sTitle) if hItem is not None: tLine = hItem.line - self.openDocument(tHandle, tLine=tLine, changeFocus=False) + self.openDocument(tHandle, tLine=tLine, changeFocus=setFocus) elif tMode == nwDocMode.VIEW: self.viewDocument(tHandle=tHandle, sTitle=sTitle) return From e0ca2e7fb5cd742fd4dcebffbbbcf74db77fbd2e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Tue, 15 Nov 2022 16:50:41 +0100 Subject: [PATCH 3/3] Update tests and remove unused functions in viewer --- novelwriter/gui/docviewer.py | 39 +++---------------- .../guiEditor_Main_Final_nwProject.nwx | 16 +++----- tests/test_gui/test_gui_doceditor.py | 12 +++--- tests/test_gui/test_gui_docviewer.py | 17 +++----- tests/test_gui/test_gui_guimain.py | 11 ------ 5 files changed, 24 insertions(+), 71 deletions(-) diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index e498a100..a25fad6d 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -323,43 +323,10 @@ class GuiDocViewer(QTextBrowser): return - ## - # Properties - ## - - def docHandle(self): - """Return the handle of the currently open document. Returns - None if no document is open. - """ - return self._docHandle - ## # Setters ## - def setCursorPosition(self, thePosition): - """Move the cursor to a given position in the document. - """ - if not isinstance(thePosition, int): - return False - if thePosition >= 0: - theCursor = self.textCursor() - theCursor.setPosition(thePosition) - self.setTextCursor(theCursor) - return True - - def setCursorLine(self, theLine): - """Move the cursor to a given line in the document. - """ - if not isinstance(theLine, int): - return False - if theLine >= 0: - theBlock = self.document().findBlockByLineNumber(theLine) - if theBlock: - self.setCursorPosition(theBlock.position()) - logger.debug("Cursor moved to line %d", theLine) - return True - def setScrollPosition(self, thePos): """Set the scrollbar position. """ @@ -372,6 +339,12 @@ class GuiDocViewer(QTextBrowser): # Getters ## + def docHandle(self): + """Return the handle of the currently open document. Returns + None if no document is open. + """ + return self._docHandle + def getScrollPosition(self): """Get the scrollbar position. Returns 0 if no scrollbar. """ diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index e384ef9e..dc0456a7 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,6 +1,6 @@ - - + + New Project New Novel Jane Doe @@ -11,9 +11,9 @@ None 000000000000f - None + 000000000000f 0000000000008 - 0000000000008 + None @@ -30,13 +30,13 @@ Finished - New + New Minor Major Main - + Novel @@ -81,9 +81,5 @@ New Note - - - Trash - diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 017c955c..3de027df 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -211,7 +211,7 @@ def testGuiEditor_MetaData(qtbot, nwGUI, projPath, mockRnd): assert nwGUI.theProject.tree[C.hSceneDoc].cursorPos == 10 assert nwGUI.docEditor.setCursorLine(None) is False - assert nwGUI.docEditor.setCursorLine(2) is True + assert nwGUI.docEditor.setCursorLine(3) is True assert nwGUI.docEditor.getCursorPosition() == 15 # Document Changed Signal @@ -510,7 +510,7 @@ def testGuiEditor_Insert(qtbot, monkeypatch, nwGUI, projPath, ipsumText, mockRnd theText = "### A Scene\n\n\n%s" % ipsumText[0] assert nwGUI.docEditor.replaceText(theText) is True - assert nwGUI.docEditor.setCursorLine(2) + assert nwGUI.docEditor.setCursorLine(3) # Invalid Keyword assert nwGUI.docEditor.insertKeyWord("stuff") is False @@ -1049,10 +1049,10 @@ def testGuiEditor_BlockFormatting(qtbot, monkeypatch, nwGUI, projPath, ipsumText assert nwGUI.docEditor.getText() == "Title\n\n" assert nwGUI.docEditor.getCursorPosition() == 5 - # Second Line + # Third Line # This also needs to add a new block assert nwGUI.docEditor.replaceText("#### Title\n\nThe Text\n\n") is True - assert nwGUI.docEditor.setCursorLine(2) is True + assert nwGUI.docEditor.setCursorLine(3) is True assert nwGUI.docEditor._formatBlock(nwDocAction.BLOCK_COM) is True assert nwGUI.docEditor.getText() == "#### Title\n\n% The Text\n\n" @@ -1086,11 +1086,11 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd): assert nwGUI.openDocument(C.hSceneDoc) is True # Empty Block - assert nwGUI.docEditor.setCursorLine(1) is True + assert nwGUI.docEditor.setCursorLine(2) is True assert nwGUI.docEditor._followTag() is False # Not On Tag - assert nwGUI.docEditor.setCursorLine(0) is True + assert nwGUI.docEditor.setCursorLine(1) is True assert nwGUI.docEditor._followTag() is False # On Tag Keyword diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 22057e15..f7c2d766 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -57,17 +57,10 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): nwGUI.docViewer.docHeader._refreshDocument() assert nwGUI.docViewer.toPlainText() == origText - # Cursor line - assert nwGUI.docViewer.setCursorLine("not a number") is False - assert nwGUI.docViewer.setCursorLine(3) is True - theCursor = nwGUI.docViewer.textCursor() - assert theCursor.position() == 40 - - # Cursor position - assert nwGUI.docViewer.setCursorPosition("not a number") is False - assert nwGUI.docViewer.setCursorPosition(100) is True - # Select word + theCursor = nwGUI.docViewer.textCursor() + theCursor.setPosition(100) + nwGUI.docViewer.setTextCursor(theCursor) nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor) qClip = qApp.clipboard() @@ -113,7 +106,9 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum): nwGUI.mainMenu.aViewDoc.activate(QAction.Trigger) # Select "Bod" link - assert nwGUI.docViewer.setCursorPosition(27) is True + theCursor = nwGUI.docViewer.textCursor() + theCursor.setPosition(27) + nwGUI.docViewer.setTextCursor(theCursor) nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor) theRect = nwGUI.docViewer.cursorRect() # qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center(), delay=100) diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 43ac328f..da1902da 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -515,17 +515,6 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): assert nwGUI.saveProject() assert nwGUI.closeDocViewer() - # Check a Quick Create and Delete - assert nwGUI.projView.projTree.newTreeItem(nwItemType.FILE, None) - newHandle = nwGUI.projView.getSelectedHandle() - assert newHandle == "0000000000013" - assert nwGUI.theProject.tree[newHandle] is not None - assert nwGUI.projView.requestDeleteItem() - assert nwGUI.projView.setSelectedHandle(newHandle) - assert nwGUI.projView.requestDeleteItem() - assert nwGUI.theProject.tree["0000000000014"] is not None # Trash - assert nwGUI.saveProject() - # Check the files projFile = projPath / "nwProject.nwx" testFile = tstPaths.outDir / "guiEditor_Main_Final_nwProject.nwx"