eric7/Plugins/WizardPlugins/PyRegExpWizard/PyRegExpWizardDialog.py

branch
eric7
changeset 8312
800c432b34c8
parent 8218
7c09585bd960
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2004 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the Python re wizard dialog.
8 """
9
10 import os
11 import re
12
13 from PyQt5.QtCore import QFileInfo, pyqtSlot
14 from PyQt5.QtGui import QClipboard, QTextCursor
15 from PyQt5.QtWidgets import (
16 QWidget, QDialog, QInputDialog, QApplication, QDialogButtonBox,
17 QVBoxLayout, QTableWidgetItem
18 )
19
20 from E5Gui import E5MessageBox, E5FileDialog
21 from E5Gui.E5MainWindow import E5MainWindow
22
23 from .Ui_PyRegExpWizardDialog import Ui_PyRegExpWizardDialog
24
25 import UI.PixmapCache
26
27 import Utilities
28 import Preferences
29
30
31 class PyRegExpWizardWidget(QWidget, Ui_PyRegExpWizardDialog):
32 """
33 Class implementing the Python re wizard dialog.
34 """
35 def __init__(self, parent=None, fromEric=True):
36 """
37 Constructor
38
39 @param parent parent widget (QWidget)
40 @param fromEric flag indicating a call from within eric
41 """
42 super().__init__(parent)
43 self.setupUi(self)
44
45 # initialize icons of the tool buttons
46 self.commentButton.setIcon(UI.PixmapCache.getIcon("comment"))
47 self.charButton.setIcon(UI.PixmapCache.getIcon("characters"))
48 self.anycharButton.setIcon(UI.PixmapCache.getIcon("anychar"))
49 self.repeatButton.setIcon(UI.PixmapCache.getIcon("repeat"))
50 self.nonGroupButton.setIcon(UI.PixmapCache.getIcon("nongroup"))
51 self.groupButton.setIcon(UI.PixmapCache.getIcon("group"))
52 self.namedGroupButton.setIcon(UI.PixmapCache.getIcon("namedgroup"))
53 self.namedReferenceButton.setIcon(
54 UI.PixmapCache.getIcon("namedreference"))
55 self.altnButton.setIcon(UI.PixmapCache.getIcon("altn"))
56 self.beglineButton.setIcon(UI.PixmapCache.getIcon("begline"))
57 self.endlineButton.setIcon(UI.PixmapCache.getIcon("endline"))
58 self.wordboundButton.setIcon(
59 UI.PixmapCache.getIcon("wordboundary"))
60 self.nonwordboundButton.setIcon(
61 UI.PixmapCache.getIcon("nonwordboundary"))
62 self.poslookaheadButton.setIcon(
63 UI.PixmapCache.getIcon("poslookahead"))
64 self.neglookaheadButton.setIcon(
65 UI.PixmapCache.getIcon("neglookahead"))
66 self.poslookbehindButton.setIcon(
67 UI.PixmapCache.getIcon("poslookbehind"))
68 self.neglookbehindButton.setIcon(
69 UI.PixmapCache.getIcon("neglookbehind"))
70 self.undoButton.setIcon(UI.PixmapCache.getIcon("editUndo"))
71 self.redoButton.setIcon(UI.PixmapCache.getIcon("editRedo"))
72
73 self.namedGroups = re.compile(r"""\(?P<([^>]+)>""").findall
74
75 self.saveButton = self.buttonBox.addButton(
76 self.tr("Save"), QDialogButtonBox.ButtonRole.ActionRole)
77 self.saveButton.setToolTip(
78 self.tr("Save the regular expression to a file"))
79 self.loadButton = self.buttonBox.addButton(
80 self.tr("Load"), QDialogButtonBox.ButtonRole.ActionRole)
81 self.loadButton.setToolTip(
82 self.tr("Load a regular expression from a file"))
83 self.validateButton = self.buttonBox.addButton(
84 self.tr("Validate"), QDialogButtonBox.ButtonRole.ActionRole)
85 self.validateButton.setToolTip(
86 self.tr("Validate the regular expression"))
87 self.executeButton = self.buttonBox.addButton(
88 self.tr("Execute"), QDialogButtonBox.ButtonRole.ActionRole)
89 self.executeButton.setToolTip(
90 self.tr("Execute the regular expression"))
91 self.nextButton = self.buttonBox.addButton(
92 self.tr("Next match"), QDialogButtonBox.ButtonRole.ActionRole)
93 self.nextButton.setToolTip(
94 self.tr("Show the next match of the regular expression"))
95 self.nextButton.setEnabled(False)
96
97 if fromEric:
98 self.buttonBox.setStandardButtons(
99 QDialogButtonBox.StandardButton.Cancel |
100 QDialogButtonBox.StandardButton.Ok)
101 self.copyButton = None
102 else:
103 self.copyButton = self.buttonBox.addButton(
104 self.tr("Copy"), QDialogButtonBox.ButtonRole.ActionRole)
105 self.copyButton.setToolTip(
106 self.tr("Copy the regular expression to the clipboard"))
107 self.buttonBox.setStandardButtons(
108 QDialogButtonBox.StandardButton.Close)
109 self.variableLabel.hide()
110 self.variableLineEdit.hide()
111 self.variableLine.hide()
112 self.importCheckBox.hide()
113 self.regexpTextEdit.setFocus()
114
115 def __insertString(self, s, steps=0):
116 """
117 Private method to insert a string into line edit and move cursor.
118
119 @param s string to be inserted into the regexp line edit
120 (string)
121 @param steps number of characters to move the cursor (integer).
122 Negative steps moves cursor back, positives forward.
123 """
124 self.regexpTextEdit.insertPlainText(s)
125 tc = self.regexpTextEdit.textCursor()
126 if steps != 0:
127 if steps < 0:
128 act = QTextCursor.MoveOperation.Left
129 steps = abs(steps)
130 else:
131 act = QTextCursor.MoveOperation.Right
132 for _ in range(steps):
133 tc.movePosition(act)
134 self.regexpTextEdit.setTextCursor(tc)
135
136 @pyqtSlot()
137 def on_commentButton_clicked(self):
138 """
139 Private slot to handle the comment toolbutton.
140 """
141 self.__insertString("(?#)", -1)
142
143 @pyqtSlot()
144 def on_anycharButton_clicked(self):
145 """
146 Private slot to handle the any character toolbutton.
147 """
148 self.__insertString(".")
149
150 @pyqtSlot()
151 def on_nonGroupButton_clicked(self):
152 """
153 Private slot to handle the non group toolbutton.
154 """
155 self.__insertString("(?:)", -1)
156
157 @pyqtSlot()
158 def on_groupButton_clicked(self):
159 """
160 Private slot to handle the group toolbutton.
161 """
162 self.__insertString("()", -1)
163
164 @pyqtSlot()
165 def on_namedGroupButton_clicked(self):
166 """
167 Private slot to handle the named group toolbutton.
168 """
169 self.__insertString("(?P<>)", -2)
170
171 @pyqtSlot()
172 def on_namedReferenceButton_clicked(self):
173 """
174 Private slot to handle the named reference toolbutton.
175 """
176 # determine cursor position as length into text
177 length = self.regexpTextEdit.textCursor().position()
178
179 # only present group names that occur before the
180 # current cursor position
181 regex = self.regexpTextEdit.toPlainText()[:length]
182 names = self.namedGroups(regex)
183 if not names:
184 E5MessageBox.information(
185 self,
186 self.tr("Named reference"),
187 self.tr("""No named groups have been defined yet."""))
188 return
189
190 groupName, ok = QInputDialog.getItem(
191 self,
192 self.tr("Named reference"),
193 self.tr("Select group name:"),
194 names,
195 0, True)
196 if ok and groupName:
197 self.__insertString("(?P={0})".format(groupName))
198
199 @pyqtSlot()
200 def on_altnButton_clicked(self):
201 """
202 Private slot to handle the alternatives toolbutton.
203 """
204 self.__insertString("(|)", -2)
205
206 @pyqtSlot()
207 def on_beglineButton_clicked(self):
208 """
209 Private slot to handle the begin line toolbutton.
210 """
211 self.__insertString("^")
212
213 @pyqtSlot()
214 def on_endlineButton_clicked(self):
215 """
216 Private slot to handle the end line toolbutton.
217 """
218 self.__insertString("$")
219
220 @pyqtSlot()
221 def on_wordboundButton_clicked(self):
222 """
223 Private slot to handle the word boundary toolbutton.
224 """
225 self.__insertString("\\b")
226
227 @pyqtSlot()
228 def on_nonwordboundButton_clicked(self):
229 """
230 Private slot to handle the non word boundary toolbutton.
231 """
232 self.__insertString("\\B")
233
234 @pyqtSlot()
235 def on_poslookaheadButton_clicked(self):
236 """
237 Private slot to handle the positive lookahead toolbutton.
238 """
239 self.__insertString("(?=)", -1)
240
241 @pyqtSlot()
242 def on_neglookaheadButton_clicked(self):
243 """
244 Private slot to handle the negative lookahead toolbutton.
245 """
246 self.__insertString("(?!)", -1)
247
248 @pyqtSlot()
249 def on_poslookbehindButton_clicked(self):
250 """
251 Private slot to handle the positive lookbehind toolbutton.
252 """
253 self.__insertString("(?<=)", -1)
254
255 @pyqtSlot()
256 def on_neglookbehindButton_clicked(self):
257 """
258 Private slot to handle the negative lookbehind toolbutton.
259 """
260 self.__insertString("(?<!)", -1)
261
262 @pyqtSlot()
263 def on_repeatButton_clicked(self):
264 """
265 Private slot to handle the repeat toolbutton.
266 """
267 from .PyRegExpWizardRepeatDialog import PyRegExpWizardRepeatDialog
268 dlg = PyRegExpWizardRepeatDialog(self)
269 if dlg.exec() == QDialog.DialogCode.Accepted:
270 self.__insertString(dlg.getRepeat())
271
272 @pyqtSlot()
273 def on_charButton_clicked(self):
274 """
275 Private slot to handle the characters toolbutton.
276 """
277 from .PyRegExpWizardCharactersDialog import (
278 PyRegExpWizardCharactersDialog
279 )
280 dlg = PyRegExpWizardCharactersDialog(self)
281 if dlg.exec() == QDialog.DialogCode.Accepted:
282 self.__insertString(dlg.getCharacters())
283
284 @pyqtSlot()
285 def on_undoButton_clicked(self):
286 """
287 Private slot to handle the undo action.
288 """
289 self.regexpTextEdit.document().undo()
290
291 @pyqtSlot()
292 def on_redoButton_clicked(self):
293 """
294 Private slot to handle the redo action.
295 """
296 self.regexpTextEdit.document().redo()
297
298 def on_buttonBox_clicked(self, button):
299 """
300 Private slot called by a button of the button box clicked.
301
302 @param button button that was clicked (QAbstractButton)
303 """
304 if button == self.validateButton:
305 self.on_validateButton_clicked()
306 elif button == self.executeButton:
307 self.on_executeButton_clicked()
308 elif button == self.saveButton:
309 self.on_saveButton_clicked()
310 elif button == self.loadButton:
311 self.on_loadButton_clicked()
312 elif button == self.nextButton:
313 self.on_nextButton_clicked()
314 elif self.copyButton and button == self.copyButton:
315 self.on_copyButton_clicked()
316
317 @pyqtSlot()
318 def on_saveButton_clicked(self):
319 """
320 Private slot to save the regexp to a file.
321 """
322 fname, selectedFilter = E5FileDialog.getSaveFileNameAndFilter(
323 self,
324 self.tr("Save regular expression"),
325 "",
326 self.tr("RegExp Files (*.rx);;All Files (*)"),
327 None,
328 E5FileDialog.Options(E5FileDialog.DontConfirmOverwrite))
329 if fname:
330 ext = QFileInfo(fname).suffix()
331 if not ext:
332 ex = selectedFilter.split("(*")[1].split(")")[0]
333 if ex:
334 fname += ex
335 if QFileInfo(fname).exists():
336 res = E5MessageBox.yesNo(
337 self,
338 self.tr("Save regular expression"),
339 self.tr("<p>The file <b>{0}</b> already exists."
340 " Overwrite it?</p>").format(fname),
341 icon=E5MessageBox.Warning)
342 if not res:
343 return
344
345 fname = Utilities.toNativeSeparators(fname)
346 try:
347 with open(fname, "w", encoding="utf-8") as f:
348 f.write(self.regexpTextEdit.toPlainText())
349 except OSError as err:
350 E5MessageBox.information(
351 self,
352 self.tr("Save regular expression"),
353 self.tr("""<p>The regular expression could not"""
354 """ be saved.</p><p>Reason: {0}</p>""")
355 .format(str(err)))
356
357 @pyqtSlot()
358 def on_loadButton_clicked(self):
359 """
360 Private slot to load a regexp from a file.
361 """
362 fname = E5FileDialog.getOpenFileName(
363 self,
364 self.tr("Load regular expression"),
365 "",
366 self.tr("RegExp Files (*.rx);;All Files (*)"))
367 if fname:
368 fname = Utilities.toNativeSeparators(fname)
369 try:
370 with open(fname, "r", encoding="utf-8") as f:
371 regexp = f.read()
372 self.regexpTextEdit.setPlainText(regexp)
373 except OSError as err:
374 E5MessageBox.information(
375 self,
376 self.tr("Save regular expression"),
377 self.tr("""<p>The regular expression could not"""
378 """ be saved.</p><p>Reason: {0}</p>""")
379 .format(str(err)))
380
381 @pyqtSlot()
382 def on_copyButton_clicked(self):
383 """
384 Private slot to copy the regexp string into the clipboard.
385
386 This slot is only available, if not called from within eric.
387 """
388 escaped = self.regexpTextEdit.toPlainText()
389 if escaped:
390 escaped = escaped.replace("\\", "\\\\")
391 cb = QApplication.clipboard()
392 cb.setText(escaped, QClipboard.Mode.Clipboard)
393 if cb.supportsSelection():
394 cb.setText(escaped, QClipboard.Mode.Selection)
395
396 @pyqtSlot()
397 def on_validateButton_clicked(self):
398 """
399 Private slot to validate the entered regexp.
400 """
401 regex = self.regexpTextEdit.toPlainText()
402 if regex:
403 try:
404 flags = 0
405 if not self.caseSensitiveCheckBox.isChecked():
406 flags |= re.IGNORECASE
407 if self.multilineCheckBox.isChecked():
408 flags |= re.MULTILINE
409 if self.dotallCheckBox.isChecked():
410 flags |= re.DOTALL
411 if self.verboseCheckBox.isChecked():
412 flags |= re.VERBOSE
413 if self.unicodeCheckBox.isChecked():
414 flags |= re.ASCII
415 re.compile(regex, flags)
416 E5MessageBox.information(
417 self,
418 self.tr("Validation"),
419 self.tr("""The regular expression is valid."""))
420 except re.error as e:
421 E5MessageBox.critical(
422 self,
423 self.tr("Error"),
424 self.tr("""Invalid regular expression: {0}""")
425 .format(str(e)))
426 return
427 except IndexError:
428 E5MessageBox.critical(
429 self,
430 self.tr("Error"),
431 self.tr("""Invalid regular expression: missing"""
432 """ group name"""))
433 return
434 else:
435 E5MessageBox.critical(
436 self,
437 self.tr("Error"),
438 self.tr("""A regular expression must be given."""))
439
440 @pyqtSlot()
441 def on_executeButton_clicked(self, startpos=0):
442 """
443 Private slot to execute the entered regexp on the test text.
444
445 This slot will execute the entered regexp on the entered test
446 data and will display the result in the table part of the dialog.
447
448 @param startpos starting position for the regexp matching
449 """
450 regex = self.regexpTextEdit.toPlainText()
451 text = self.textTextEdit.toPlainText()
452 if regex and text:
453 try:
454 flags = 0
455 if not self.caseSensitiveCheckBox.isChecked():
456 flags |= re.IGNORECASE
457 if self.multilineCheckBox.isChecked():
458 flags |= re.MULTILINE
459 if self.dotallCheckBox.isChecked():
460 flags |= re.DOTALL
461 if self.verboseCheckBox.isChecked():
462 flags |= re.VERBOSE
463 if self.unicodeCheckBox.isChecked():
464 flags |= re.ASCII
465 regobj = re.compile(regex, flags)
466 matchobj = regobj.search(text, startpos)
467 if matchobj is not None:
468 captures = len(matchobj.groups())
469 if captures is None:
470 captures = 0
471 else:
472 captures = 0
473 row = 0
474 OFFSET = 5
475
476 self.resultTable.setColumnCount(0)
477 self.resultTable.setColumnCount(3)
478 self.resultTable.setRowCount(0)
479 self.resultTable.setRowCount(OFFSET)
480 self.resultTable.setItem(
481 row, 0, QTableWidgetItem(self.tr("Regexp")))
482 self.resultTable.setItem(
483 row, 1, QTableWidgetItem(regex))
484
485 if matchobj is not None:
486 offset = matchobj.start()
487 self.lastMatchEnd = matchobj.end()
488 self.nextButton.setEnabled(True)
489 row += 1
490 self.resultTable.setItem(
491 row, 0,
492 QTableWidgetItem(self.tr("Offset")))
493 self.resultTable.setItem(
494 row, 1,
495 QTableWidgetItem("{0:d}".format(matchobj.start(0))))
496
497 row += 1
498 self.resultTable.setItem(
499 row, 0,
500 QTableWidgetItem(self.tr("Captures")))
501 self.resultTable.setItem(
502 row, 1,
503 QTableWidgetItem("{0:d}".format(captures)))
504 row += 1
505 self.resultTable.setItem(
506 row, 1,
507 QTableWidgetItem(self.tr("Text")))
508 self.resultTable.setItem(
509 row, 2,
510 QTableWidgetItem(self.tr("Characters")))
511
512 row += 1
513 self.resultTable.setItem(
514 row, 0,
515 QTableWidgetItem(self.tr("Match")))
516 self.resultTable.setItem(
517 row, 1,
518 QTableWidgetItem(matchobj.group(0)))
519 self.resultTable.setItem(
520 row, 2,
521 QTableWidgetItem(
522 "{0:d}".format(len(matchobj.group(0)))))
523
524 for i in range(1, captures + 1):
525 if matchobj.group(i) is not None:
526 row += 1
527 self.resultTable.insertRow(row)
528 self.resultTable.setItem(
529 row, 0,
530 QTableWidgetItem(
531 self.tr("Capture #{0}").format(i)))
532 self.resultTable.setItem(
533 row, 1, QTableWidgetItem(matchobj.group(i)))
534 self.resultTable.setItem(
535 row, 2, QTableWidgetItem(
536 "{0:d}".format(len(matchobj.group(i)))))
537
538 # highlight the matched text
539 tc = self.textTextEdit.textCursor()
540 tc.setPosition(offset)
541 tc.setPosition(self.lastMatchEnd,
542 QTextCursor.MoveMode.KeepAnchor)
543 self.textTextEdit.setTextCursor(tc)
544 else:
545 self.nextButton.setEnabled(False)
546 self.resultTable.setRowCount(2)
547 row += 1
548 if startpos > 0:
549 self.resultTable.setItem(
550 row, 0,
551 QTableWidgetItem(self.tr("No more matches")))
552 else:
553 self.resultTable.setItem(
554 row, 0,
555 QTableWidgetItem(self.tr("No matches")))
556
557 # remove the highlight
558 tc = self.textTextEdit.textCursor()
559 tc.setPosition(0)
560 self.textTextEdit.setTextCursor(tc)
561
562 self.resultTable.resizeColumnsToContents()
563 self.resultTable.resizeRowsToContents()
564 self.resultTable.verticalHeader().hide()
565 self.resultTable.horizontalHeader().hide()
566 except re.error as e:
567 E5MessageBox.critical(
568 self,
569 self.tr("Error"),
570 self.tr("""Invalid regular expression: {0}""")
571 .format(str(e)))
572 return
573 except IndexError:
574 E5MessageBox.critical(
575 self,
576 self.tr("Error"),
577 self.tr("""Invalid regular expression: missing"""
578 """ group name"""))
579 return
580 else:
581 E5MessageBox.critical(
582 self,
583 self.tr("Error"),
584 self.tr("""A regular expression and a text must be"""
585 """ given."""))
586
587 @pyqtSlot()
588 def on_nextButton_clicked(self):
589 """
590 Private slot to find the next match.
591 """
592 self.on_executeButton_clicked(self.lastMatchEnd)
593
594 @pyqtSlot()
595 def on_regexpTextEdit_textChanged(self):
596 """
597 Private slot called when the regexp changes.
598 """
599 self.nextButton.setEnabled(False)
600
601 def getCode(self, indLevel, indString):
602 """
603 Public method to get the source code.
604
605 @param indLevel indentation level (int)
606 @param indString string used for indentation (space or tab) (string)
607 @return generated code (string)
608 """
609 # calculate the indentation string
610 istring = indLevel * indString
611 i1string = (indLevel + 1) * indString
612 estring = os.linesep + indLevel * indString
613
614 # now generate the code
615 reVar = self.variableLineEdit.text()
616 if not reVar:
617 reVar = "regexp"
618
619 regexp = self.regexpTextEdit.toPlainText()
620
621 flags = []
622 if not self.caseSensitiveCheckBox.isChecked():
623 flags.append('re.IGNORECASE')
624 if self.multilineCheckBox.isChecked():
625 flags.append('re.MULTILINE')
626 if self.dotallCheckBox.isChecked():
627 flags.append('re.DOTALL')
628 if self.verboseCheckBox.isChecked():
629 flags.append('re.VERBOSE')
630 if self.unicodeCheckBox.isChecked():
631 flags.append('re.ASCII')
632 flags = " | ".join(flags)
633
634 code = ''
635 if self.importCheckBox.isChecked():
636 code += 'import re{0}{1}'.format(os.linesep, istring)
637 code += '{0} = re.compile('.format(reVar)
638 code += '{0}{1}r"""{2}"""'.format(
639 os.linesep, i1string, regexp.replace('"', '\\"'))
640 if flags:
641 code += ',{0}{1}{2}'.format(os.linesep, i1string, flags)
642 code += '){0}'.format(estring)
643 return code
644
645
646 class PyRegExpWizardDialog(QDialog):
647 """
648 Class for the dialog variant.
649 """
650 def __init__(self, parent=None, fromEric=True):
651 """
652 Constructor
653
654 @param parent parent widget (QWidget)
655 @param fromEric flag indicating a call from within eric
656 """
657 super().__init__(parent)
658 self.setModal(fromEric)
659 self.setSizeGripEnabled(True)
660
661 self.__layout = QVBoxLayout(self)
662 self.__layout.setContentsMargins(0, 0, 0, 0)
663 self.setLayout(self.__layout)
664
665 self.cw = PyRegExpWizardWidget(self, fromEric)
666 size = self.cw.size()
667 self.__layout.addWidget(self.cw)
668 self.resize(size)
669 self.setWindowTitle(self.cw.windowTitle())
670
671 self.cw.buttonBox.accepted.connect(self.accept)
672 self.cw.buttonBox.rejected.connect(self.reject)
673
674 def getCode(self, indLevel, indString):
675 """
676 Public method to get the source code.
677
678 @param indLevel indentation level (int)
679 @param indString string used for indentation (space or tab) (string)
680 @return generated code (string)
681 """
682 return self.cw.getCode(indLevel, indString)
683
684
685 class PyRegExpWizardWindow(E5MainWindow):
686 """
687 Main window class for the standalone dialog.
688 """
689 def __init__(self, parent=None):
690 """
691 Constructor
692
693 @param parent reference to the parent widget (QWidget)
694 """
695 super().__init__(parent)
696 self.cw = PyRegExpWizardWidget(self, fromEric=False)
697 size = self.cw.size()
698 self.setCentralWidget(self.cw)
699 self.resize(size)
700 self.setWindowTitle(self.cw.windowTitle())
701
702 self.setStyle(
703 Preferences.getUI("Style"), Preferences.getUI("StyleSheet"))
704
705 self.cw.buttonBox.accepted.connect(self.close)
706 self.cw.buttonBox.rejected.connect(self.close)

eric ide

mercurial