Improve document navigation (#1243)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
##
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user