Merge branch 'master' into project_outline

This commit is contained in:
Veronica K. B. Olsen
2019-11-21 15:31:43 +01:00
6 changed files with 296 additions and 100 deletions
+10 -3
View File
@@ -60,10 +60,16 @@ These are as following:
:header: "Shortcut", "Description" :header: "Shortcut", "Description"
:widths: 15, 50 :widths: 15, 50
":kbd:`Alt-1`", "Switch focus to tree view pane."
":kbd:`Alt-2`", "Switch focus to document editor pane."
":kbd:`Alt-3`", "Switch focus to document viewer pane."
":kbd:`Ctrl-.`", "Correct word under cursor." ":kbd:`Ctrl-.`", "Correct word under cursor."
":kbd:`Ctrl-1`", "Switch focus to tree view pane." ":kbd:`Ctrl-/`", "Change block format to comment."
":kbd:`Ctrl-2`", "Switch focus to document editor pane." ":kbd:`Ctrl-0`", "Remove block formatting for block under cursor."
":kbd:`Ctrl-3`", "Switch focus to document viewer pane." ":kbd:`Ctrl-1`", "Change block format to header level 1."
":kbd:`Ctrl-2`", "Change block format to header level 2."
":kbd:`Ctrl-3`", "Change block format to header level 3."
":kbd:`Ctrl-4`", "Change block format to header level 4."
":kbd:`Ctrl-A`", "Select all text in document." ":kbd:`Ctrl-A`", "Select all text in document."
":kbd:`Ctrl-B`", "Format selected text, or word under cursor, as bold." ":kbd:`Ctrl-B`", "Format selected text, or word under cursor, as bold."
":kbd:`Ctrl-C`", "Copy selected text to clipboard." ":kbd:`Ctrl-C`", "Copy selected text to clipboard."
@@ -89,6 +95,7 @@ These are as following:
":kbd:`Ctrl-Del`", "If in tree view, move a document to trash, or delete a folder." ":kbd:`Ctrl-Del`", "If in tree view, move a document to trash, or delete a folder."
":kbd:`Ctrl-Enter`", "Open the tag or reference under the cursor in the view panel." ":kbd:`Ctrl-Enter`", "Open the tag or reference under the cursor in the view panel."
":kbd:`Ctrl-Shift-,`", "Change project settings." ":kbd:`Ctrl-Shift-,`", "Change project settings."
":kbd:`Ctrl-Shift-/`", "Remove block formatting for block under cursor."
":kbd:`Ctrl-Shift-1`", "Replace occurrence of word in current document, and search for next occurrence." ":kbd:`Ctrl-Shift-1`", "Replace occurrence of word in current document, and search for next occurrence."
":kbd:`Ctrl-Shift-A`", "Select all text in current paragraph." ":kbd:`Ctrl-Shift-A`", "Select all text in current paragraph."
":kbd:`Ctrl-Shift-D`", "Wrap selected text, or word under cursor, in single quotes." ":kbd:`Ctrl-Shift-D`", "Wrap selected text, or word under cursor, in single quotes."
+6
View File
@@ -71,6 +71,12 @@ class nwDocAction(Enum):
GO_NEXT = 15 GO_NEXT = 15
GO_PREV = 16 GO_PREV = 16
REPL_NEXT = 17 REPL_NEXT = 17
BLOCK_H1 = 18
BLOCK_H2 = 19
BLOCK_H3 = 20
BLOCK_H4 = 21
BLOCK_COM = 22
BLOCK_TXT = 23
# END Enum nwDocAction # END Enum nwDocAction
+189 -89
View File
@@ -440,8 +440,20 @@ class GuiDocEditor(QTextEdit):
self._findPrev() self._findPrev()
elif theAction == nwDocAction.REPL_NEXT: elif theAction == nwDocAction.REPL_NEXT:
self._replaceNext() self._replaceNext()
elif theAction == nwDocAction.BLOCK_H1:
self._formatBlock(nwDocAction.BLOCK_H1)
elif theAction == nwDocAction.BLOCK_H2:
self._formatBlock(nwDocAction.BLOCK_H2)
elif theAction == nwDocAction.BLOCK_H3:
self._formatBlock(nwDocAction.BLOCK_H3)
elif theAction == nwDocAction.BLOCK_H4:
self._formatBlock(nwDocAction.BLOCK_H4)
elif theAction == nwDocAction.BLOCK_COM:
self._formatBlock(nwDocAction.BLOCK_COM)
elif theAction == nwDocAction.BLOCK_TXT:
self._formatBlock(nwDocAction.BLOCK_TXT)
else: else:
logger.error("Unknown or unsupported document action %s" % str(theAction)) logger.debug("Unknown or unsupported document action %s" % str(theAction))
return False return False
return True return True
@@ -515,59 +527,26 @@ class GuiDocEditor(QTextEdit):
return return
## ##
# Internal Functions # Signals and Slots
## ##
def _followTag(self, theCursor=None): def _docChange(self, thePos, charsRemoved, charsAdded):
"""Activated by Ctrl+Enter. Checks that we're in a block """Triggered by QTextDocument->contentsChanged. This also
starting with '@'. We then find the word under the cursor and triggers the syntax highlighter.
check that it is after the ':'. If all this is fine, we have a
tag and can tell the document viewer to try and find and load
the file where the tag is defined.
""" """
self.lastEdit = time()
if theCursor is None: if not self.docChanged:
theCursor = self.textCursor() self.setDocumentChanged(True)
if not self.wcTimer.isActive():
theBlock = theCursor.block() self.wcTimer.start()
theText = theBlock.text() if self.mainConf.doReplace and not self.hasSelection:
self._docAutoReplace(self.qDocument.findBlock(thePos))
if len(theText) == 0:
return False
if theText.startswith("@"):
theCursor.select(QTextCursor.WordUnderCursor)
theWord = theCursor.selectedText()
cPos = theText.find(":")
wPos = theCursor.selectionStart() - theBlock.position()
if wPos <= cPos:
return False
logger.verbose("Attempting to follow tag '%s'" % theWord)
self.theParent.docViewer.loadFromTag(theWord)
return True
def _insertHardBreak(self):
theCursor = self.textCursor()
theCursor.beginEditBlock()
theCursor.insertText(" \n")
theCursor.endEditBlock()
return
def _insertNonBreakingSpace(self):
theCursor = self.textCursor()
theCursor.beginEditBlock()
theCursor.insertText(nwUnicode.U_NBSP)
theCursor.endEditBlock()
return
def _openSpellContext(self):
self._openContextMenu(self.cursorRect().center())
return return
def _openContextMenu(self, thePos): def _openContextMenu(self, thePos):
"""Triggered by right click to open the context menu. Also
triggered by the Ctrl+. shortcut.
"""
if not self.spellCheck: if not self.spellCheck:
return return
@@ -625,17 +604,88 @@ class GuiDocEditor(QTextEdit):
self.hLight.rehighlightBlock(theCursor.block()) self.hLight.rehighlightBlock(theCursor.block())
return return
def _docChange(self, thePos, charsRemoved, charsAdded): def _runCounter(self):
"""Triggered by QTextDocument->contentsChanged. This also """Decide whether to run the word counter, or stop the timer due
triggers the syntax highlighter. to inactivity.
""" """
self.lastEdit = time() sinceActive = time()-self.lastEdit
if not self.docChanged: if sinceActive > 5*self.wcInterval:
self.setDocumentChanged(True) logger.debug("Stopping word count timer: no activity last %.1f seconds" % sinceActive)
if not self.wcTimer.isActive(): self.wcTimer.stop()
self.wcTimer.start() elif self.wCounter.isRunning():
if self.mainConf.doReplace and not self.hasSelection: logger.verbose("Word counter thread is busy")
self._docAutoReplace(self.qDocument.findBlock(thePos)) else:
logger.verbose("Starting word counter")
self.wCounter.start()
return
def _updateCounts(self):
"""Slot for the word counter's finished signal
"""
logger.verbose("Updating word count")
tHandle = self.nwDocument.docHandle
self.charCount = self.wCounter.charCount
self.wordCount = self.wCounter.wordCount
self.paraCount = self.wCounter.paraCount
self.theParent.statusBar.setCounts(self.charCount,self.wordCount,self.paraCount)
self.theParent.treeView.propagateCount(tHandle, self.wordCount)
self.theParent.treeView.projectWordCount()
self._checkDocSize(self.charCount)
return
##
# Internal Functions
##
def _followTag(self, theCursor=None):
"""Activated by Ctrl+Enter. Checks that we're in a block
starting with '@'. We then find the word under the cursor and
check that it is after the ':'. If all this is fine, we have a
tag and can tell the document viewer to try and find and load
the file where the tag is defined.
"""
if theCursor is None:
theCursor = self.textCursor()
theBlock = theCursor.block()
theText = theBlock.text()
if len(theText) == 0:
return False
if theText.startswith("@"):
theCursor.select(QTextCursor.WordUnderCursor)
theWord = theCursor.selectedText()
cPos = theText.find(":")
wPos = theCursor.selectionStart() - theBlock.position()
if wPos <= cPos:
return False
logger.verbose("Attempting to follow tag '%s'" % theWord)
self.theParent.docViewer.loadFromTag(theWord)
return True
def _insertHardBreak(self):
theCursor = self.textCursor()
theCursor.beginEditBlock()
theCursor.insertText(" \n")
theCursor.endEditBlock()
return
def _insertNonBreakingSpace(self):
theCursor = self.textCursor()
theCursor.beginEditBlock()
theCursor.insertText(nwUnicode.U_NBSP)
theCursor.endEditBlock()
return
def _openSpellContext(self):
self._openContextMenu(self.cursorRect().center())
return return
def _docAutoReplace(self, theBlock): def _docAutoReplace(self, theBlock):
@@ -693,37 +743,6 @@ class GuiDocEditor(QTextEdit):
return return
def _runCounter(self):
"""Decide whether to run the word counter, or stop the timer due
to inactivity.
"""
sinceActive = time()-self.lastEdit
if sinceActive > 5*self.wcInterval:
logger.debug("Stopping word count timer: no activity last %.1f seconds" % sinceActive)
self.wcTimer.stop()
elif self.wCounter.isRunning():
logger.verbose("Word counter thread is busy")
else:
logger.verbose("Starting word counter")
self.wCounter.start()
return
def _updateCounts(self):
"""Slot for the word counter's finished signal
"""
logger.verbose("Updating word count")
tHandle = self.nwDocument.docHandle
self.charCount = self.wCounter.charCount
self.wordCount = self.wCounter.wordCount
self.paraCount = self.wCounter.paraCount
self.theParent.statusBar.setCounts(self.charCount,self.wordCount,self.paraCount)
self.theParent.treeView.propagateCount(tHandle, self.wordCount)
self.theParent.treeView.projectWordCount()
self._checkDocSize(self.charCount)
return
def _checkDocSize(self, theSize): def _checkDocSize(self, theSize):
"""Check if document size crosses the big document limit set in """Check if document size crosses the big document limit set in
config. If so, we will set the big document flag to True. config. If so, we will set the big document flag to True.
@@ -761,6 +780,87 @@ class GuiDocEditor(QTextEdit):
logger.warning("No selection made, nothing to do") logger.warning("No selection made, nothing to do")
return return
def _formatBlock(self, docAction):
"""Changes the block format of the block under the cursor.
"""
theCursor = self.textCursor()
theBlock = theCursor.block()
if not theBlock.isValid():
logger.debug("Invalid block selected for action %s" % str(docAction))
return
theText = theBlock.text()
if len(theText.strip()) == 0:
logger.debug("Empty block selected for action %s" % str(docAction))
return
# Remove existing format first, if any
if theText.startswith("@"):
logger.error("Cannot apply block format to keyword/value line")
return
elif theText.startswith("% "):
newText = theText[2:]
cOffset = 2
elif theText.startswith("%"):
newText = theText[1:]
cOffset = 1
elif theText.startswith("# "):
newText = theText[2:]
cOffset = 2
elif theText.startswith("## "):
newText = theText[3:]
cOffset = 3
elif theText.startswith("### "):
newText = theText[4:]
cOffset = 4
elif theText.startswith("#### "):
newText = theText[5:]
cOffset = 5
else:
newText = theText
cOffset = 0
# Apply new format
if docAction == nwDocAction.BLOCK_COM:
theText = "% "+newText
cOffset -= 2
elif docAction == nwDocAction.BLOCK_H1:
theText = "# "+newText
cOffset -= 2
elif docAction == nwDocAction.BLOCK_H2:
theText = "## "+newText
cOffset -= 3
elif docAction == nwDocAction.BLOCK_H3:
theText = "### "+newText
cOffset -= 4
elif docAction == nwDocAction.BLOCK_H4:
theText = "#### "+newText
cOffset -= 5
elif docAction == nwDocAction.BLOCK_TXT:
theText = newText
cOffset -= 0
else:
logger.error("Unknown or unsupported block format requested: %s" % str(docAction))
return
# Replace the block text
theCursor.beginEditBlock()
posO = theCursor.position()
theCursor.select(QTextCursor.BlockUnderCursor)
posS = theCursor.selectionStart()
theCursor.removeSelectedText()
theCursor.setPosition(posS)
if posS > 0:
theCursor.insertBlock()
theCursor.insertText(theText)
if posO - cOffset >= 0:
theCursor.setPosition(posO - cOffset)
theCursor.endEditBlock()
self.setTextCursor(theCursor)
return
def _makeSelection(self, selMode): def _makeSelection(self, selMode):
theCursor = self.textCursor() theCursor = self.textCursor()
theCursor.clearSelection() theCursor.clearSelection()
+28 -2
View File
@@ -15,10 +15,10 @@ import nw
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QTextBrowser from PyQt5.QtWidgets import QTextBrowser
from PyQt5.QtGui import QTextOption, QFont, QPalette, QColor from PyQt5.QtGui import QTextOption, QFont, QPalette, QColor, QTextCursor
from nw.convert import ToHtml from nw.convert import ToHtml
from nw.constants import nwAlert, nwItemType from nw.constants import nwAlert, nwItemType, nwDocAction
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -47,6 +47,7 @@ class GuiDocViewer(QTextBrowser):
self.qDocument.setDefaultTextOption(theOpt) self.qDocument.setDefaultTextOption(theOpt)
self.anchorClicked.connect(self._linkClicked) self.anchorClicked.connect(self._linkClicked)
self.setFocusPolicy(Qt.StrongFocus)
logger.debug("DocViewer initialisation complete") logger.debug("DocViewer initialisation complete")
@@ -142,10 +143,35 @@ class GuiDocViewer(QTextBrowser):
return True return True
def docAction(self, theAction):
logger.verbose("Requesting action: %s" % theAction.name)
if self.theHandle is None:
logger.error("No document open")
return False
if theAction == nwDocAction.CUT:
self.copy()
elif theAction == nwDocAction.COPY:
self.copy()
elif theAction == nwDocAction.SEL_ALL:
self._makeSelection(QTextCursor.Document)
elif theAction == nwDocAction.SEL_PARA:
self._makeSelection(QTextCursor.BlockUnderCursor)
else:
logger.debug("Unknown or unsupported document action %s" % str(theAction))
return False
return True
## ##
# Internal Functions # Internal Functions
## ##
def _makeSelection(self, selMode):
theCursor = self.textCursor()
theCursor.clearSelection()
theCursor.select(selMode)
self.setTextCursor(theCursor)
return
def _linkClicked(self, theURL): def _linkClicked(self, theURL):
theLink = theURL.url() theLink = theURL.url()
+51 -6
View File
@@ -40,7 +40,7 @@ class GuiMainMenu(QMenuBar):
self._buildHelpMenu() self._buildHelpMenu()
# Function Pointers # Function Pointers
self._docAction = self.theParent.docEditor.docAction self._docAction = self.theParent.passDocumentAction
self._moveTreeItem = self.theParent.treeView.moveTreeItem self._moveTreeItem = self.theParent.treeView.moveTreeItem
self._newTreeItem = self.theParent.treeView.newTreeItem self._newTreeItem = self.theParent.treeView.newTreeItem
@@ -379,21 +379,21 @@ class GuiMainMenu(QMenuBar):
# View > TreeView # View > TreeView
self.aFocusTree = QAction("TreeView", self) self.aFocusTree = QAction("TreeView", self)
self.aFocusTree.setStatusTip("Move focus to project tree") self.aFocusTree.setStatusTip("Move focus to project tree")
self.aFocusTree.setShortcut("Ctrl+1") self.aFocusTree.setShortcut("Alt+1")
self.aFocusTree.triggered.connect(lambda : self.theParent.setFocus(1)) self.aFocusTree.triggered.connect(lambda : self.theParent.setFocus(1))
self.viewMenu.addAction(self.aFocusTree) self.viewMenu.addAction(self.aFocusTree)
# View > Document Pane 1 # View > Document Pane 1
self.aFocusEditor = QAction("Left Document Pane", self) self.aFocusEditor = QAction("Left Document Pane", self)
self.aFocusEditor.setStatusTip("Move focus to left document pane") self.aFocusEditor.setStatusTip("Move focus to left document pane")
self.aFocusEditor.setShortcut("Ctrl+2") self.aFocusEditor.setShortcut("Alt+2")
self.aFocusEditor.triggered.connect(lambda : self.theParent.setFocus(2)) self.aFocusEditor.triggered.connect(lambda : self.theParent.setFocus(2))
self.viewMenu.addAction(self.aFocusEditor) self.viewMenu.addAction(self.aFocusEditor)
# View > Document Pane 2 # View > Document Pane 2
self.aFocusView = QAction("Right Document Pane", self) self.aFocusView = QAction("Right Document Pane", self)
self.aFocusView.setStatusTip("Move focus to right document pane") self.aFocusView.setStatusTip("Move focus to right document pane")
self.aFocusView.setShortcut("Ctrl+3") self.aFocusView.setShortcut("Alt+3")
self.aFocusView.triggered.connect(lambda : self.theParent.setFocus(3)) self.aFocusView.triggered.connect(lambda : self.theParent.setFocus(3))
self.viewMenu.addAction(self.aFocusView) self.viewMenu.addAction(self.aFocusView)
@@ -570,6 +570,51 @@ class GuiMainMenu(QMenuBar):
self.aFmtSQuote.triggered.connect(lambda: self._docAction(nwDocAction.S_QUOTE)) self.aFmtSQuote.triggered.connect(lambda: self._docAction(nwDocAction.S_QUOTE))
self.fmtMenu.addAction(self.aFmtSQuote) self.fmtMenu.addAction(self.aFmtSQuote)
# Edit > Separator
self.fmtMenu.addSeparator()
# Format > Header 1
self.aFmtHead1 = QAction("Header 1", self)
self.aFmtHead1.setStatusTip("Change the block format to Header 1")
self.aFmtHead1.setShortcut("Ctrl+1")
self.aFmtHead1.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_H1))
self.fmtMenu.addAction(self.aFmtHead1)
# Format > Header 2
self.aFmtHead2 = QAction("Header 2", self)
self.aFmtHead2.setStatusTip("Change the block format to Header 2")
self.aFmtHead2.setShortcut("Ctrl+2")
self.aFmtHead2.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_H2))
self.fmtMenu.addAction(self.aFmtHead2)
# Format > Header 3
self.aFmtHead3 = QAction("Header 3", self)
self.aFmtHead3.setStatusTip("Change the block format to Header 3")
self.aFmtHead3.setShortcut("Ctrl+3")
self.aFmtHead3.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_H3))
self.fmtMenu.addAction(self.aFmtHead3)
# Format > Header 4
self.aFmtHead4 = QAction("Header 4", self)
self.aFmtHead4.setStatusTip("Change the block format to Header 4")
self.aFmtHead4.setShortcut("Ctrl+4")
self.aFmtHead4.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_H4))
self.fmtMenu.addAction(self.aFmtHead4)
# Format > Comment
self.aFmtComment = QAction("Comment", self)
self.aFmtComment.setStatusTip("Change the block format to comment")
self.aFmtComment.setShortcut("Ctrl+/")
self.aFmtComment.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_COM))
self.fmtMenu.addAction(self.aFmtComment)
# Format > Remove Format
self.aFmtNoFormat = QAction("Remove Format", self)
self.aFmtNoFormat.setStatusTip("Strips block format")
self.aFmtNoFormat.setShortcuts(["Ctrl+0","Ctrl+Shift+/"])
self.aFmtNoFormat.triggered.connect(lambda: self._docAction(nwDocAction.BLOCK_TXT))
self.fmtMenu.addAction(self.aFmtNoFormat)
return return
def _buildToolsMenu(self): def _buildToolsMenu(self):
@@ -657,8 +702,8 @@ class GuiMainMenu(QMenuBar):
self.helpMenu.addSeparator() self.helpMenu.addSeparator()
# Document > Preview # Document > Preview
self.aHelp = QAction("Documentation", self) self.aHelp = QAction("Online Documentation", self)
self.aHelp.setStatusTip("View documentation") self.aHelp.setStatusTip("View online documentation")
self.aHelp.setShortcut("F1") self.aHelp.setShortcut("F1")
self.aHelp.triggered.connect(self._openHelp) self.aHelp.triggered.connect(self._openHelp)
self.helpMenu.addAction(self.aHelp) self.helpMenu.addAction(self.aHelp)
+12
View File
@@ -469,6 +469,18 @@ class GuiMain(QMainWindow):
return True return True
def passDocumentAction(self, theAction):
"""Pass on document action theAction to whatever document has
the focus. If no document has focus, the action is discarded.
"""
if self.docEditor.hasFocus():
self.docEditor.docAction(theAction)
elif self.docViewer.hasFocus():
self.docViewer.docAction(theAction)
else:
logger.debug("Document action requested, but no document has focus")
return True
## ##
# Tree Item Actions # Tree Item Actions
## ##