Improve document navigation (#1243)

This commit is contained in:
Veronica Berglyd Olsen
2022-11-15 17:00:27 +01:00
committed by GitHub
10 changed files with 65 additions and 97 deletions
+19 -11
View File
@@ -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
@@ -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()
@@ -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,18 @@ 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)
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())
self.docFooter.updateLineCount()
logger.debug("Cursor moved to line %d", theLine)
logger.debug("Cursor moved to line %d", lineNo)
return True
+6 -33
View File
@@ -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.
"""
+3 -3
View File
@@ -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
##
+2 -2
View File
@@ -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()
+5 -5
View File
@@ -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
+12 -5
View File
@@ -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
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" timeStamp="2022-11-07 12:58:26">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="4" autoCount="2" editTime="3">
<novelWriterXML appVersion="2.0-rc2" hexVersion="0x020000c2" fileVersion="1.5" timeStamp="2022-11-15 15:14:31">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="3">
<name>New Project</name>
<title>New Novel</title>
<author>Jane Doe</author>
@@ -11,9 +11,9 @@
<spellChecking auto="yes">None</spellChecking>
<lastHandle>
<entry key="editor">000000000000f</entry>
<entry key="viewer">None</entry>
<entry key="viewer">000000000000f</entry>
<entry key="novelTree">0000000000008</entry>
<entry key="outline">0000000000008</entry>
<entry key="outline">None</entry>
</lastHandle>
<autoReplace/>
<titleFormat>
@@ -30,13 +30,13 @@
<entry key="s000003" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry key="i000004" count="7" red="100" green="100" blue="100">New</entry>
<entry key="i000004" count="6" red="100" green="100" blue="100">New</entry>
<entry key="i000005" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i000006" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content items="12" novelWords="136" notesWords="27">
<content items="11" novelWords="136" notesWords="27">
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes"/>
<name status="s000000" import="i000004">Novel</name>
@@ -81,9 +81,5 @@
<meta expanded="no" heading="H1" charCount="51" wordCount="9" paraCount="1" cursorPos="68"/>
<name status="s000000" import="i000004" active="yes">New Note</name>
</item>
<item handle="0000000000014" parent="None" root="0000000000014" order="4" type="ROOT" class="TRASH">
<meta expanded="no"/>
<name status="s000000" import="i000004">Trash</name>
</item>
</content>
</novelWriterXML>
+6 -6
View File
@@ -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
+6 -11
View File
@@ -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)
-11
View File
@@ -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"