Search code examples
pythonpysidepyside6qmainwindowqdialog

Unable to keep searching on QplainTextEdit while Qdialog is open


I am trying to replicate default Notepad find operation.Notepad Find Functionality

But in my case the QDialog is getting closed after the first operation. Can somebody please help me in keeping the QDialog open while the MainWindow is performing the search operations or any alternative.

Here is my code:

class MainWindow(QMainWindow, Ui_Widget):
    def init(self, app):
        super().init()
        self.app = app
        self.setupUi(self)
        self.setWindowTitle( "Untitled - Notepad")
        self.actionFind.triggered.connect(self.find)

    def find(self):
        cursor = self.plainTextEdit.textCursor()
        selected_text = cursor.selectedText()
        if selected_text != "":
            self.text = selected_text
        else:
            clipboard = QClipboard()
            self.text = clipboard.text(QClipboard.Mode.Clipboard)


        self.find_dialog = FindDialog(self.text)
        ret = self.find_dialog.exec()
        if ret == QDialog.Accepted:
            self.text = self.find_dialog.f_search_lineEdit.text()
            self.plainTextEdit.find(self.text)


class FindDialog(QDialog, Ui_FindDialog):
    def init(self, text):
        super().init()
        self.setupUi(self)
        self.setWindowTitle("Find")
        self.search_text = text

        #Connections
        self.f_find_next_pushButton.clicked.connect(self.ok)
        self.f_cancel_find_pushButton.clicked.connect(self.cancel)


        self.f_search_lineEdit.setText(self.search_text)

    def cancel(self):
        self.reject()

    def ok(self):
        self.search_text = self.f_search_lineEdit.text()
        self.accept()

Solution

  • The QDialog exec() function blocks the code execution until it returns, and that only happens when the dialog is closed.

    You need to keep the dialog visible in order to perform multiple searches, so you obviously cannot use the accept() function, since, as the name suggests, that call will accept the dialog by closing it and returning its result to the caller: exec() returns and code execution in the function that called it will then continue.

    For a situation like this, the solution is to use signals: you create a custom signal for the dialog, which will be emitted with the search query, and connect that signal to the function that finally performs the text search.

    Note that for this case it doesn't make a lot of sense to continuously create a new dialog every time, and it's better to just create it once and eventually show it again when required. Also remember that in order to make dialogs always display above their parent window, the parent argument is mandatory, otherwise they can be potentially hidden if the user clicks on the main window.

    While we could use the open() function (which doesn't block code execution), that will make the dialog completely modal, meaning that no interaction with the other application windows will be possible. This is not acceptable for a "find dialog", because the user may want to edit the text while keeping the dialog opened. That's why the parent argument is important: it always keeps the window above its parent, while allowing interaction with that parent.

    By doing the above, when the find action is triggered we just need to call show() and then activateWindow(): the first is required if it wasn't visible, while the second ensures that the dialog will get input focus in case it already was.

    Finally,

    class TextEditor(QMainWindow):
        def __init__(self):
            super().__init__()
            self.setWindowTitle('Untitled[*] - Notepad')
    
            editMenu = self.menuBar().addMenu('Edit')
            self.findAction = editMenu.addAction('Find')
            self.findAction.setShortcut(
                QKeySequence(QKeySequence.StandardKey.Find))
    
            self.editor = QPlainTextEdit()
            self.setCentralWidget(self.editor)
    
            self.findDialog = FindDialog(self)
    
            self.findAction.triggered.connect(self.showFindDialog)
            self.editor.document().modificationChanged.connect(
                self.setWindowModified)
            self.findDialog.findRequested.connect(self.findText)
    
        def findText(self, text, back=False):
            findFlag = QTextDocument.FindFlag.FindBackward if back else 0
            self.editor.find(text, QTextDocument.FindFlag(findFlag))
    
        def showFindDialog(self):
            selection = self.editor.textCursor().selectedText()
            if selection:
                self.findDialog.setText(selection)
            elif not self.findDialog.isVisible():
                clipboard = QApplication.clipboard().text()
                if clipboard:
                    self.findDialog.setText(clipboard)
    
            self.findDialog.show()
            self.findDialog.activateWindow()
    
        def sizeHint(self):
            return QApplication.primaryScreen().size() / 2
    
    
    class FindDialog(QDialog):
        findRequested = pyqtSignal(str, bool)
    
        def __init__(self, parent):
            super().__init__(parent)
            self.setWindowTitle('Find text')
            self.lineEdit = QLineEdit()
            self.nextRadio = QRadioButton('&Forward', checked=True)
            self.prevRadio = QRadioButton('&Backward')
    
            buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
            buttonBox.addButton('&Search', QDialogButtonBox.ButtonRole.AcceptRole)
    
            layout = QVBoxLayout(self)
            layout.addWidget(self.lineEdit)
            optLayout = QHBoxLayout()
            layout.addLayout(optLayout)
            optLayout.addWidget(self.nextRadio)
            optLayout.addWidget(self.prevRadio)
            optLayout.addStretch()
            layout.addWidget(buttonBox)
    
            # disable focus for all buttons
            for btn in self.findChildren(QAbstractButton):
                btn.setFocusPolicy(Qt.FocusPolicy(0))
            hint = self.sizeHint()
            hint.setWidth(hint.width() * 2)
            self.setFixedSize(hint)
    
            buttonBox.rejected.connect(self.reject)
            buttonBox.accepted.connect(self.emitFind)
    
        def setText(self, text):
            self.lineEdit.setText(text)
    
        def emitFind(self):
            self.findRequested.emit(
                self.lineEdit.text(), self.prevRadio.isChecked())
    
    
    if __name__ == '__main__':
        import sys
        app = QApplication(sys.argv)
        editor = TextEditor()
        editor.show()
        sys.exit(app.exec())