eric6/HexEdit/HexEditSearchReplaceWidget.py

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

eric ide

mercurial