diff --git a/docs/source/interface.txt b/docs/source/interface.txt index 7a0ccca0..3703bf22 100644 --- a/docs/source/interface.txt +++ b/docs/source/interface.txt @@ -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." diff --git a/nw/constants/enum.py b/nw/constants/enum.py index 738ebce8..9d47581e 100644 --- a/nw/constants/enum.py +++ b/nw/constants/enum.py @@ -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 diff --git a/nw/gui/elements/doceditor.py b/nw/gui/elements/doceditor.py index 6fa2294f..681f1082 100644 --- a/nw/gui/elements/doceditor.py +++ b/nw/gui/elements/doceditor.py @@ -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() diff --git a/nw/gui/elements/docviewer.py b/nw/gui/elements/docviewer.py index ab9585f6..39d8f57f 100644 --- a/nw/gui/elements/docviewer.py +++ b/nw/gui/elements/docviewer.py @@ -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() diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 5966509d..679dbacc 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -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) @@ -580,6 +580,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): @@ -667,8 +712,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) diff --git a/nw/guimain.py b/nw/guimain.py index 95e0c6a3..54a928e2 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -458,6 +458,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 ##