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"
: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-1`", "Switch focus to tree view pane."
":kbd:`Ctrl-2`", "Switch focus to document editor pane."
":kbd:`Ctrl-3`", "Switch focus to document viewer pane."
":kbd:`Ctrl-/`", "Change block format to comment."
":kbd:`Ctrl-0`", "Remove block formatting for block under cursor."
":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-B`", "Format selected text, or word under cursor, as bold."
":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-Enter`", "Open the tag or reference under the cursor in the view panel."
":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-A`", "Select all text in current paragraph."
":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_PREV = 16
REPL_NEXT = 17
BLOCK_H1 = 18
BLOCK_H2 = 19
BLOCK_H3 = 20
BLOCK_H4 = 21
BLOCK_COM = 22
BLOCK_TXT = 23
# END Enum nwDocAction
+189 -89
View File
@@ -440,8 +440,20 @@ class GuiDocEditor(QTextEdit):
self._findPrev()
elif theAction == nwDocAction.REPL_NEXT:
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:
logger.error("Unknown or unsupported document action %s" % str(theAction))
logger.debug("Unknown or unsupported document action %s" % str(theAction))
return False
return True
@@ -515,59 +527,26 @@ class GuiDocEditor(QTextEdit):
return
##
# Internal Functions
# Signals and Slots
##
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.
def _docChange(self, thePos, charsRemoved, charsAdded):
"""Triggered by QTextDocument->contentsChanged. This also
triggers the syntax highlighter.
"""
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())
self.lastEdit = time()
if not self.docChanged:
self.setDocumentChanged(True)
if not self.wcTimer.isActive():
self.wcTimer.start()
if self.mainConf.doReplace and not self.hasSelection:
self._docAutoReplace(self.qDocument.findBlock(thePos))
return
def _openContextMenu(self, thePos):
"""Triggered by right click to open the context menu. Also
triggered by the Ctrl+. shortcut.
"""
if not self.spellCheck:
return
@@ -625,17 +604,88 @@ class GuiDocEditor(QTextEdit):
self.hLight.rehighlightBlock(theCursor.block())
return
def _docChange(self, thePos, charsRemoved, charsAdded):
"""Triggered by QTextDocument->contentsChanged. This also
triggers the syntax highlighter.
def _runCounter(self):
"""Decide whether to run the word counter, or stop the timer due
to inactivity.
"""
self.lastEdit = time()
if not self.docChanged:
self.setDocumentChanged(True)
if not self.wcTimer.isActive():
self.wcTimer.start()
if self.mainConf.doReplace and not self.hasSelection:
self._docAutoReplace(self.qDocument.findBlock(thePos))
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
##
# 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
def _docAutoReplace(self, theBlock):
@@ -693,37 +743,6 @@ class GuiDocEditor(QTextEdit):
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):
"""Check if document size crosses the big document limit set in
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")
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):
theCursor = self.textCursor()
theCursor.clearSelection()
+28 -2
View File
@@ -15,10 +15,10 @@ import nw
from PyQt5.QtCore import Qt
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.constants import nwAlert, nwItemType
from nw.constants import nwAlert, nwItemType, nwDocAction
logger = logging.getLogger(__name__)
@@ -47,6 +47,7 @@ class GuiDocViewer(QTextBrowser):
self.qDocument.setDefaultTextOption(theOpt)
self.anchorClicked.connect(self._linkClicked)
self.setFocusPolicy(Qt.StrongFocus)
logger.debug("DocViewer initialisation complete")
@@ -142,10 +143,35 @@ class GuiDocViewer(QTextBrowser):
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
##
def _makeSelection(self, selMode):
theCursor = self.textCursor()
theCursor.clearSelection()
theCursor.select(selMode)
self.setTextCursor(theCursor)
return
def _linkClicked(self, theURL):
theLink = theURL.url()
+51 -6
View File
@@ -40,7 +40,7 @@ class GuiMainMenu(QMenuBar):
self._buildHelpMenu()
# Function Pointers
self._docAction = self.theParent.docEditor.docAction
self._docAction = self.theParent.passDocumentAction
self._moveTreeItem = self.theParent.treeView.moveTreeItem
self._newTreeItem = self.theParent.treeView.newTreeItem
@@ -379,21 +379,21 @@ class GuiMainMenu(QMenuBar):
# View > TreeView
self.aFocusTree = QAction("TreeView", self)
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.viewMenu.addAction(self.aFocusTree)
# View > Document Pane 1
self.aFocusEditor = QAction("Left Document Pane", self)
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.viewMenu.addAction(self.aFocusEditor)
# View > Document Pane 2
self.aFocusView = QAction("Right Document Pane", self)
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.viewMenu.addAction(self.aFocusView)
@@ -570,6 +570,51 @@ class GuiMainMenu(QMenuBar):
self.aFmtSQuote.triggered.connect(lambda: self._docAction(nwDocAction.S_QUOTE))
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
def _buildToolsMenu(self):
@@ -657,8 +702,8 @@ class GuiMainMenu(QMenuBar):
self.helpMenu.addSeparator()
# Document > Preview
self.aHelp = QAction("Documentation", self)
self.aHelp.setStatusTip("View documentation")
self.aHelp = QAction("Online Documentation", self)
self.aHelp.setStatusTip("View online documentation")
self.aHelp.setShortcut("F1")
self.aHelp.triggered.connect(self._openHelp)
self.helpMenu.addAction(self.aHelp)
+12
View File
@@ -469,6 +469,18 @@ class GuiMain(QMainWindow):
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
##