src/eric7/HexEdit/HexEditSearchReplaceWidget.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2016 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a search and replace widget for the hex editor.
8 """
9
10 from PyQt6.QtCore import pyqtSlot, Qt, QByteArray, QRegularExpression
11 from PyQt6.QtGui import QRegularExpressionValidator
12 from PyQt6.QtWidgets import QWidget
13
14 from EricGui.EricAction import EricAction
15 from EricWidgets import EricMessageBox
16
17 import UI.PixmapCache
18
19
20 class HexEditSearchReplaceWidget(QWidget):
21 """
22 Class implementing a search and replace widget for the hex editor.
23 """
24 def __init__(self, editor, mainWindow, replace=False, parent=None):
25 """
26 Constructor
27
28 @param editor reference to the hex editor widget
29 @type HexEditWidget
30 @param mainWindow reference to the main window
31 @type HexEditMainWindow
32 @param replace flag indicating a replace widget
33 @type bool
34 @param parent reference to the parent widget
35 @type QWidget
36 """
37 super().__init__(parent)
38
39 self.__replace = replace
40 self.__editor = editor
41
42 # keep this in sync with the logic in __getContent()
43 self.__formatAndValidators = {
44 "hex": (self.tr("Hex"), QRegularExpressionValidator(
45 QRegularExpression("[0-9a-f]*"))),
46 "dec": (self.tr("Dec"), QRegularExpressionValidator(
47 QRegularExpression("[0-9]*"))),
48 "oct": (self.tr("Oct"), QRegularExpressionValidator(
49 QRegularExpression("[0-7]*"))),
50 "bin": (self.tr("Bin"), QRegularExpressionValidator(
51 QRegularExpression("[01]*"))),
52 "iso-8859-1": (self.tr("Text"), None),
53 # text as latin-1/iso-8859-1
54 "utf-8": (self.tr("UTF-8"), None),
55 # text as utf-8
56 }
57 formatOrder = ["hex", "dec", "oct", "bin", "iso-8859-1", "utf-8"]
58
59 self.__currentFindFormat = ""
60 self.__currentReplaceFormat = ""
61
62 self.__findHistory = mainWindow.getSRHistory("search")
63 if replace:
64 from .Ui_HexEditReplaceWidget import Ui_HexEditReplaceWidget
65 self.__replaceHistory = mainWindow.getSRHistory("replace")
66 self.__ui = Ui_HexEditReplaceWidget()
67 else:
68 from .Ui_HexEditSearchWidget import Ui_HexEditSearchWidget
69 self.__ui = Ui_HexEditSearchWidget()
70 self.__ui.setupUi(self)
71
72 self.__ui.closeButton.setIcon(UI.PixmapCache.getIcon("close"))
73 self.__ui.findPrevButton.setIcon(
74 UI.PixmapCache.getIcon("1leftarrow"))
75 self.__ui.findNextButton.setIcon(
76 UI.PixmapCache.getIcon("1rightarrow"))
77
78 if replace:
79 self.__ui.replaceButton.setIcon(
80 UI.PixmapCache.getIcon("editReplace"))
81 self.__ui.replaceSearchButton.setIcon(
82 UI.PixmapCache.getIcon("editReplaceSearch"))
83 self.__ui.replaceAllButton.setIcon(
84 UI.PixmapCache.getIcon("editReplaceAll"))
85
86 for dataFormat in formatOrder:
87 formatStr, validator = self.__formatAndValidators[dataFormat]
88 self.__ui.findFormatCombo.addItem(formatStr, dataFormat)
89 if replace:
90 for dataFormat in formatOrder:
91 formatStr, validator = self.__formatAndValidators[dataFormat]
92 self.__ui.replaceFormatCombo.addItem(formatStr, dataFormat)
93
94 self.__ui.findtextCombo.setCompleter(None)
95 self.__ui.findtextCombo.lineEdit().returnPressed.connect(
96 self.__findByReturnPressed)
97 if replace:
98 self.__ui.replacetextCombo.setCompleter(None)
99 self.__ui.replacetextCombo.lineEdit().returnPressed.connect(
100 self.on_replaceButton_clicked)
101
102 self.findNextAct = EricAction(
103 self.tr('Find Next'),
104 self.tr('Find Next'),
105 0, 0, self, 'hexEditor_search_widget_find_next')
106 self.findNextAct.triggered.connect(self.on_findNextButton_clicked)
107 self.findNextAct.setEnabled(False)
108 self.__ui.findtextCombo.addAction(self.findNextAct)
109
110 self.findPrevAct = EricAction(
111 self.tr('Find Prev'),
112 self.tr('Find Prev'),
113 0, 0, self, 'hexEditor_search_widget_find_prev')
114 self.findPrevAct.triggered.connect(self.on_findPrevButton_clicked)
115 self.findPrevAct.setEnabled(False)
116 self.__ui.findtextCombo.addAction(self.findPrevAct)
117
118 self.__havefound = False
119
120 @pyqtSlot(int)
121 def on_findFormatCombo_currentIndexChanged(self, idx):
122 """
123 Private slot to handle a selection from the find format.
124
125 @param idx index of the selected entry
126 @type int
127 """
128 if idx >= 0:
129 findFormat = self.__ui.findFormatCombo.itemData(idx)
130
131 if findFormat != self.__currentFindFormat:
132 txt = self.__ui.findtextCombo.currentText()
133 newTxt = self.__convertText(
134 txt, self.__currentFindFormat, findFormat)
135 self.__currentFindFormat = findFormat
136
137 self.__ui.findtextCombo.setValidator(
138 self.__formatAndValidators[findFormat][1])
139
140 self.__ui.findtextCombo.setEditText(newTxt)
141
142 @pyqtSlot(str)
143 def on_findtextCombo_editTextChanged(self, txt):
144 """
145 Private slot to enable/disable the find buttons.
146
147 @param txt text of the find text combo
148 @type str
149 """
150 if not txt:
151 self.__ui.findNextButton.setEnabled(False)
152 self.findNextAct.setEnabled(False)
153 self.__ui.findPrevButton.setEnabled(False)
154 self.findPrevAct.setEnabled(False)
155 if self.__replace:
156 self.__ui.replaceButton.setEnabled(False)
157 self.__ui.replaceSearchButton.setEnabled(False)
158 self.__ui.replaceAllButton.setEnabled(False)
159 else:
160 self.__ui.findNextButton.setEnabled(True)
161 self.findNextAct.setEnabled(True)
162 self.__ui.findPrevButton.setEnabled(True)
163 self.findPrevAct.setEnabled(True)
164 if self.__replace:
165 self.__ui.replaceButton.setEnabled(False)
166 self.__ui.replaceSearchButton.setEnabled(False)
167 self.__ui.replaceAllButton.setEnabled(True)
168
169 @pyqtSlot(int)
170 def on_findtextCombo_activated(self, idx):
171 """
172 Private slot to handle a selection from the find history.
173
174 @param idx index of the selected entry
175 @type int
176 """
177 if idx >= 0:
178 formatIndex = self.__ui.findtextCombo.itemData(idx)
179 if formatIndex is not None:
180 self.__ui.findFormatCombo.setCurrentIndex(formatIndex)
181
182 def __getContent(self, replace=False):
183 """
184 Private method to get the contents of the find/replace combo as
185 a bytearray.
186
187 @param replace flag indicating to retrieve the replace contents
188 @type bool
189 @return search or replace term as text and binary data
190 @rtype tuple of bytearray and str
191 """
192 if replace:
193 textCombo = self.__ui.replacetextCombo
194 formatCombo = self.__ui.replaceFormatCombo
195 history = self.__replaceHistory
196 else:
197 textCombo = self.__ui.findtextCombo
198 formatCombo = self.__ui.findFormatCombo
199 history = self.__findHistory
200
201 txt = textCombo.currentText()
202 idx = formatCombo.currentIndex()
203 findFormat = formatCombo.itemData(idx)
204 ba = self.__text2bytearray(txt, findFormat)
205
206 # This moves any previous occurrence of this statement to the head
207 # of the list and updates the combobox
208 historyEntry = (idx, txt)
209 if historyEntry in history:
210 history.remove(historyEntry)
211 history.insert(0, historyEntry)
212 textCombo.clear()
213 for index, text in history:
214 textCombo.addItem(text, index)
215
216 return ba, txt
217
218 @pyqtSlot()
219 def on_findNextButton_clicked(self):
220 """
221 Private slot to find the next occurrence.
222 """
223 self.findPrevNext(False)
224
225 @pyqtSlot()
226 def on_findPrevButton_clicked(self):
227 """
228 Private slot to find the previous occurrence.
229 """
230 self.findPrevNext(True)
231
232 def findPrevNext(self, prev=False):
233 """
234 Public slot to find the next occurrence of the search term.
235
236 @param prev flag indicating a backwards search
237 @type bool
238 @return flag indicating a successful search
239 @rtype bool
240 """
241 if not self.__havefound or not self.__ui.findtextCombo.currentText():
242 self.show()
243 return False
244
245 self.__findBackwards = prev
246 ba, txt = self.__getContent()
247
248 idx = -1
249 if len(ba) > 0:
250 startIndex = self.__editor.cursorPosition() // 2
251 if prev:
252 if (
253 self.__editor.hasSelection() and
254 startIndex == self.__editor.getSelectionEnd()
255 ):
256 # skip to the selection start
257 startIndex = self.__editor.getSelectionBegin()
258 idx = self.__editor.lastIndexOf(ba, startIndex)
259 else:
260 if (
261 self.__editor.hasSelection() and
262 startIndex == self.__editor.getSelectionBegin() - 1
263 ):
264 # skip to the selection end
265 startIndex = self.__editor.getSelectionEnd()
266 idx = self.__editor.indexOf(ba, startIndex)
267
268 if idx >= 0:
269 if self.__replace:
270 self.__ui.replaceButton.setEnabled(True)
271 self.__ui.replaceSearchButton.setEnabled(True)
272 else:
273 EricMessageBox.information(
274 self, self.windowTitle(),
275 self.tr("'{0}' was not found.").format(txt))
276
277 return idx >= 0
278
279 def __findByReturnPressed(self):
280 """
281 Private slot to handle a return pressed in the find combo.
282 """
283 if self.__findBackwards:
284 self.findPrevNext(True)
285 else:
286 self.findPrevNext(False)
287
288 @pyqtSlot(int)
289 def on_replaceFormatCombo_currentIndexChanged(self, idx):
290 """
291 Private slot to handle a selection from the replace format.
292
293 @param idx index of the selected entry
294 @type int
295 """
296 if idx >= 0:
297 replaceFormat = self.__ui.replaceFormatCombo.itemData(idx)
298
299 if replaceFormat != self.__currentReplaceFormat:
300 txt = self.__ui.replacetextCombo.currentText()
301 newTxt = self.__convertText(
302 txt, self.__currentReplaceFormat, replaceFormat)
303 self.__currentReplaceFormat = replaceFormat
304
305 self.__ui.replacetextCombo.setValidator(
306 self.__formatAndValidators[replaceFormat][1])
307
308 self.__ui.replacetextCombo.setEditText(newTxt)
309
310 @pyqtSlot(int)
311 def on_replacetextCombo_activated(self, idx):
312 """
313 Private slot to handle a selection from the replace history.
314
315 @param idx index of the selected entry
316 @type int
317 """
318 if idx >= 0:
319 formatIndex = self.__ui.replacetextCombo.itemData(idx)
320 if formatIndex is not None:
321 self.__ui.replaceFormatCombo.setCurrentIndex(formatIndex)
322
323 @pyqtSlot()
324 def on_replaceButton_clicked(self):
325 """
326 Private slot to replace one occurrence of data.
327 """
328 self.__doReplace(False)
329
330 @pyqtSlot()
331 def on_replaceSearchButton_clicked(self):
332 """
333 Private slot to replace one occurrence of data and search for the next
334 one.
335 """
336 self.__doReplace(True)
337
338 def __doReplace(self, searchNext):
339 """
340 Private method to replace one occurrence of data.
341
342 @param searchNext flag indicating to search for the next occurrence
343 @type bool
344 """
345 # Check enabled status due to dual purpose usage of this method
346 if (
347 not self.__ui.replaceButton.isEnabled() and
348 not self.__ui.replaceSearchButton.isEnabled()
349 ):
350 return
351
352 fba, ftxt = self.__getContent(False)
353 rba, rtxt = self.__getContent(True)
354
355 ok = False
356 if self.__editor.hasSelection():
357 # we did a successful search before
358 startIdx = self.__editor.getSelectionBegin()
359 self.__editor.replaceByteArray(startIdx, len(fba), rba)
360
361 if searchNext:
362 ok = self.findPrevNext(self.__findBackwards)
363
364 if not ok:
365 self.__ui.replaceButton.setEnabled(False)
366 self.__ui.replaceSearchButton.setEnabled(False)
367
368 @pyqtSlot()
369 def on_replaceAllButton_clicked(self):
370 """
371 Private slot to replace all occurrences of data.
372 """
373 replacements = 0
374
375 cursorPosition = self.__editor.cursorPosition()
376
377 fba, ftxt = self.__getContent(False)
378 rba, rtxt = self.__getContent(True)
379
380 idx = 0
381 while idx >= 0:
382 idx = self.__editor.indexOf(fba, idx)
383 if idx >= 0:
384 self.__editor.replaceByteArray(idx, len(fba), rba)
385 idx += len(rba)
386 replacements += 1
387
388 if replacements:
389 EricMessageBox.information(
390 self, self.windowTitle(),
391 self.tr("Replaced {0} occurrences.")
392 .format(replacements))
393 else:
394 EricMessageBox.information(
395 self, self.windowTitle(),
396 self.tr("Nothing replaced because '{0}' was not found.")
397 .format(ftxt))
398
399 self.__editor.setCursorPosition(cursorPosition)
400 self.__editor.ensureVisible()
401
402 def __showFind(self, text=''):
403 """
404 Private method to display this widget in find mode.
405
406 @param text hex encoded text to be shown in the findtext edit
407 @type str
408 """
409 self.__replace = False
410
411 self.__ui.findtextCombo.clear()
412 for index, txt in self.__findHistory:
413 self.__ui.findtextCombo.addItem(txt, index)
414 self.__ui.findFormatCombo.setCurrentIndex(0) # 0 is always Hex
415 self.on_findFormatCombo_currentIndexChanged(0)
416 self.__ui.findtextCombo.setEditText(text)
417 self.__ui.findtextCombo.lineEdit().selectAll()
418 self.__ui.findtextCombo.setFocus()
419 self.on_findtextCombo_editTextChanged(text)
420
421 self.__havefound = True
422 self.__findBackwards = False
423
424 def __showReplace(self, text=''):
425 """
426 Private slot to display this widget in replace mode.
427
428 @param text hex encoded text to be shown in the findtext edit
429 @type str
430 """
431 self.__replace = True
432
433 self.__ui.findtextCombo.clear()
434 for index, txt in self.__findHistory:
435 self.__ui.findtextCombo.addItem(txt, index)
436 self.__ui.findFormatCombo.setCurrentIndex(0) # 0 is always Hex
437 self.on_findFormatCombo_currentIndexChanged(0)
438 self.__ui.findtextCombo.setEditText(text)
439 self.__ui.findtextCombo.lineEdit().selectAll()
440 self.__ui.findtextCombo.setFocus()
441 self.on_findtextCombo_editTextChanged(text)
442
443 self.__ui.replacetextCombo.clear()
444 for index, txt in self.__replaceHistory:
445 self.__ui.replacetextCombo.addItem(txt, index)
446 self.__ui.replaceFormatCombo.setCurrentIndex(0) # 0 is always Hex
447 self.on_replaceFormatCombo_currentIndexChanged(0)
448 self.__ui.replacetextCombo.setEditText('')
449
450 self.__havefound = True
451 self.__findBackwards = False
452
453 def show(self, text=''):
454 """
455 Public slot to show the widget.
456
457 @param text hex encoded text to be shown in the findtext edit
458 @type str
459 """
460 if self.__replace:
461 self.__showReplace(text)
462 else:
463 self.__showFind(text)
464 super().show()
465 self.activateWindow()
466
467 @pyqtSlot()
468 def on_closeButton_clicked(self):
469 """
470 Private slot to close the widget.
471 """
472 self.__editor.setFocus(Qt.FocusReason.OtherFocusReason)
473 self.close()
474
475 def keyPressEvent(self, event):
476 """
477 Protected slot to handle key press events.
478
479 @param event reference to the key press event
480 @type QKeyEvent
481 """
482 if event.key() == Qt.Key.Key_Escape:
483 self.close()
484
485 def __convertText(self, txt, oldFormat, newFormat):
486 """
487 Private method to convert text from one format into another.
488
489 @param txt text to be converted
490 @type str
491 @param oldFormat current format of the text
492 @type str
493 @param newFormat format to convert to
494 @type str
495 @return converted text
496 @rtype str
497 """
498 if txt and oldFormat and newFormat and oldFormat != newFormat:
499 # step 1: convert the text to a byte array using the old format
500 byteArray = self.__text2bytearray(txt, oldFormat)
501
502 # step 2: convert the byte array to text using the new format
503 txt = self.__bytearray2text(byteArray, newFormat)
504
505 return txt
506
507 def __int2bytearray(self, value):
508 """
509 Private method to convert an integer to a byte array.
510
511 @param value value to be converted
512 @type int
513 @return byte array for the given value
514 @rtype bytearray
515 """
516 ba = bytearray()
517 while value > 0:
518 value, modulus = divmod(value, 256)
519 ba.insert(0, modulus)
520
521 return ba
522
523 def __bytearray2int(self, array):
524 """
525 Private method to convert a byte array to an integer value.
526
527 @param array byte array to be converted
528 @type bytearray
529 @return integer value of the given array
530 @rtype int
531 """
532 value = 0
533 for b in array:
534 value = value * 256 + b
535
536 return value
537
538 def __text2bytearray(self, txt, dataFormat):
539 """
540 Private method to convert a text to a byte array.
541
542 @param txt text to be converted
543 @type str
544 @param dataFormat format of the text
545 @type str
546 @return converted text
547 @rtype bytearray
548 @exception ValueError raised to indicate an invalid dataFormat
549 parameter
550 """
551 if dataFormat not in self.__formatAndValidators.keys():
552 raise ValueError("Bad value for 'dataFormat' parameter.")
553
554 if dataFormat == "hex": # hex format
555 ba = bytearray(QByteArray.fromHex(
556 bytes(txt, encoding="ascii")))
557 elif dataFormat == "dec": # decimal format
558 ba = self.__int2bytearray(int(txt, 10))
559 elif dataFormat == "oct": # octal format
560 ba = self.__int2bytearray(int(txt, 8))
561 elif dataFormat == "bin": # binary format
562 ba = self.__int2bytearray(int(txt, 2))
563 elif dataFormat == "iso-8859-1": # latin-1/iso-8859-1 text
564 ba = bytearray(txt, encoding="iso-8859-1")
565 elif dataFormat == "utf-8": # utf-8 text
566 ba = bytearray(txt, encoding="utf-8")
567
568 return ba
569
570 def __bytearray2text(self, array, dataFormat):
571 """
572 Private method to convert a byte array to a text.
573
574 @param array byte array to be converted
575 @type bytearray
576 @param dataFormat format of the text
577 @type str
578 @return formatted text
579 @rtype str
580 @exception ValueError raised to indicate an invalid dataFormat
581 parameter
582 """
583 if dataFormat not in self.__formatAndValidators.keys():
584 raise ValueError("Bad value for 'dataFormat' parameter.")
585
586 if dataFormat == "hex": # hex format
587 txt = "{0:x}".format(self.__bytearray2int(array))
588 elif dataFormat == "dec": # decimal format
589 txt = "{0:d}".format(self.__bytearray2int(array))
590 elif dataFormat == "oct": # octal format
591 txt = "{0:o}".format(self.__bytearray2int(array))
592 elif dataFormat == "bin": # binary format
593 txt = "{0:b}".format(self.__bytearray2int(array))
594 elif dataFormat == "iso-8859-1": # latin-1/iso-8859-1 text
595 txt = str(array, encoding="iso-8859-1")
596 elif dataFormat == "utf-8": # utf-8 text
597 txt = str(array, encoding="utf-8", errors="replace")
598
599 return txt

eric ide

mercurial