--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric6/Plugins/WizardPlugins/QRegExpWizard/QRegExpWizardDialog.py Sun Apr 14 15:09:21 2019 +0200 @@ -0,0 +1,721 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2004 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the QRegExp wizard dialog. +""" + +from __future__ import unicode_literals + +import os + +from PyQt5.QtCore import QFileInfo, QRegExp, Qt, pyqtSlot +from PyQt5.QtGui import QClipboard, QTextCursor +from PyQt5.QtWidgets import QWidget, QDialog, QApplication, QDialogButtonBox, \ + QVBoxLayout, QTableWidgetItem + +from E5Gui import E5MessageBox, E5FileDialog +from E5Gui.E5MainWindow import E5MainWindow + +from .Ui_QRegExpWizardDialog import Ui_QRegExpWizardWidget + +import UI.PixmapCache + +import Utilities +import Preferences +from Globals import qVersionTuple + + +class QRegExpWizardWidget(QWidget, Ui_QRegExpWizardWidget): + """ + Class implementing the QRegExp wizard dialog. + """ + def __init__(self, parent=None, fromEric=True): + """ + Constructor + + @param parent parent widget (QWidget) + @param fromEric flag indicating a call from within eric6 + """ + super(QRegExpWizardWidget, self).__init__(parent) + self.setupUi(self) + + # initialize icons of the tool buttons + # regexp tool buttons + self.charButton.setIcon(UI.PixmapCache.getIcon("characters.png")) + self.anycharButton.setIcon(UI.PixmapCache.getIcon("anychar.png")) + self.repeatButton.setIcon(UI.PixmapCache.getIcon("repeat.png")) + self.nonGroupButton.setIcon(UI.PixmapCache.getIcon("nongroup.png")) + self.groupButton.setIcon(UI.PixmapCache.getIcon("group.png")) + self.altnButton.setIcon(UI.PixmapCache.getIcon("altn.png")) + self.beglineButton.setIcon(UI.PixmapCache.getIcon("begline.png")) + self.endlineButton.setIcon(UI.PixmapCache.getIcon("endline.png")) + self.wordboundButton.setIcon( + UI.PixmapCache.getIcon("wordboundary.png")) + self.nonwordboundButton.setIcon( + UI.PixmapCache.getIcon("nonwordboundary.png")) + self.poslookaheadButton.setIcon( + UI.PixmapCache.getIcon("poslookahead.png")) + self.neglookaheadButton.setIcon( + UI.PixmapCache.getIcon("neglookahead.png")) + self.undoButton.setIcon(UI.PixmapCache.getIcon("editUndo.png")) + self.redoButton.setIcon(UI.PixmapCache.getIcon("editRedo.png")) + # wildcard tool buttons + self.wildcardCharButton.setIcon( + UI.PixmapCache.getIcon("characters.png")) + self.wildcardAnycharButton.setIcon( + UI.PixmapCache.getIcon("anychar.png")) + self.wildcardRepeatButton.setIcon(UI.PixmapCache.getIcon("repeat.png")) + # W3C tool buttons + self.w3cCharButton.setIcon(UI.PixmapCache.getIcon("characters.png")) + self.w3cAnycharButton.setIcon(UI.PixmapCache.getIcon("anychar.png")) + self.w3cRepeatButton.setIcon(UI.PixmapCache.getIcon("repeat.png")) + self.w3cGroupButton.setIcon(UI.PixmapCache.getIcon("group.png")) + self.w3cAltnButton.setIcon(UI.PixmapCache.getIcon("altn.png")) + + # initialize the syntax pattern combo + self.syntaxCombo.addItem("RegExp", QRegExp.RegExp) + self.syntaxCombo.addItem("RegExp2", QRegExp.RegExp2) + self.syntaxCombo.addItem("Wildcard", QRegExp.Wildcard) + self.syntaxCombo.addItem("Unix Wildcard", QRegExp.WildcardUnix) + self.syntaxCombo.addItem("Fixed String", QRegExp.FixedString) + self.syntaxCombo.addItem("W3C XML Schema 1.1", QRegExp.W3CXmlSchema11) + if qVersionTuple() >= (5, 0, 0): + self.syntaxCombo.setCurrentIndex(1) + + self.saveButton = self.buttonBox.addButton( + self.tr("Save"), QDialogButtonBox.ActionRole) + self.saveButton.setToolTip( + self.tr("Save the regular expression to a file")) + self.loadButton = self.buttonBox.addButton( + self.tr("Load"), QDialogButtonBox.ActionRole) + self.loadButton.setToolTip( + self.tr("Load a regular expression from a file")) + self.validateButton = self.buttonBox.addButton( + self.tr("Validate"), QDialogButtonBox.ActionRole) + self.validateButton.setToolTip( + self.tr("Validate the regular expression")) + self.executeButton = self.buttonBox.addButton( + self.tr("Execute"), QDialogButtonBox.ActionRole) + self.executeButton.setToolTip( + self.tr("Execute the regular expression")) + self.nextButton = self.buttonBox.addButton( + self.tr("Next match"), QDialogButtonBox.ActionRole) + self.nextButton.setToolTip( + self.tr("Show the next match of the regular expression")) + self.nextButton.setEnabled(False) + + if fromEric: + self.buttonBox.setStandardButtons( + QDialogButtonBox.Cancel | QDialogButtonBox.Ok) + self.copyButton = None + else: + self.copyButton = self.buttonBox.addButton( + self.tr("Copy"), QDialogButtonBox.ActionRole) + self.copyButton.setToolTip( + self.tr("Copy the regular expression to the clipboard")) + self.buttonBox.setStandardButtons(QDialogButtonBox.Close) + self.variableLabel.hide() + self.variableLineEdit.hide() + self.variableLine.hide() + self.regexpLineEdit.setFocus() + + @pyqtSlot(int) + def on_syntaxCombo_currentIndexChanged(self, index): + """ + Private slot handling the selection of a pattern syntax. + + @param index index of the selected entry (integer) + """ + syntax = self.syntaxCombo.itemData(index) + self.regexpButtonsFrame.setVisible(syntax in [ + QRegExp.RegExp, QRegExp.RegExp2]) + self.regexpButtonsFrame.setEnabled(syntax in [ + QRegExp.RegExp, QRegExp.RegExp2]) + self.wildcardButtonsFrame.setVisible(syntax in [ + QRegExp.Wildcard, QRegExp.WildcardUnix]) + self.wildcardButtonsFrame.setEnabled(syntax in [ + QRegExp.Wildcard, QRegExp.WildcardUnix]) + self.w3cButtonsFrame.setVisible(syntax in [QRegExp.W3CXmlSchema11]) + self.w3cButtonsFrame.setEnabled(syntax in [QRegExp.W3CXmlSchema11]) + + def __insertString(self, s, steps=0): + """ + Private method to insert a string into line edit and move cursor. + + @param s string to be inserted into the regexp line edit + (string) + @param steps number of characters to move the cursor (integer). + Negative steps moves cursor back, positives forward. + """ + self.regexpLineEdit.insert(s) + self.regexpLineEdit.cursorForward(False, steps) + + @pyqtSlot() + def on_anycharButton_clicked(self): + """ + Private slot to handle the any character toolbutton. + """ + self.__insertString(".") + + @pyqtSlot() + def on_nonGroupButton_clicked(self): + """ + Private slot to handle the non group toolbutton. + """ + self.__insertString("(?:)", -1) + + @pyqtSlot() + def on_groupButton_clicked(self): + """ + Private slot to handle the group toolbutton. + """ + self.__insertString("()", -1) + + @pyqtSlot() + def on_altnButton_clicked(self): + """ + Private slot to handle the alternatives toolbutton. + """ + self.__insertString("(|)", -2) + + @pyqtSlot() + def on_beglineButton_clicked(self): + """ + Private slot to handle the begin line toolbutton. + """ + self.__insertString("^") + + @pyqtSlot() + def on_endlineButton_clicked(self): + """ + Private slot to handle the end line toolbutton. + """ + self.__insertString("$") + + @pyqtSlot() + def on_wordboundButton_clicked(self): + """ + Private slot to handle the word boundary toolbutton. + """ + self.__insertString("\\b") + + @pyqtSlot() + def on_nonwordboundButton_clicked(self): + """ + Private slot to handle the non word boundary toolbutton. + """ + self.__insertString("\\B") + + @pyqtSlot() + def on_poslookaheadButton_clicked(self): + """ + Private slot to handle the positive lookahead toolbutton. + """ + self.__insertString("(?=)", -1) + + @pyqtSlot() + def on_neglookaheadButton_clicked(self): + """ + Private slot to handle the negative lookahead toolbutton. + """ + self.__insertString("(?!)", -1) + + @pyqtSlot() + def on_repeatButton_clicked(self): + """ + Private slot to handle the repeat toolbutton. + """ + from .QRegExpWizardRepeatDialog import QRegExpWizardRepeatDialog + dlg = QRegExpWizardRepeatDialog(self) + if dlg.exec_() == QDialog.Accepted: + self.__insertString(dlg.getRepeat()) + + @pyqtSlot() + def on_charButton_clicked(self): + """ + Private slot to handle the characters toolbutton. + """ + from .QRegExpWizardCharactersDialog import \ + QRegExpWizardCharactersDialog + dlg = QRegExpWizardCharactersDialog( + mode=QRegExpWizardCharactersDialog.RegExpMode, parent=self) + if dlg.exec_() == QDialog.Accepted: + self.__insertString(dlg.getCharacters()) + + @pyqtSlot() + def on_wildcardCharButton_clicked(self): + """ + Private slot to handle the wildcard characters toolbutton. + """ + from .QRegExpWizardCharactersDialog import \ + QRegExpWizardCharactersDialog + dlg = QRegExpWizardCharactersDialog( + mode=QRegExpWizardCharactersDialog.WildcardMode, parent=self) + if dlg.exec_() == QDialog.Accepted: + self.__insertString(dlg.getCharacters()) + + @pyqtSlot() + def on_wildcardAnycharButton_clicked(self): + """ + Private slot to handle the wildcard any character toolbutton. + """ + self.__insertString("?") + + @pyqtSlot() + def on_wildcardRepeatButton_clicked(self): + """ + Private slot to handle the wildcard multiple characters toolbutton. + """ + self.__insertString("*") + + @pyqtSlot() + def on_w3cCharButton_clicked(self): + """ + Private slot to handle the wildcard characters toolbutton. + """ + from .QRegExpWizardCharactersDialog import \ + QRegExpWizardCharactersDialog + dlg = QRegExpWizardCharactersDialog( + mode=QRegExpWizardCharactersDialog.W3CMode, parent=self) + if dlg.exec_() == QDialog.Accepted: + self.__insertString(dlg.getCharacters()) + + @pyqtSlot() + def on_w3cAnycharButton_clicked(self): + """ + Private slot to handle the W3C any character toolbutton. + """ + self.__insertString(".") + + @pyqtSlot() + def on_w3cRepeatButton_clicked(self): + """ + Private slot to handle the W3C repeat toolbutton. + """ + from .QRegExpWizardRepeatDialog import QRegExpWizardRepeatDialog + dlg = QRegExpWizardRepeatDialog(self) + if dlg.exec_() == QDialog.Accepted: + self.__insertString(dlg.getRepeat()) + + @pyqtSlot() + def on_w3cGroupButton_clicked(self): + """ + Private slot to handle the W3C group toolbutton. + """ + self.__insertString("()", -1) + + @pyqtSlot() + def on_w3cAltnButton_clicked(self): + """ + Private slot to handle the alternatives toolbutton. + """ + self.__insertString("(|)", -2) + + def on_buttonBox_clicked(self, button): + """ + Private slot called by a button of the button box clicked. + + @param button button that was clicked (QAbstractButton) + """ + if button == self.validateButton: + self.on_validateButton_clicked() + elif button == self.executeButton: + self.on_executeButton_clicked() + elif button == self.saveButton: + self.on_saveButton_clicked() + elif button == self.loadButton: + self.on_loadButton_clicked() + elif button == self.nextButton: + self.on_nextButton_clicked() + elif self.copyButton and button == self.copyButton: + self.on_copyButton_clicked() + + @pyqtSlot() + def on_saveButton_clicked(self): + """ + Private slot to save the regexp to a file. + """ + fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter( + self, + self.tr("Save regular expression"), + "", + self.tr("RegExp Files (*.rx);;All Files (*)"), + None, + E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite)) + if fname: + ext = QFileInfo(fname).suffix() + if not ext: + ex = selectedFilter.split("(*")[1].split(")")[0] + if ex: + fname += ex + if QFileInfo(fname).exists(): + res = E5MessageBox.yesNo( + self, + self.tr("Save regular expression"), + self.tr("<p>The file <b>{0}</b> already exists." + " Overwrite it?</p>").format(fname), + icon=E5MessageBox.Warning) + if not res: + return + + syntax = self.syntaxCombo.itemData(self.syntaxCombo.currentIndex()) + try: + f = open( + Utilities.toNativeSeparators(fname), "w", encoding="utf-8") + f.write("syntax={0}\n".format(syntax)) + f.write(self.regexpLineEdit.text()) + f.close() + except IOError as err: + E5MessageBox.information( + self, + self.tr("Save regular expression"), + self.tr("""<p>The regular expression could not""" + """ be saved.</p><p>Reason: {0}</p>""") + .format(str(err))) + + @pyqtSlot() + def on_loadButton_clicked(self): + """ + Private slot to load a regexp from a file. + """ + fname = E5FileDialog.getOpenFileName( + self, + self.tr("Load regular expression"), + "", + self.tr("RegExp Files (*.rx);;All Files (*)")) + if fname: + try: + f = open( + Utilities.toNativeSeparators(fname), "r", encoding="utf-8") + regexp = f.read() + f.close() + if regexp.startswith("syntax="): + lines = regexp.splitlines() + syntax = int(lines[0].replace("syntax=", "")) + index = self.syntaxCombo.findData(syntax) + self.syntaxCombo.setCurrentIndex(index) + regexp = lines[1] + self.regexpLineEdit.setText(regexp) + except IOError as err: + E5MessageBox.information( + self, + self.tr("Save regular expression"), + self.tr("""<p>The regular expression could not""" + """ be saved.</p><p>Reason: {0}</p>""") + .format(str(err))) + + @pyqtSlot() + def on_copyButton_clicked(self): + """ + Private slot to copy the regexp string into the clipboard. + + This slot is only available, if not called from within eric6. + """ + escaped = self.regexpLineEdit.text() + if escaped: + escaped = escaped.replace("\\", "\\\\") + cb = QApplication.clipboard() + cb.setText(escaped, QClipboard.Clipboard) + if cb.supportsSelection(): + cb.setText(escaped, QClipboard.Selection) + + @pyqtSlot() + def on_validateButton_clicked(self): + """ + Private slot to validate the entered regexp. + """ + regex = self.regexpLineEdit.text() + if regex: + re = QRegExp(regex) + if self.caseSensitiveCheckBox.isChecked(): + re.setCaseSensitivity(Qt.CaseSensitive) + else: + re.setCaseSensitivity(Qt.CaseInsensitive) + re.setMinimal(self.minimalCheckBox.isChecked()) + re.setPatternSyntax( + self.syntaxCombo.itemData(self.syntaxCombo.currentIndex())) + if re.isValid(): + E5MessageBox.information( + self, + self.tr("Validation"), + self.tr("""The regular expression is valid.""")) + else: + E5MessageBox.critical( + self, + self.tr("Error"), + self.tr("""Invalid regular expression: {0}""") + .format(re.errorString())) + return + else: + E5MessageBox.critical( + self, + self.tr("Error"), + self.tr("""A regular expression must be given.""")) + + @pyqtSlot() + def on_executeButton_clicked(self, startpos=0): + """ + Private slot to execute the entered regexp on the test text. + + This slot will execute the entered regexp on the entered test + data and will display the result in the table part of the dialog. + + @param startpos starting position for the regexp matching + """ + regex = self.regexpLineEdit.text() + text = self.textTextEdit.toPlainText() + if regex and text: + re = QRegExp(regex) + if self.caseSensitiveCheckBox.isChecked(): + re.setCaseSensitivity(Qt.CaseSensitive) + else: + re.setCaseSensitivity(Qt.CaseInsensitive) + re.setMinimal(self.minimalCheckBox.isChecked()) + syntax = self.syntaxCombo.itemData(self.syntaxCombo.currentIndex()) + wildcard = syntax in [QRegExp.Wildcard, QRegExp.WildcardUnix] + re.setPatternSyntax(syntax) + if not re.isValid(): + E5MessageBox.critical( + self, + self.tr("Error"), + self.tr("""Invalid regular expression: {0}""") + .format(re.errorString())) + return + offset = re.indexIn(text, startpos) + captures = re.captureCount() + row = 0 + OFFSET = 5 + + self.resultTable.setColumnCount(0) + self.resultTable.setColumnCount(3) + self.resultTable.setRowCount(0) + self.resultTable.setRowCount(OFFSET) + self.resultTable.setItem( + row, 0, QTableWidgetItem(self.tr("Regexp"))) + self.resultTable.setItem(row, 1, QTableWidgetItem(regex)) + + if offset != -1: + self.lastMatchEnd = offset + re.matchedLength() + self.nextButton.setEnabled(True) + row += 1 + self.resultTable.setItem( + row, 0, QTableWidgetItem(self.tr("Offset"))) + self.resultTable.setItem( + row, 1, QTableWidgetItem("{0:d}".format(offset))) + + if not wildcard: + row += 1 + self.resultTable.setItem( + row, 0, QTableWidgetItem(self.tr("Captures"))) + self.resultTable.setItem( + row, 1, QTableWidgetItem("{0:d}".format(captures))) + row += 1 + self.resultTable.setItem( + row, 1, QTableWidgetItem(self.tr("Text"))) + self.resultTable.setItem( + row, 2, QTableWidgetItem(self.tr("Characters"))) + + row += 1 + self.resultTable.setItem( + row, 0, QTableWidgetItem(self.tr("Match"))) + self.resultTable.setItem( + row, 1, QTableWidgetItem(re.cap(0))) + self.resultTable.setItem( + row, 2, + QTableWidgetItem("{0:d}".format(re.matchedLength()))) + + if not wildcard: + for i in range(1, captures + 1): + if len(re.cap(i)) > 0: + row += 1 + self.resultTable.insertRow(row) + self.resultTable.setItem( + row, 0, + QTableWidgetItem( + self.tr("Capture #{0}").format(i))) + self.resultTable.setItem( + row, 1, + QTableWidgetItem(re.cap(i))) + self.resultTable.setItem( + row, 2, + QTableWidgetItem( + "{0:d}".format(len(re.cap(i))))) + else: + self.resultTable.setRowCount(3) + + # highlight the matched text + tc = self.textTextEdit.textCursor() + tc.setPosition(offset) + tc.setPosition(self.lastMatchEnd, QTextCursor.KeepAnchor) + self.textTextEdit.setTextCursor(tc) + else: + self.nextButton.setEnabled(False) + self.resultTable.setRowCount(2) + row += 1 + if startpos > 0: + self.resultTable.setItem( + row, 0, + QTableWidgetItem(self.tr("No more matches"))) + else: + self.resultTable.setItem( + row, 0, + QTableWidgetItem(self.tr("No matches"))) + + # remove the highlight + tc = self.textTextEdit.textCursor() + tc.setPosition(0) + self.textTextEdit.setTextCursor(tc) + + self.resultTable.resizeColumnsToContents() + self.resultTable.resizeRowsToContents() + self.resultTable.verticalHeader().hide() + self.resultTable.horizontalHeader().hide() + else: + E5MessageBox.critical( + self, + self.tr("Error"), + self.tr("""A regular expression and a text must""" + """ be given.""")) + + @pyqtSlot() + def on_nextButton_clicked(self): + """ + Private slot to find the next match. + """ + self.on_executeButton_clicked(self.lastMatchEnd) + + def on_regexpLineEdit_textChanged(self, txt): + """ + Private slot called when the regexp changes. + + @param txt the new text of the line edit (string) + """ + self.nextButton.setEnabled(False) + + def __getPatternSyntaxCode(self, syntaxValue): + """ + Private method to convert a pattern syntax value into a + pattern syntax string. + + @param syntaxValue pattern syntax value (integer) + @return pattern syntax string (string) + """ + syntax = "QRegExp." + if syntaxValue == QRegExp.RegExp: + syntax += "RegExp" + elif syntaxValue == QRegExp.RegExp2: + syntax += "RegExp2" + elif syntaxValue == QRegExp.Wildcard: + syntax += "Wildcard" + elif syntaxValue == QRegExp.WildcardUnix: + syntax += "WildcardUnix" + elif syntaxValue == QRegExp.FixedString: + syntax += "FixedString" + elif syntaxValue == QRegExp.W3CXmlSchema11: + syntax += "W3CXmlSchema11" + return syntax + + def getCode(self, indLevel, indString): + """ + Public method to get the source code. + + @param indLevel indentation level (int) + @param indString string used for indentation (space or tab) (string) + @return generated code (string) + """ + # calculate the indentation string + istring = indLevel * indString + estring = os.linesep + indLevel * indString + + # now generate the code + reVar = self.variableLineEdit.text() + if not reVar: + reVar = "regexp" + + regexp = self.regexpLineEdit.text() + + code = '{0} = QRegExp(r"""{1}"""){2}'.format( + reVar, regexp.replace('"', '\\"'), os.linesep) + if not self.caseSensitiveCheckBox.isChecked(): + code += '{0}{1}.setCaseSensitivity(Qt.CaseInsensitive){2}'.format( + istring, reVar, os.linesep) + if self.minimalCheckBox.isChecked(): + code += '{0}{1}.setMinimal(True){2}'.format( + istring, reVar, os.linesep) + syntax = self.syntaxCombo.itemData(self.syntaxCombo.currentIndex()) + needPatternSyntax = True + if qVersionTuple() < (5, 0, 0) and syntax == QRegExp.RegExp or \ + qVersionTuple() >= (5, 0, 0) and syntax == QRegExp.RegExp2: + # default value selected + needPatternSyntax = False + if needPatternSyntax: + code += '{0}{1}.setPatternSyntax({2}){3}'.format( + istring, reVar, self.__getPatternSyntaxCode(syntax), + estring) + return code + + +class QRegExpWizardDialog(QDialog): + """ + Class for the dialog variant. + """ + def __init__(self, parent=None, fromEric=True): + """ + Constructor + + @param parent parent widget (QWidget) + @param fromEric flag indicating a call from within eric6 + """ + super(QRegExpWizardDialog, self).__init__(parent) + self.setModal(fromEric) + self.setSizeGripEnabled(True) + + self.__layout = QVBoxLayout(self) + self.__layout.setContentsMargins(0, 0, 0, 0) + self.setLayout(self.__layout) + + self.cw = QRegExpWizardWidget(self, fromEric) + size = self.cw.size() + self.__layout.addWidget(self.cw) + self.resize(size) + self.setWindowTitle(self.cw.windowTitle()) + + self.cw.buttonBox.accepted.connect(self.accept) + self.cw.buttonBox.rejected.connect(self.reject) + + def getCode(self, indLevel, indString): + """ + Public method to get the source code. + + @param indLevel indentation level (int) + @param indString string used for indentation (space or tab) (string) + @return generated code (string) + """ + return self.cw.getCode(indLevel, indString) + + +class QRegExpWizardWindow(E5MainWindow): + """ + Main window class for the standalone dialog. + """ + def __init__(self, parent=None): + """ + Constructor + + @param parent reference to the parent widget (QWidget) + """ + super(QRegExpWizardWindow, self).__init__(parent) + self.cw = QRegExpWizardWidget(self, fromEric=False) + size = self.cw.size() + self.setCentralWidget(self.cw) + self.resize(size) + self.setWindowTitle(self.cw.windowTitle()) + + self.setStyle( + Preferences.getUI("Style"), Preferences.getUI("StyleSheet")) + + self.cw.buttonBox.accepted.connect(self.close) + self.cw.buttonBox.rejected.connect(self.close)