From d91d77d5aba1877cb46aacec4b741fafaf610e0f Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Sun, 6 Oct 2019 15:36:34 +0200 Subject: [PATCH] Search/replace now fully implemented, although only for currently edited document --- nw/enum.py | 6 ++-- nw/gui/doceditor.py | 69 ++++++++++++++++++++++++++++++-------------- nw/gui/mainmenu.py | 14 +++++++++ nw/gui/searchbar.py | 70 +++++++++++++++++++++++++++++++++++++-------- nw/gui/winmain.py | 9 ++++++ 5 files changed, 133 insertions(+), 35 deletions(-) diff --git a/nw/enum.py b/nw/enum.py index 54fee904..f83d9135 100644 --- a/nw/enum.py +++ b/nw/enum.py @@ -66,8 +66,10 @@ class nwDocAction(Enum): SEL_ALL = 11 SEL_PARA = 12 FIND = 13 - GO_NEXT = 14 - GO_PREV = 15 + REPLACE = 14 + GO_NEXT = 15 + GO_PREV = 16 + REPL_NEXT = 17 # END Enum nwDocAction diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 7f1466fc..5e0bc81c 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -267,21 +267,23 @@ class GuiDocEditor(QTextEdit): if not self.theParent.hasProject: logger.error("No project open") return False - if theAction == nwDocAction.UNDO: self.undo() - elif theAction == nwDocAction.REDO: self.redo() - elif theAction == nwDocAction.CUT: self.cut() - elif theAction == nwDocAction.COPY: self.copy() - elif theAction == nwDocAction.PASTE: self.paste() - elif theAction == nwDocAction.BOLD: self._wrapSelection("**","**") - elif theAction == nwDocAction.ITALIC: self._wrapSelection("_","_") - elif theAction == nwDocAction.U_LINE: self._wrapSelection("__","__") - elif theAction == nwDocAction.S_QUOTE: self._wrapSelection(self.typSQOpen,self.typSQClose) - elif theAction == nwDocAction.D_QUOTE: self._wrapSelection(self.typDQOpen,self.typDQClose) - elif theAction == nwDocAction.SEL_ALL: self._makeSelection(QTextCursor.Document) - elif theAction == nwDocAction.SEL_PARA: self._makeSelection(QTextCursor.BlockUnderCursor) - elif theAction == nwDocAction.FIND: self._beginSearch() - elif theAction == nwDocAction.GO_NEXT: self._findNext() - elif theAction == nwDocAction.GO_PREV: self._findPrev() + if theAction == nwDocAction.UNDO: self.undo() + elif theAction == nwDocAction.REDO: self.redo() + elif theAction == nwDocAction.CUT: self.cut() + elif theAction == nwDocAction.COPY: self.copy() + elif theAction == nwDocAction.PASTE: self.paste() + elif theAction == nwDocAction.BOLD: self._wrapSelection("**","**") + elif theAction == nwDocAction.ITALIC: self._wrapSelection("_","_") + elif theAction == nwDocAction.U_LINE: self._wrapSelection("__","__") + elif theAction == nwDocAction.S_QUOTE: self._wrapSelection(self.typSQOpen,self.typSQClose) + elif theAction == nwDocAction.D_QUOTE: self._wrapSelection(self.typDQOpen,self.typDQClose) + elif theAction == nwDocAction.SEL_ALL: self._makeSelection(QTextCursor.Document) + elif theAction == nwDocAction.SEL_PARA: self._makeSelection(QTextCursor.BlockUnderCursor) + elif theAction == nwDocAction.FIND: self._beginSearch() + elif theAction == nwDocAction.REPLACE: self._beginReplace() + elif theAction == nwDocAction.GO_NEXT: self._findNext() + elif theAction == nwDocAction.GO_PREV: self._findPrev() + elif theAction == nwDocAction.REPL_NEXT: self._replaceNext() else: logger.error("Unknown or unsupported document action %s" % str(theAction)) return False @@ -489,21 +491,26 @@ class GuiDocEditor(QTextEdit): return def _beginSearch(self): - + """Sets the selected text as the search text for the search bar. + """ theCursor = self.textCursor() if theCursor.hasSelection(): selText = theCursor.selectedText() else: selText = "" - self.theParent.searchBar.setSearchText(selText) + return - if selText != "": - self._findNext() - + def _beginReplace(self): + """Opens the replace line of the search bar and sets the replace text. + """ + self.theParent.searchBar.setReplaceText("") return def _findNext(self): + """Searches for the next occurrence of the search bar text in the document. + Wraps back to the top if not found. + """ searchFor = self.theParent.searchBar.getSearchText() wasFound = self.find(searchFor) if not wasFound: @@ -513,12 +520,32 @@ class GuiDocEditor(QTextEdit): return def _findPrev(self): + """Searches for the previous occurrence of the search bar text in the document. + Wraps back to the end if not found. + """ searchFor = self.theParent.searchBar.getSearchText() - wasFound = self.find(searchFor, QTextDocument.FindBackward) + wasFound = self.find(searchFor, QTextDocument.FindBackward) if not wasFound: theCursor = self.textCursor() theCursor.movePosition(QTextCursor.End) self.setTextCursor(theCursor) return + def _replaceNext(self): + """Searches for the next occurrence of the search bar text in the document and replaces it + with the replace text. Wraps back to the top if not found. + """ + theCursor = self.textCursor() + replWith = self.theParent.searchBar.getReplaceText() + if theCursor.hasSelection(): + xPos = theCursor.selectionStart() + theCursor.beginEditBlock() + theCursor.removeSelectedText() + theCursor.insertText(replWith) + theCursor.endEditBlock() + theCursor.setPosition(xPos) + self.setTextCursor(theCursor) + self._findNext() + return + # END Class GuiDocEditor diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index f1c1a6d1..a4367953 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -405,6 +405,13 @@ class GuiMainMenu(QMenuBar): menuItem.triggered.connect(lambda: self._docAction(nwDocAction.FIND)) self.editMenu.addAction(menuItem) + # Edit > Replace + menuItem = QAction(QIcon.fromTheme("edit-find-replace"), "Replace", self) + menuItem.setStatusTip("Replace text in document") + menuItem.setShortcut("Ctrl+H") + menuItem.triggered.connect(lambda: self._docAction(nwDocAction.REPLACE)) + self.editMenu.addAction(menuItem) + # Edit > Find Next menuItem = QAction(QIcon.fromTheme("go-next"), "Go Next", self) menuItem.setStatusTip("Find next occurrence text in document") @@ -419,6 +426,13 @@ class GuiMainMenu(QMenuBar): menuItem.triggered.connect(lambda: self._docAction(nwDocAction.GO_PREV)) self.editMenu.addAction(menuItem) + # Edit > Replace Next + menuItem = QAction(QIcon.fromTheme("go-next"), "Replace Next", self) + menuItem.setStatusTip("Find and replace next occurrence text in document") + menuItem.setShortcut("Ctrl+Shift+1") + menuItem.triggered.connect(lambda: self._docAction(nwDocAction.REPL_NEXT)) + self.editMenu.addAction(menuItem) + # Edit > Separator self.editMenu.addSeparator() diff --git a/nw/gui/searchbar.py b/nw/gui/searchbar.py index f88de328..90ceab42 100644 --- a/nw/gui/searchbar.py +++ b/nw/gui/searchbar.py @@ -13,8 +13,9 @@ import logging import nw +from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import QFrame, QGridLayout, QLabel, QLineEdit, QPushButton +from PyQt5.QtWidgets import QFrame, QGridLayout, QLabel, QLineEdit, QPushButton, QApplication from nw.enum import nwDocAction @@ -27,26 +28,37 @@ class GuiSearchBar(QFrame): logger.debug("Initialising GuiSearchBar ...") - self.mainConf = nw.CONFIG - self.theParent = theParent + self.mainConf = nw.CONFIG + self.theParent = theParent + self.repVisible = False self.setContentsMargins(0,0,0,0) self.mainBox = QGridLayout(self) self.setLayout(self.mainBox) - self.searchBox = QLineEdit() - self.closeButton = QPushButton(QIcon.fromTheme("edit-delete"),"") - self.searchButton = QPushButton(QIcon.fromTheme("edit-find"),"") + self.searchBox = QLineEdit() + self.replaceBox = QLineEdit() + self.searchLabel = QLabel("Search") + self.replaceLabel = QLabel("Replace") + self.closeButton = QPushButton(QIcon.fromTheme("edit-delete"),"") + self.searchButton = QPushButton(QIcon.fromTheme("edit-find"),"") + self.replaceButton = QPushButton(QIcon.fromTheme("edit-find-replace"),"") self.closeButton.clicked.connect(self._doClose) self.searchButton.clicked.connect(self._doSearch) + self.replaceButton.clicked.connect(self._doReplace) + self.searchBox.returnPressed.connect(self._doSearch) + self.replaceBox.returnPressed.connect(self._doSearch) - self.mainBox.addWidget(QLabel(""), 0,0) - self.mainBox.addWidget(QLabel("Search"), 0,1) - self.mainBox.addWidget(self.searchBox, 0,2) - self.mainBox.addWidget(self.searchButton,0,3) - self.mainBox.addWidget(self.closeButton, 0,4) + self.mainBox.addWidget(QLabel(""), 0,0) + self.mainBox.addWidget(self.searchLabel, 0,1) + self.mainBox.addWidget(self.searchBox, 0,2) + self.mainBox.addWidget(self.searchButton, 0,3) + self.mainBox.addWidget(self.closeButton, 0,4) + self.mainBox.addWidget(self.replaceLabel, 1,1) + self.mainBox.addWidget(self.replaceBox, 1,2) + self.mainBox.addWidget(self.replaceButton, 1,3) self.mainBox.setColumnStretch(0,1) self.mainBox.setColumnStretch(1,0) @@ -55,10 +67,19 @@ class GuiSearchBar(QFrame): self.mainBox.setColumnStretch(4,0) self.mainBox.setContentsMargins(0,0,0,0) + self.searchBox.setMinimumWidth(180) + self.replaceBox.setMinimumWidth(180) + + self._replaceVisible(False) + logger.debug("GuiSearchBar initialisation complete") return + ## + # Get and Set Functions + ## + def setSearchText(self, theText): if not self.isVisible(): @@ -71,19 +92,44 @@ class GuiSearchBar(QFrame): return True + def setReplaceText(self, theText): + self._replaceVisible(True) + self.replaceBox.setFocus(True) + self.replaceBox.setText(theText) + return True + def getSearchText(self): return self.searchBox.text() + def getReplaceText(self): + return self.replaceBox.text() + ## # Internal Functions ## def _doClose(self): + self._replaceVisible(False) self.setVisible(False) return def _doSearch(self): - self.theParent.docEditor.docAction(nwDocAction.GO_NEXT) + modKey = QApplication.keyboardModifiers() + if modKey == Qt.ShiftModifier: + self.theParent.docEditor.docAction(nwDocAction.GO_PREV) + else: + self.theParent.docEditor.docAction(nwDocAction.GO_NEXT) return + def _doReplace(self): + self.theParent.docEditor.docAction(nwDocAction.REPL_NEXT) + return + + def _replaceVisible(self, isVisible): + self.replaceLabel.setVisible(isVisible) + self.replaceBox.setVisible(isVisible) + self.replaceButton.setVisible(isVisible) + self.repVisible = isVisible + return True + # END Class GuiSearchBar diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index c39aa6f5..8dee5c41 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -137,6 +137,7 @@ class GuiMain(QMainWindow): # Keyboard Shortcuts QShortcut(Qt.Key_Return, self.treeView, context=Qt.WidgetShortcut, activated=self._treeKeyPressReturn) + QShortcut(Qt.Key_Escape, self, activated=self._keyPressEscape) # Forward Functions self.setStatus = self.statusBar.setStatus @@ -689,6 +690,14 @@ class GuiMain(QMainWindow): logger.verbose("Requested item %s is a folder" % tHandle) return + def _keyPressEscape(self): + """When the escape key is pressed somewhere in the main window, do the following, in order + """ + if self.searchBar.isVisible(): + self.searchBar.setVisible(False) + return + return + def _splitMainMove(self, pWidth, pHeight): """Alert dependent GUI elements that the main pane splitter has been moved. """