1# -*- coding: utf-8 -*-
2
3# Copyright (c) 2013 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4#
5
6"""
7Module implementing the QRegularExpression wizard dialog.
8"""
9
10import os
11import re
12import sys
13import json
14
15from PyQt5.QtCore import QFileInfo, pyqtSlot, QProcess, QByteArray
16from PyQt5.QtGui import QClipboard, QTextCursor
17from PyQt5.QtWidgets import (
18    QWidget, QDialog, QInputDialog, QApplication, QDialogButtonBox,
19    QVBoxLayout, QTableWidgetItem
20)
21
22from E5Gui import E5MessageBox, E5FileDialog
23from E5Gui.E5MainWindow import E5MainWindow
24
25from .Ui_QRegularExpressionWizardDialog import (
26    Ui_QRegularExpressionWizardDialog
27)
28
29import UI.PixmapCache
30
31import Utilities
32import Preferences
33
34
35class QRegularExpressionWizardWidget(QWidget,
36                                     Ui_QRegularExpressionWizardDialog):
37    """
38    Class implementing the QRegularExpression wizard dialog.
39    """
40    def __init__(self, parent=None, fromEric=True):
41        """
42        Constructor
43
44        @param parent parent widget (QWidget)
45        @param fromEric flag indicating a call from within eric
46        """
47        super().__init__(parent)
48        self.setupUi(self)
49
50        # initialize icons of the tool buttons
51        self.commentButton.setIcon(UI.PixmapCache.getIcon("comment"))
52        self.charButton.setIcon(UI.PixmapCache.getIcon("characters"))
53        self.anycharButton.setIcon(UI.PixmapCache.getIcon("anychar"))
54        self.repeatButton.setIcon(UI.PixmapCache.getIcon("repeat"))
55        self.nonGroupButton.setIcon(UI.PixmapCache.getIcon("nongroup"))
56        self.atomicGroupButton.setIcon(
57            UI.PixmapCache.getIcon("atomicgroup"))
58        self.groupButton.setIcon(UI.PixmapCache.getIcon("group"))
59        self.namedGroupButton.setIcon(UI.PixmapCache.getIcon("namedgroup"))
60        self.namedReferenceButton.setIcon(
61            UI.PixmapCache.getIcon("namedreference"))
62        self.altnButton.setIcon(UI.PixmapCache.getIcon("altn"))
63        self.beglineButton.setIcon(UI.PixmapCache.getIcon("begline"))
64        self.endlineButton.setIcon(UI.PixmapCache.getIcon("endline"))
65        self.wordboundButton.setIcon(
66            UI.PixmapCache.getIcon("wordboundary"))
67        self.nonwordboundButton.setIcon(
68            UI.PixmapCache.getIcon("nonwordboundary"))
69        self.poslookaheadButton.setIcon(
70            UI.PixmapCache.getIcon("poslookahead"))
71        self.neglookaheadButton.setIcon(
72            UI.PixmapCache.getIcon("neglookahead"))
73        self.poslookbehindButton.setIcon(
74            UI.PixmapCache.getIcon("poslookbehind"))
75        self.neglookbehindButton.setIcon(
76            UI.PixmapCache.getIcon("neglookbehind"))
77        self.undoButton.setIcon(UI.PixmapCache.getIcon("editUndo"))
78        self.redoButton.setIcon(UI.PixmapCache.getIcon("editRedo"))
79
80        self.namedGroups = re.compile(r"""\(?P<([^>]+)>""").findall
81
82        # start the PyQt5 server part
83        self.__pyqt5Available = False
84        self.__pyqt5Server = QProcess(self)
85        self.__pyqt5Server.start(
86            sys.executable, [os.path.join(
87                os.path.dirname(__file__), "QRegularExpressionWizardServer.py")
88            ])
89        if self.__pyqt5Server.waitForStarted(5000):
90            self.__pyqt5Server.setReadChannel(
91                QProcess.ProcessChannel.StandardOutput)
92            if self.__sendCommand("available"):
93                response = self.__receiveResponse()
94                if response and response["available"]:
95                    self.__pyqt5Available = True
96
97        self.saveButton = self.buttonBox.addButton(
98            self.tr("Save"), QDialogButtonBox.ButtonRole.ActionRole)
99        self.saveButton.setToolTip(
100            self.tr("Save the regular expression to a file"))
101        self.loadButton = self.buttonBox.addButton(
102            self.tr("Load"), QDialogButtonBox.ButtonRole.ActionRole)
103        self.loadButton.setToolTip(
104            self.tr("Load a regular expression from a file"))
105        if self.__pyqt5Available:
106            self.validateButton = self.buttonBox.addButton(
107                self.tr("Validate"), QDialogButtonBox.ButtonRole.ActionRole)
108            self.validateButton.setToolTip(
109                self.tr("Validate the regular expression"))
110            self.executeButton = self.buttonBox.addButton(
111                self.tr("Execute"), QDialogButtonBox.ButtonRole.ActionRole)
112            self.executeButton.setToolTip(
113                self.tr("Execute the regular expression"))
114            self.nextButton = self.buttonBox.addButton(
115                self.tr("Next match"), QDialogButtonBox.ButtonRole.ActionRole)
116            self.nextButton.setToolTip(
117                self.tr("Show the next match of the regular expression"))
118            self.nextButton.setEnabled(False)
119        else:
120            self.validateButton = None
121            self.executeButton = None
122            self.nextButton = None
123
124        if fromEric:
125            self.buttonBox.setStandardButtons(
126                QDialogButtonBox.StandardButton.Cancel |
127                QDialogButtonBox.StandardButton.Ok)
128            self.copyButton = None
129        else:
130            self.copyButton = self.buttonBox.addButton(
131                self.tr("Copy"), QDialogButtonBox.ButtonRole.ActionRole)
132            self.copyButton.setToolTip(
133                self.tr("Copy the regular expression to the clipboard"))
134            self.buttonBox.setStandardButtons(
135                QDialogButtonBox.StandardButton.Close)
136            self.variableLabel.hide()
137            self.variableLineEdit.hide()
138            self.variableLine.hide()
139            self.regexpTextEdit.setFocus()
140
141    def __sendCommand(self, command, **kw):
142        """
143        Private method to send a command to the PyQt5 server.
144
145        @param command dictionary with command string and related
146            data (dict)
147        @keyparam kw parameters for the command
148        @return flag indicating a successful transmission (boolean)
149        """
150        result = False
151        if command:
152            commandDict = {"command": command}
153            commandDict.update(kw)
154            commandStr = json.dumps(commandDict) + "\n"
155            data = QByteArray(commandStr.encode("utf-8"))
156            self.__pyqt5Server.write(data)
157            result = self.__pyqt5Server.waitForBytesWritten(10000)
158        return result
159
160    def __receiveResponse(self):
161        """
162        Private method to receive a response from the PyQt5 server.
163
164        @return response dictionary (dict)
165        """
166        responseDict = {}
167        if self.__pyqt5Server.waitForReadyRead(10000):
168            data = bytes(self.__pyqt5Server.readAllStandardOutput())
169            responseStr = data.decode("utf-8")
170            responseDict = json.loads(responseStr)
171            if responseDict["error"]:
172                E5MessageBox.critical(
173                    self,
174                    self.tr("Communication Error"),
175                    self.tr("""<p>The PyQt5 backend reported"""
176                            """ an error.</p><p>{0}</p>""")
177                    .format(responseDict["error"]))
178                responseDict = {}
179
180        return responseDict
181
182    def shutdown(self):
183        """
184        Public method to shut down the PyQt5 server part.
185        """
186        self.__sendCommand("exit")
187        self.__pyqt5Server.waitForFinished(5000)
188
189    def __insertString(self, s, steps=0):
190        """
191        Private method to insert a string into line edit and move cursor.
192
193        @param s string to be inserted into the regexp line edit
194            (string)
195        @param steps number of characters to move the cursor (integer).
196            Negative steps moves cursor back, positives forward.
197        """
198        self.regexpTextEdit.insertPlainText(s)
199        tc = self.regexpTextEdit.textCursor()
200        if steps != 0:
201            if steps < 0:
202                act = QTextCursor.MoveOperation.Left
203                steps = abs(steps)
204            else:
205                act = QTextCursor.MoveOperation.Right
206            for _ in range(steps):
207                tc.movePosition(act)
208        self.regexpTextEdit.setTextCursor(tc)
209
210    @pyqtSlot()
211    def on_commentButton_clicked(self):
212        """
213        Private slot to handle the comment toolbutton.
214        """
215        self.__insertString("(?#)", -1)
216
217    @pyqtSlot()
218    def on_charButton_clicked(self):
219        """
220        Private slot to handle the characters toolbutton.
221        """
222        from .QRegularExpressionWizardCharactersDialog import (
223            QRegularExpressionWizardCharactersDialog
224        )
225        dlg = QRegularExpressionWizardCharactersDialog(self)
226        if dlg.exec() == QDialog.DialogCode.Accepted:
227            self.__insertString(dlg.getCharacters())
228
229    @pyqtSlot()
230    def on_anycharButton_clicked(self):
231        """
232        Private slot to handle the any character toolbutton.
233        """
234        self.__insertString(".")
235
236    @pyqtSlot()
237    def on_repeatButton_clicked(self):
238        """
239        Private slot to handle the repeat toolbutton.
240        """
241        from .QRegularExpressionWizardRepeatDialog import (
242            QRegularExpressionWizardRepeatDialog
243        )
244        dlg = QRegularExpressionWizardRepeatDialog(self)
245        if dlg.exec() == QDialog.DialogCode.Accepted:
246            self.__insertString(dlg.getRepeat())
247
248    @pyqtSlot()
249    def on_nonGroupButton_clicked(self):
250        """
251        Private slot to handle the non group toolbutton.
252        """
253        self.__insertString("(?:)", -1)
254
255    @pyqtSlot()
256    def on_atomicGroupButton_clicked(self):
257        """
258        Private slot to handle the atomic non group toolbutton.
259        """
260        self.__insertString("(?>)", -1)
261
262    @pyqtSlot()
263    def on_groupButton_clicked(self):
264        """
265        Private slot to handle the group toolbutton.
266        """
267        self.__insertString("()", -1)
268
269    @pyqtSlot()
270    def on_namedGroupButton_clicked(self):
271        """
272        Private slot to handle the named group toolbutton.
273        """
274        self.__insertString("(?P<>)", -2)
275
276    @pyqtSlot()
277    def on_namedReferenceButton_clicked(self):
278        """
279        Private slot to handle the named reference toolbutton.
280        """
281        # determine cursor position as length into text
282        length = self.regexpTextEdit.textCursor().position()
283
284        # only present group names that occur before the current
285        # cursor position
286        regex = self.regexpTextEdit.toPlainText()[:length]
287        names = self.namedGroups(regex)
288        if not names:
289            E5MessageBox.information(
290                self,
291                self.tr("Named reference"),
292                self.tr("""No named groups have been defined yet."""))
293            return
294
295        groupName, ok = QInputDialog.getItem(
296            self,
297            self.tr("Named reference"),
298            self.tr("Select group name:"),
299            names,
300            0, True)
301        if ok and groupName:
302            self.__insertString("(?P={0})".format(groupName))
303
304    @pyqtSlot()
305    def on_altnButton_clicked(self):
306        """
307        Private slot to handle the alternatives toolbutton.
308        """
309        self.__insertString("(|)", -2)
310
311    @pyqtSlot()
312    def on_beglineButton_clicked(self):
313        """
314        Private slot to handle the begin line toolbutton.
315        """
316        self.__insertString("^")
317
318    @pyqtSlot()
319    def on_endlineButton_clicked(self):
320        """
321        Private slot to handle the end line toolbutton.
322        """
323        self.__insertString("$")
324
325    @pyqtSlot()
326    def on_wordboundButton_clicked(self):
327        """
328        Private slot to handle the word boundary toolbutton.
329        """
330        self.__insertString("\\b")
331
332    @pyqtSlot()
333    def on_nonwordboundButton_clicked(self):
334        """
335        Private slot to handle the non word boundary toolbutton.
336        """
337        self.__insertString("\\B")
338
339    @pyqtSlot()
340    def on_poslookaheadButton_clicked(self):
341        """
342        Private slot to handle the positive lookahead toolbutton.
343        """
344        self.__insertString("(?=)", -1)
345
346    @pyqtSlot()
347    def on_neglookaheadButton_clicked(self):
348        """
349        Private slot to handle the negative lookahead toolbutton.
350        """
351        self.__insertString("(?!)", -1)
352
353    @pyqtSlot()
354    def on_poslookbehindButton_clicked(self):
355        """
356        Private slot to handle the positive lookbehind toolbutton.
357        """
358        self.__insertString("(?<=)", -1)
359
360    @pyqtSlot()
361    def on_neglookbehindButton_clicked(self):
362        """
363        Private slot to handle the negative lookbehind toolbutton.
364        """
365        self.__insertString("(?<!)", -1)
366
367    @pyqtSlot()
368    def on_undoButton_clicked(self):
369        """
370        Private slot to handle the undo action.
371        """
372        self.regexpTextEdit.document().undo()
373
374    @pyqtSlot()
375    def on_redoButton_clicked(self):
376        """
377        Private slot to handle the redo action.
378        """
379        self.regexpTextEdit.document().redo()
380
381    def on_buttonBox_clicked(self, button):
382        """
383        Private slot called by a button of the button box clicked.
384
385        @param button button that was clicked (QAbstractButton)
386        """
387        if button == self.validateButton:
388            self.on_validateButton_clicked()
389        elif button == self.executeButton:
390            self.on_executeButton_clicked()
391        elif button == self.saveButton:
392            self.on_saveButton_clicked()
393        elif button == self.loadButton:
394            self.on_loadButton_clicked()
395        elif button == self.nextButton:
396            self.on_nextButton_clicked()
397        elif self.copyButton and button == self.copyButton:
398            self.on_copyButton_clicked()
399
400    @pyqtSlot()
401    def on_saveButton_clicked(self):
402        """
403        Private slot to save the QRegularExpression to a file.
404        """
405        fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
406            self,
407            self.tr("Save regular expression"),
408            "",
409            self.tr("RegExp Files (*.rx);;All Files (*)"),
410            None,
411            E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
412        if fname:
413            ext = QFileInfo(fname).suffix()
414            if not ext:
415                ex = selectedFilter.split("(*")[1].split(")")[0]
416                if ex:
417                    fname += ex
418            if QFileInfo(fname).exists():
419                res = E5MessageBox.yesNo(
420                    self,
421                    self.tr("Save regular expression"),
422                    self.tr("<p>The file <b>{0}</b> already exists."
423                            " Overwrite it?</p>").format(fname),
424                    icon=E5MessageBox.Warning)
425                if not res:
426                    return
427
428            fname = Utilities.toNativeSeparators(fname)
429            try:
430                with open(fname, "w", encoding="utf-8") as f:
431                    f.write(self.regexpTextEdit.toPlainText())
432            except OSError as err:
433                E5MessageBox.information(
434                    self,
435                    self.tr("Save regular expression"),
436                    self.tr("""<p>The regular expression could not"""
437                            """ be saved.</p><p>Reason: {0}</p>""")
438                    .format(str(err)))
439
440    @pyqtSlot()
441    def on_loadButton_clicked(self):
442        """
443        Private slot to load a QRegularExpression from a file.
444        """
445        fname = E5FileDialog.getOpenFileName(
446            self,
447            self.tr("Load regular expression"),
448            "",
449            self.tr("RegExp Files (*.rx);;All Files (*)"))
450        if fname:
451            fname = Utilities.toNativeSeparators(fname)
452            try:
453                with open(fname, "r", encoding="utf-8") as f:
454                    regexp = f.read()
455                self.regexpTextEdit.setPlainText(regexp)
456            except OSError as err:
457                E5MessageBox.information(
458                    self,
459                    self.tr("Save regular expression"),
460                    self.tr("""<p>The regular expression could not"""
461                            """ be saved.</p><p>Reason: {0}</p>""")
462                    .format(str(err)))
463
464    @pyqtSlot()
465    def on_copyButton_clicked(self):
466        """
467        Private slot to copy the QRegularExpression string into the clipboard.
468
469        This slot is only available, if not called from within eric.
470        """
471        escaped = self.regexpTextEdit.toPlainText()
472        if escaped:
473            escaped = escaped.replace("\\", "\\\\")
474            cb = QApplication.clipboard()
475            cb.setText(escaped, QClipboard.Mode.Clipboard)
476            if cb.supportsSelection():
477                cb.setText(escaped, QClipboard.Mode.Selection)
478
479    @pyqtSlot()
480    def on_validateButton_clicked(self):
481        """
482        Private slot to validate the entered QRegularExpression.
483        """
484        if not self.__pyqt5Available:
485            # only available for PyQt5
486            return
487
488        regexp = self.regexpTextEdit.toPlainText()
489        if regexp:
490            options = []
491            if self.caseInsensitiveCheckBox.isChecked():
492                options.append("CaseInsensitiveOption")
493            if self.multilineCheckBox.isChecked():
494                options.append("MultilineOption")
495            if self.dotallCheckBox.isChecked():
496                options.append("DotMatchesEverythingOption")
497            if self.extendedCheckBox.isChecked():
498                options.append("ExtendedPatternSyntaxOption")
499            if self.greedinessCheckBox.isChecked():
500                options.append("InvertedGreedinessOption")
501            if self.unicodeCheckBox.isChecked():
502                options.append("UseUnicodePropertiesOption")
503            if self.captureCheckBox.isChecked():
504                options.append("DontCaptureOption")
505
506            if self.__sendCommand("validate", options=options, regexp=regexp):
507                response = self.__receiveResponse()
508                if response and "valid" in response:
509                    if response["valid"]:
510                        E5MessageBox.information(
511                            self,
512                            self.tr("Validation"),
513                            self.tr(
514                                """The regular expression is valid."""))
515                    else:
516                        E5MessageBox.critical(
517                            self,
518                            self.tr("Error"),
519                            self.tr("""Invalid regular expression: {0}""")
520                            .format(response["errorMessage"]))
521                        # move cursor to error offset
522                        offset = response["errorOffset"]
523                        tc = self.regexpTextEdit.textCursor()
524                        tc.setPosition(offset)
525                        self.regexpTextEdit.setTextCursor(tc)
526                        self.regexpTextEdit.setFocus()
527                        return
528                else:
529                    E5MessageBox.critical(
530                        self,
531                        self.tr("Communication Error"),
532                        self.tr("""Invalid response received from"""
533                                """ PyQt5 backend."""))
534            else:
535                E5MessageBox.critical(
536                    self,
537                    self.tr("Communication Error"),
538                    self.tr("""Communication with PyQt5 backend"""
539                            """ failed."""))
540        else:
541            E5MessageBox.critical(
542                self,
543                self.tr("Error"),
544                self.tr("""A regular expression must be given."""))
545
546    @pyqtSlot()
547    def on_executeButton_clicked(self, startpos=0):
548        """
549        Private slot to execute the entered QRegularExpression on the test
550        text.
551
552        This slot will execute the entered QRegularExpression on the entered
553        test data and will display the result in the table part of the dialog.
554
555        @param startpos starting position for the QRegularExpression matching
556        """
557        if not self.__pyqt5Available:
558            # only available for PyQt5
559            return
560
561        regexp = self.regexpTextEdit.toPlainText()
562        text = self.textTextEdit.toPlainText()
563        if regexp and text:
564            options = []
565            if self.caseInsensitiveCheckBox.isChecked():
566                options.append("CaseInsensitiveOption")
567            if self.multilineCheckBox.isChecked():
568                options.append("MultilineOption")
569            if self.dotallCheckBox.isChecked():
570                options.append("DotMatchesEverythingOption")
571            if self.extendedCheckBox.isChecked():
572                options.append("ExtendedPatternSyntaxOption")
573            if self.greedinessCheckBox.isChecked():
574                options.append("InvertedGreedinessOption")
575            if self.unicodeCheckBox.isChecked():
576                options.append("UseUnicodePropertiesOption")
577            if self.captureCheckBox.isChecked():
578                options.append("DontCaptureOption")
579
580            if self.__sendCommand("execute", options=options, regexp=regexp,
581                                  text=text, startpos=startpos):
582                response = self.__receiveResponse()
583                if response and ("valid" in response or "matched" in response):
584                    if "valid" in response:
585                        E5MessageBox.critical(
586                            self,
587                            self.tr("Error"),
588                            self.tr("""Invalid regular expression: {0}""")
589                            .format(response["errorMessage"]))
590                        # move cursor to error offset
591                        offset = response["errorOffset"]
592                        tc = self.regexpTextEdit.textCursor()
593                        tc.setPosition(offset)
594                        self.regexpTextEdit.setTextCursor(tc)
595                        self.regexpTextEdit.setFocus()
596                        return
597                    else:
598                        row = 0
599                        OFFSET = 5
600
601                        self.resultTable.setColumnCount(0)
602                        self.resultTable.setColumnCount(3)
603                        self.resultTable.setRowCount(0)
604                        self.resultTable.setRowCount(OFFSET)
605                        self.resultTable.setItem(
606                            row, 0, QTableWidgetItem(self.tr("Regexp")))
607                        self.resultTable.setItem(
608                            row, 1, QTableWidgetItem(regexp))
609                        if response["matched"]:
610                            captures = response["captures"]
611                            # index 0 is the complete match
612                            offset = captures[0][1]
613                            self.lastMatchEnd = captures[0][2]
614                            self.nextButton.setEnabled(True)
615                            row += 1
616                            self.resultTable.setItem(
617                                row, 0,
618                                QTableWidgetItem(self.tr("Offset")))
619                            self.resultTable.setItem(
620                                row, 1,
621                                QTableWidgetItem("{0:d}".format(offset)))
622
623                            row += 1
624                            self.resultTable.setItem(
625                                row, 0,
626                                QTableWidgetItem(self.tr("Captures")))
627                            self.resultTable.setItem(
628                                row, 1,
629                                QTableWidgetItem(
630                                    "{0:d}".format(len(captures) - 1)))
631                            row += 1
632                            self.resultTable.setItem(
633                                row, 1,
634                                QTableWidgetItem(self.tr("Text")))
635                            self.resultTable.setItem(
636                                row, 2,
637                                QTableWidgetItem(self.tr("Characters")))
638
639                            row += 1
640                            self.resultTable.setItem(
641                                row, 0,
642                                QTableWidgetItem(self.tr("Match")))
643                            self.resultTable.setItem(
644                                row, 1,
645                                QTableWidgetItem(captures[0][0]))
646                            self.resultTable.setItem(
647                                row, 2,
648                                QTableWidgetItem(
649                                    "{0:d}".format(captures[0][3])))
650
651                            for i in range(1, len(captures)):
652                                if captures[i][0]:
653                                    row += 1
654                                    self.resultTable.insertRow(row)
655                                    self.resultTable.setItem(
656                                        row, 0,
657                                        QTableWidgetItem(
658                                            self.tr("Capture #{0}")
659                                            .format(i)))
660                                    self.resultTable.setItem(
661                                        row, 1,
662                                        QTableWidgetItem(captures[i][0]))
663                                    self.resultTable.setItem(
664                                        row, 2,
665                                        QTableWidgetItem(
666                                            "{0:d}".format(captures[i][3])))
667
668                            # highlight the matched text
669                            tc = self.textTextEdit.textCursor()
670                            tc.setPosition(offset)
671                            tc.setPosition(
672                                self.lastMatchEnd,
673                                QTextCursor.MoveMode.KeepAnchor)
674                            self.textTextEdit.setTextCursor(tc)
675                        else:
676                            self.nextButton.setEnabled(False)
677                            self.resultTable.setRowCount(2)
678                            row += 1
679                            if startpos > 0:
680                                self.resultTable.setItem(
681                                    row, 0,
682                                    QTableWidgetItem(
683                                        self.tr("No more matches")))
684                            else:
685                                self.resultTable.setItem(
686                                    row, 0,
687                                    QTableWidgetItem(
688                                        self.tr("No matches")))
689
690                            # remove the highlight
691                            tc = self.textTextEdit.textCursor()
692                            tc.setPosition(0)
693                            self.textTextEdit.setTextCursor(tc)
694
695                        self.resultTable.resizeColumnsToContents()
696                        self.resultTable.resizeRowsToContents()
697                        self.resultTable.verticalHeader().hide()
698                        self.resultTable.horizontalHeader().hide()
699                else:
700                    E5MessageBox.critical(
701                        self,
702                        self.tr("Communication Error"),
703                        self.tr("""Invalid response received from"""
704                                """ PyQt5 backend."""))
705            else:
706                E5MessageBox.critical(
707                    self,
708                    self.tr("Communication Error"),
709                    self.tr("""Communication with PyQt5"""
710                            """ backend failed."""))
711        else:
712            E5MessageBox.critical(
713                self,
714                self.tr("Error"),
715                self.tr("""A regular expression and a text must"""
716                        """ be given."""))
717
718    @pyqtSlot()
719    def on_nextButton_clicked(self):
720        """
721        Private slot to find the next match.
722        """
723        self.on_executeButton_clicked(self.lastMatchEnd)
724
725    @pyqtSlot()
726    def on_regexpTextEdit_textChanged(self):
727        """
728        Private slot called when the regexp changes.
729        """
730        if self.nextButton:
731            self.nextButton.setEnabled(False)
732
733    def getCode(self, indLevel, indString):
734        """
735        Public method to get the source code.
736
737        @param indLevel indentation level (int)
738        @param indString string used for indentation (space or tab) (string)
739        @return generated code (string)
740        """
741        # calculate the indentation string
742        i1string = (indLevel + 1) * indString
743        estring = os.linesep + indLevel * indString
744
745        # now generate the code
746        reVar = self.variableLineEdit.text()
747        if not reVar:
748            reVar = "regexp"
749
750        regexp = self.regexpTextEdit.toPlainText()
751
752        options = []
753        if self.caseInsensitiveCheckBox.isChecked():
754            options.append(
755                "QRegularExpression.PatternOption.CaseInsensitiveOption")
756        if self.multilineCheckBox.isChecked():
757            options.append(
758                "QRegularExpression.PatternOption.MultilineOption")
759        if self.dotallCheckBox.isChecked():
760            options.append(
761                "QRegularExpression.PatternOption.DotMatchesEverythingOption")
762        if self.extendedCheckBox.isChecked():
763            options.append(
764                "QRegularExpression.PatternOption.ExtendedPatternSyntaxOption")
765        if self.greedinessCheckBox.isChecked():
766            options.append(
767                "QRegularExpression.PatternOption.InvertedGreedinessOption")
768        if self.unicodeCheckBox.isChecked():
769            options.append(
770                "QRegularExpression.PatternOption.UseUnicodePropertiesOption")
771        if self.captureCheckBox.isChecked():
772            options.append(
773                "QRegularExpression.PatternOption.DontCaptureOption")
774        options = " |{0}{1}".format(os.linesep, i1string).join(options)
775
776        code = '{0} = QRegularExpression('.format(reVar)
777        if options:
778            code += '{0}{1}r"""{2}""",'.format(
779                os.linesep, i1string, regexp.replace('"', '\\"'))
780            code += '{0}{1}{2}'.format(os.linesep, i1string, options)
781        else:
782            code += 'r"""{0}"""'.format(regexp.replace('"', '\\"'))
783        code += '){0}'.format(estring)
784        return code
785
786
787class QRegularExpressionWizardDialog(QDialog):
788    """
789    Class for the dialog variant.
790    """
791    def __init__(self, parent=None, fromEric=True):
792        """
793        Constructor
794
795        @param parent parent widget (QWidget)
796        @param fromEric flag indicating a call from within eric
797        """
798        super().__init__(parent)
799        self.setModal(fromEric)
800        self.setSizeGripEnabled(True)
801
802        self.__layout = QVBoxLayout(self)
803        self.__layout.setContentsMargins(0, 0, 0, 0)
804        self.setLayout(self.__layout)
805
806        self.cw = QRegularExpressionWizardWidget(self, fromEric)
807        size = self.cw.size()
808        self.__layout.addWidget(self.cw)
809        self.resize(size)
810        self.setWindowTitle(self.cw.windowTitle())
811
812        self.cw.buttonBox.accepted.connect(self.accept)
813        self.cw.buttonBox.rejected.connect(self.reject)
814
815    def getCode(self, indLevel, indString):
816        """
817        Public method to get the source code.
818
819        @param indLevel indentation level (int)
820        @param indString string used for indentation (space or tab) (string)
821        @return generated code (string)
822        """
823        return self.cw.getCode(indLevel, indString)
824
825    def accept(self):
826        """
827        Public slot to hide the dialog and set the result code to Accepted.
828        """
829        self.cw.shutdown()
830        super().accept()
831
832    def reject(self):
833        """
834        Public slot to hide the dialog and set the result code to Rejected.
835        """
836        self.cw.shutdown()
837        super().reject()
838
839
840class QRegularExpressionWizardWindow(E5MainWindow):
841    """
842    Main window class for the standalone dialog.
843    """
844    def __init__(self, parent=None):
845        """
846        Constructor
847
848        @param parent reference to the parent widget (QWidget)
849        """
850        super().__init__(parent)
851        self.cw = QRegularExpressionWizardWidget(self, fromEric=False)
852        size = self.cw.size()
853        self.setCentralWidget(self.cw)
854        self.resize(size)
855        self.setWindowTitle(self.cw.windowTitle())
856
857        self.setStyle(
858            Preferences.getUI("Style"), Preferences.getUI("StyleSheet"))
859
860        self.cw.buttonBox.accepted.connect(self.close)
861        self.cw.buttonBox.rejected.connect(self.close)
862
863    def closeEvent(self, evt):
864        """
865        Protected method handling the close event.
866
867        @param evt close event (QCloseEvent)
868        """
869        self.cw.shutdown()
870        super().closeEvent(evt)
871