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.core import NWSpellEnchant, countWords
from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode, nwItemClass 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.constants import nwConst, nwFiles, nwKeyWords, nwUnicode
from novelwriter.gui.dochighlight import GuiDocHighlighter from novelwriter.gui.dochighlight import GuiDocHighlighter
@@ -417,7 +417,7 @@ class GuiDocEditor(QTextEdit):
else: else:
self.setCursorPosition(self._nwItem.cursorPos) self.setCursorPosition(self._nwItem.cursorPos)
elif isinstance(tLine, int): elif isinstance(tLine, int):
self.setCursorLine(tLine - 1) self.setCursorLine(tLine)
if self.mainConf.scrollPastEnd > 0: if self.mainConf.scrollPastEnd > 0:
fSize = QFontMetrics(self.font()).lineSpacing() fSize = QFontMetrics(self.font()).lineSpacing()
@@ -654,17 +654,25 @@ class GuiDocEditor(QTextEdit):
self.docEditedStatusChanged.emit(self._docChanged) self.docEditedStatusChanged.emit(self._docChanged)
return self._docChanged return self._docChanged
def setCursorPosition(self, thePosition): def setCursorPosition(self, position):
"""Move the cursor to a given position in the document. """Move the cursor to a given position in the document.
""" """
if not isinstance(thePosition, int): if not isinstance(position, int):
return False return False
nChars = self.document().characterCount() nChars = self.document().characterCount()
if nChars > 1: if nChars > 1:
theCursor = self.textCursor() theCursor = self.textCursor()
theCursor.setPosition(min(max(thePosition, 0), nChars-1)) theCursor.setPosition(minmax(position, 0, nChars-1))
self.setTextCursor(theCursor) 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() self.docFooter.updateLineCount()
return True return True
@@ -677,18 +685,18 @@ class GuiDocEditor(QTextEdit):
self._nwItem.setCursorPos(cursPos) self._nwItem.setCursorPos(cursPos)
return return
def setCursorLine(self, theLine): def setCursorLine(self, lineNo):
"""Move the cursor to a given line in the document. """Move the cursor to a given line in the document.
""" """
if not isinstance(theLine, int): if not isinstance(lineNo, int):
return False return False
if theLine >= 0: lineIdx = lineNo - 1 # Block index is 0 offset, lineNo is 1 offset
theBlock = self.document().findBlockByLineNumber(theLine) if lineIdx >= 0:
theBlock = self.document().findBlockByLineNumber(lineIdx)
if theBlock: if theBlock:
self.setCursorPosition(theBlock.position()) self.setCursorPosition(theBlock.position())
self.docFooter.updateLineCount() logger.debug("Cursor moved to line %d", lineNo)
logger.debug("Cursor moved to line %d", theLine)
return True return True
+6 -33
View File
@@ -323,43 +323,10 @@ class GuiDocViewer(QTextBrowser):
return return
##
# Properties
##
def docHandle(self):
"""Return the handle of the currently open document. Returns
None if no document is open.
"""
return self._docHandle
## ##
# Setters # 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): def setScrollPosition(self, thePos):
"""Set the scrollbar position. """Set the scrollbar position.
""" """
@@ -372,6 +339,12 @@ class GuiDocViewer(QTextBrowser):
# Getters # 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): def getScrollPosition(self):
"""Get the scrollbar position. Returns 0 if no scrollbar. """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 # Signals for user interaction with the novel tree
selectedItemChanged = pyqtSignal(str) selectedItemChanged = pyqtSignal(str)
openDocumentRequest = pyqtSignal(str, Enum, str) openDocumentRequest = pyqtSignal(str, Enum, str, bool)
def __init__(self, mainGui): def __init__(self, mainGui):
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
@@ -594,7 +594,7 @@ class GuiNovelTree(QTreeWidget):
if tHandle is None: if tHandle is None:
return return
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle or "") self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, sTitle or "", False)
return return
@@ -637,7 +637,7 @@ class GuiNovelTree(QTreeWidget):
document editor. document editor.
""" """
tHandle, sTitle = self.getSelectedHandle() tHandle, sTitle = self.getSelectedHandle()
self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "") self.novelView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True)
return return
## ##
+2 -2
View File
@@ -55,7 +55,7 @@ logger = logging.getLogger(__name__)
class GuiOutlineView(QWidget): class GuiOutlineView(QWidget):
loadDocumentTagRequest = pyqtSignal(str, Enum) loadDocumentTagRequest = pyqtSignal(str, Enum)
openDocumentRequest = pyqtSignal(str, Enum, str) openDocumentRequest = pyqtSignal(str, Enum, str, bool)
def __init__(self, mainGui): def __init__(self, mainGui):
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
@@ -545,7 +545,7 @@ class GuiOutlineTree(QTreeWidget):
tHandle, sTitle = self.getSelectedHandle() tHandle, sTitle = self.getSelectedHandle()
if tHandle is None: if tHandle is None:
return return
self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "") self.outlineView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, sTitle or "", True)
return return
@pyqtSlot() @pyqtSlot()
+5 -5
View File
@@ -60,7 +60,7 @@ class GuiProjectView(QWidget):
# Signals for user interaction with the project tree # Signals for user interaction with the project tree
selectedItemChanged = pyqtSignal(str) selectedItemChanged = pyqtSignal(str)
openDocumentRequest = pyqtSignal(str, Enum, str) openDocumentRequest = pyqtSignal(str, Enum, str, bool)
# Requests for the main GUI # Requests for the main GUI
projectSettingsRequest = pyqtSignal(int) projectSettingsRequest = pyqtSignal(int)
@@ -1144,7 +1144,7 @@ class GuiProjectTree(QTreeWidget):
return return
if tItem.isFileType(): if tItem.isFileType():
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "") self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT, "", True)
else: else:
trItem.setExpanded(not trItem.isExpanded()) trItem.setExpanded(not trItem.isExpanded())
@@ -1190,11 +1190,11 @@ class GuiProjectTree(QTreeWidget):
if isFile: if isFile:
aOpenDoc = ctxMenu.addAction(self.tr("Open Document")) aOpenDoc = ctxMenu.addAction(self.tr("Open Document"))
aOpenDoc.triggered.connect( 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 = ctxMenu.addAction(self.tr("View Document"))
aViewDoc.triggered.connect( aViewDoc.triggered.connect(
lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "") lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False)
) )
ctxMenu.addSeparator() ctxMenu.addSeparator()
@@ -1324,7 +1324,7 @@ class GuiProjectTree(QTreeWidget):
return return
if tItem.isFileType(): if tItem.isFileType():
self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "") self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW, "", False)
return return
+12 -5
View File
@@ -596,14 +596,21 @@ class GuiMain(QMainWindow):
logger.debug("Requested item '%s' is not a document", tHandle) logger.debug("Requested item '%s' is not a document", tHandle)
return False 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.closeDocument(beforeOpen=True)
self._changeView(nwView.EDITOR) self._changeView(nwView.EDITOR)
if self.docEditor.loadText(tHandle, tLine): if self.docEditor.loadText(tHandle, tLine):
if changeFocus:
self.docEditor.setFocus()
self.theProject.data.setLastHandle(tHandle, "editor") self.theProject.data.setLastHandle(tHandle, "editor")
self.projView.setSelectedHandle(tHandle, doScroll=doScroll) self.projView.setSelectedHandle(tHandle, doScroll=doScroll)
self.novelView.setActiveHandle(tHandle) self.novelView.setActiveHandle(tHandle)
if changeFocus:
self.docEditor.setFocus()
else: else:
return False return False
@@ -1476,8 +1483,8 @@ class GuiMain(QMainWindow):
self.viewDocument(tHandle=tHandle, sTitle=sTitle) self.viewDocument(tHandle=tHandle, sTitle=sTitle)
return return
@pyqtSlot(str, Enum, str) @pyqtSlot(str, Enum, str, bool)
def _openDocument(self, tHandle, tMode, sTitle): def _openDocument(self, tHandle, tMode, sTitle, setFocus):
"""Handle an open document request from one of the tree views. """Handle an open document request from one of the tree views.
""" """
if tHandle is not None: if tHandle is not None:
@@ -1486,7 +1493,7 @@ class GuiMain(QMainWindow):
hItem = self.theProject.index.getItemHeader(tHandle, sTitle) hItem = self.theProject.index.getItemHeader(tHandle, sTitle)
if hItem is not None: if hItem is not None:
tLine = hItem.line tLine = hItem.line
self.openDocument(tHandle, tLine=tLine, changeFocus=False) self.openDocument(tHandle, tLine=tLine, changeFocus=setFocus)
elif tMode == nwDocMode.VIEW: elif tMode == nwDocMode.VIEW:
self.viewDocument(tHandle=tHandle, sTitle=sTitle) self.viewDocument(tHandle=tHandle, sTitle=sTitle)
return return
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0-rc1" hexVersion="0x020000c1" fileVersion="1.5" timeStamp="2022-11-07 12:58:26"> <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="4" autoCount="2" editTime="3"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="3">
<name>New Project</name> <name>New Project</name>
<title>New Novel</title> <title>New Novel</title>
<author>Jane Doe</author> <author>Jane Doe</author>
@@ -11,9 +11,9 @@
<spellChecking auto="yes">None</spellChecking> <spellChecking auto="yes">None</spellChecking>
<lastHandle> <lastHandle>
<entry key="editor">000000000000f</entry> <entry key="editor">000000000000f</entry>
<entry key="viewer">None</entry> <entry key="viewer">000000000000f</entry>
<entry key="novelTree">0000000000008</entry> <entry key="novelTree">0000000000008</entry>
<entry key="outline">0000000000008</entry> <entry key="outline">None</entry>
</lastHandle> </lastHandle>
<autoReplace/> <autoReplace/>
<titleFormat> <titleFormat>
@@ -30,13 +30,13 @@
<entry key="s000003" count="0" red="50" green="200" blue="0">Finished</entry> <entry key="s000003" count="0" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <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="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="i000006" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry> <entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </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"> <item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes"/> <meta expanded="yes"/>
<name status="s000000" import="i000004">Novel</name> <name status="s000000" import="i000004">Novel</name>
@@ -81,9 +81,5 @@
<meta expanded="no" heading="H1" charCount="51" wordCount="9" paraCount="1" cursorPos="68"/> <meta expanded="no" heading="H1" charCount="51" wordCount="9" paraCount="1" cursorPos="68"/>
<name status="s000000" import="i000004" active="yes">New Note</name> <name status="s000000" import="i000004" active="yes">New Note</name>
</item> </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> </content>
</novelWriterXML> </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.theProject.tree[C.hSceneDoc].cursorPos == 10
assert nwGUI.docEditor.setCursorLine(None) is False 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 assert nwGUI.docEditor.getCursorPosition() == 15
# Document Changed Signal # 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] theText = "### A Scene\n\n\n%s" % ipsumText[0]
assert nwGUI.docEditor.replaceText(theText) is True assert nwGUI.docEditor.replaceText(theText) is True
assert nwGUI.docEditor.setCursorLine(2) assert nwGUI.docEditor.setCursorLine(3)
# Invalid Keyword # Invalid Keyword
assert nwGUI.docEditor.insertKeyWord("stuff") is False 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.getText() == "Title\n\n"
assert nwGUI.docEditor.getCursorPosition() == 5 assert nwGUI.docEditor.getCursorPosition() == 5
# Second Line # Third Line
# This also needs to add a new block # This also needs to add a new block
assert nwGUI.docEditor.replaceText("#### Title\n\nThe Text\n\n") is True 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._formatBlock(nwDocAction.BLOCK_COM) is True
assert nwGUI.docEditor.getText() == "#### Title\n\n% The Text\n\n" 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 assert nwGUI.openDocument(C.hSceneDoc) is True
# Empty Block # Empty Block
assert nwGUI.docEditor.setCursorLine(1) is True assert nwGUI.docEditor.setCursorLine(2) is True
assert nwGUI.docEditor._followTag() is False assert nwGUI.docEditor._followTag() is False
# Not On Tag # Not On Tag
assert nwGUI.docEditor.setCursorLine(0) is True assert nwGUI.docEditor.setCursorLine(1) is True
assert nwGUI.docEditor._followTag() is False assert nwGUI.docEditor._followTag() is False
# On Tag Keyword # On Tag Keyword
+6 -11
View File
@@ -57,17 +57,10 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
nwGUI.docViewer.docHeader._refreshDocument() nwGUI.docViewer.docHeader._refreshDocument()
assert nwGUI.docViewer.toPlainText() == origText 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 # Select word
theCursor = nwGUI.docViewer.textCursor()
theCursor.setPosition(100)
nwGUI.docViewer.setTextCursor(theCursor)
nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor) nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor)
qClip = qApp.clipboard() qClip = qApp.clipboard()
@@ -113,7 +106,9 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
nwGUI.mainMenu.aViewDoc.activate(QAction.Trigger) nwGUI.mainMenu.aViewDoc.activate(QAction.Trigger)
# Select "Bod" link # 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) nwGUI.docViewer._makeSelection(QTextCursor.WordUnderCursor)
theRect = nwGUI.docViewer.cursorRect() theRect = nwGUI.docViewer.cursorRect()
# qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center(), delay=100) # 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.saveProject()
assert nwGUI.closeDocViewer() 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 # Check the files
projFile = projPath / "nwProject.nwx" projFile = projPath / "nwProject.nwx"
testFile = tstPaths.outDir / "guiEditor_Main_Final_nwProject.nwx" testFile = tstPaths.outDir / "guiEditor_Main_Final_nwProject.nwx"