HexEdit/HexEditSearchReplaceWidget.py

changeset 4652
a88a2ba7a48a
child 4653
e8b51747c48e
equal deleted inserted replaced
4651:7f3f276d3bf3 4652:a88a2ba7a48a
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2016 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 PyQt5.QtCore import pyqtSlot, Qt, QByteArray
11 from PyQt5.QtWidgets import QWidget
12
13 from E5Gui.E5Action import E5Action
14 from E5Gui import E5MessageBox
15
16 import UI.PixmapCache
17
18
19 class HexEditSearchReplaceWidget(QWidget):
20 """
21 Class implementing a search and replace widget for the hex editor.
22 """
23 def __init__(self, editor, replace=False, parent=None):
24 """
25 Constructor
26
27 @param editor reference to the hex editor widget
28 @type HexEditWidget
29 @param replace flag indicating a replace widget
30 @type bool
31 @param parent reference to the parent widget
32 @type QWidget
33 """
34 super(HexEditSearchReplaceWidget, self).__init__(parent)
35
36 self.__replace = replace
37 self.__editor = editor
38
39 self.__findHistory = []
40 if replace:
41 from .Ui_HexEditReplaceWidget import Ui_HexEditReplaceWidget
42 self.__replaceHistory = []
43 self.__ui = Ui_HexEditReplaceWidget()
44 else:
45 from .Ui_HexEditSearchWidget import Ui_HexEditSearchWidget
46 self.__ui = Ui_HexEditSearchWidget()
47 self.__ui.setupUi(self)
48
49 self.__ui.closeButton.setIcon(UI.PixmapCache.getIcon("close.png"))
50 self.__ui.findPrevButton.setIcon(
51 UI.PixmapCache.getIcon("1leftarrow.png"))
52 self.__ui.findNextButton.setIcon(
53 UI.PixmapCache.getIcon("1rightarrow.png"))
54
55 if replace:
56 self.__ui.replaceButton.setIcon(
57 UI.PixmapCache.getIcon("editReplace.png"))
58 self.__ui.replaceSearchButton.setIcon(
59 UI.PixmapCache.getIcon("editReplaceSearch.png"))
60 self.__ui.replaceAllButton.setIcon(
61 UI.PixmapCache.getIcon("editReplaceAll.png"))
62
63 self.__ui.findtextCombo.setCompleter(None)
64 self.__ui.findtextCombo.lineEdit().returnPressed.connect(
65 self.__findByReturnPressed)
66 if replace:
67 self.__ui.replacetextCombo.setCompleter(None)
68 self.__ui.replacetextCombo.lineEdit().returnPressed.connect(
69 self.on_replaceButton_clicked)
70
71 self.findNextAct = E5Action(
72 self.tr('Find Next'),
73 self.tr('Find Next'),
74 0, 0, self, 'hexEditor_search_widget_find_next')
75 self.findNextAct.triggered.connect(self.on_findNextButton_clicked)
76 self.findNextAct.setEnabled(False)
77 self.__ui.findtextCombo.addAction(self.findNextAct)
78
79 self.findPrevAct = E5Action(
80 self.tr('Find Prev'),
81 self.tr('Find Prev'),
82 0, 0, self, 'hexEditor_search_widget_find_prev')
83 self.findPrevAct.triggered.connect(self.on_findPrevButton_clicked)
84 self.findPrevAct.setEnabled(False)
85 self.__ui.findtextCombo.addAction(self.findPrevAct)
86
87 self.__havefound = False
88
89 def on_findtextCombo_editTextChanged(self, txt):
90 """
91 Private slot to enable/disable the find buttons.
92
93 @param txt text of the find text combo (string)
94 """
95 if not txt:
96 self.__ui.findNextButton.setEnabled(False)
97 self.findNextAct.setEnabled(False)
98 self.__ui.findPrevButton.setEnabled(False)
99 self.findPrevAct.setEnabled(False)
100 if self.__replace:
101 self.__ui.replaceButton.setEnabled(False)
102 self.__ui.replaceSearchButton.setEnabled(False)
103 self.__ui.replaceAllButton.setEnabled(False)
104 else:
105 self.__ui.findNextButton.setEnabled(True)
106 self.findNextAct.setEnabled(True)
107 self.__ui.findPrevButton.setEnabled(True)
108 self.findPrevAct.setEnabled(True)
109 if self.__replace:
110 self.__ui.replaceButton.setEnabled(False)
111 self.__ui.replaceSearchButton.setEnabled(False)
112 self.__ui.replaceAllButton.setEnabled(True)
113
114 def __getContent(self, replace=False):
115 """
116 Private method to get the contents of the find/replace combo as
117 a bytearray.
118
119 @param replace flag indicating to retrieve the replace contents
120 @type bool
121 @return search or replace term as text and binary data
122 @rtype tuple of bytearray and str
123 """
124 if replace:
125 textCombo = self.__ui.replacetextCombo
126 formatCombo = self.__ui.replaceFormatCombo
127 history = self.__replaceHistory
128 else:
129 textCombo = self.__ui.findtextCombo
130 formatCombo = self.__ui.findFormatCombo
131 history = self.__findHistory
132
133 txt = textCombo.currentText()
134 idx = formatCombo.currentIndex()
135 if idx == 0: # hex format
136 ba = bytearray(QByteArray.fromHex(
137 bytes(txt, encoding="ascii")))
138 else:
139 ba = bytearray(txt, encoding="utf-8")
140
141 # This moves any previous occurrence of this statement to the head
142 # of the list and updates the combobox
143 if txt in history:
144 history.remove(txt)
145 history.insert(0, txt)
146 textCombo.clear()
147 textCombo.addItems(history)
148
149 return ba, txt
150
151 @pyqtSlot()
152 def on_findNextButton_clicked(self):
153 """
154 Private slot to find the next occurrence.
155 """
156 self.findPrevNext(False)
157
158 @pyqtSlot()
159 def on_findPrevButton_clicked(self):
160 """
161 Private slot to find the previous occurrence.
162 """
163 self.findPrevNext(True)
164
165 def findPrevNext(self, prev=False):
166 """
167 Public slot to find the next occurrence of the search term.
168
169 @param prev flag indicating a backwards search
170 @type bool
171 @return flag indicating a successful search
172 @rtype bool
173 """
174 if not self.__havefound or not self.__ui.findtextCombo.currentText():
175 self.show()
176 return
177
178 self.__findBackwards = prev
179 ba, txt = self.__getContent()
180
181 idx = -1
182 if len(ba) > 0:
183 startIndex = self.__editor.cursorPosition() // 2
184 if prev:
185 if self.__editor.hasSelection() and \
186 startIndex == self.__editor.getSelectionEnd():
187 # skip to the selection start
188 startIndex = self.__editor.getSelectionBegin()
189 idx = self.__editor.lastIndexOf(ba, startIndex)
190 else:
191 if self.__editor.hasSelection() and \
192 startIndex == self.__editor.getSelectionBegin() - 1:
193 # skip to the selection end
194 startIndex = self.__editor.getSelectionEnd()
195 idx = self.__editor.indexOf(ba, startIndex)
196
197 if idx >= 0:
198 if self.__replace:
199 self.__ui.replaceButton.setEnabled(True)
200 self.__ui.replaceSearchButton.setEnabled(True)
201 else:
202 E5MessageBox.information(
203 self, self.windowTitle(),
204 self.tr("'{0}' was not found.").format(txt))
205
206 return idx >= 0
207
208 def __findByReturnPressed(self):
209 """
210 Private slot to handle a return pressed in the find combo.
211 """
212 if self.__findBackwards:
213 self.findPrevNext(True)
214 else:
215 self.findPrevNext(False)
216
217 @pyqtSlot()
218 def on_replaceButton_clicked(self):
219 """
220 Private slot to replace one occurrence of data.
221 """
222 self.__doReplace(False)
223
224 @pyqtSlot()
225 def on_replaceSearchButton_clicked(self):
226 """
227 Private slot to replace one occurrence of data and search for the next
228 one.
229 """
230 self.__doReplace(True)
231
232 def __doReplace(self, searchNext):
233 """
234 Private method to replace one occurrence of data.
235
236 @param searchNext flag indicating to search for the next occurrence
237 (boolean).
238 """
239 # Check enabled status due to dual purpose usage of this method
240 if not self.__ui.replaceButton.isEnabled() and \
241 not self.__ui.replaceSearchButton.isEnabled():
242 return
243
244 rba, rtxt = self.__getContent(True)
245
246 ok = False
247 if self.__editor.hasSelection():
248 # we did a successful search before
249 startIdx = self.__editor.getSelectionBegin()
250 self.__editor.replaceByteArray(startIdx, len(rba), rba)
251
252 if searchNext:
253 ok = self.findPrevNext(self.__findBackwards)
254
255 if not ok:
256 self.__ui.replaceButton.setEnabled(False)
257 self.__ui.replaceSearchButton.setEnabled(False)
258
259 @pyqtSlot()
260 def on_replaceAllButton_clicked(self):
261 """
262 Private slot to replace all occurrences of data.
263 """
264 replacements = 0
265
266 cursorPosition = self.__editor.cursorPosition()
267
268 fba, ftxt = self.__getContent(False)
269 rba, rtxt = self.__getContent(True)
270
271 idx = 0
272 while idx >= 0:
273 idx = self.__editor.indexOf(fba, idx)
274 if idx >= 0:
275 self.__editor.replaceByteArray(idx, len(rba), rba)
276 idx += len(rba)
277 replacements += 1
278
279 if replacements:
280 E5MessageBox.information(
281 self, self.windowTitle(),
282 self.tr("Replaced {0} occurrences.")
283 .format(replacements))
284 else:
285 E5MessageBox.information(
286 self, self.windowTitle(),
287 self.tr("Nothing replaced because '{0}' was not found.")
288 .format(ftxt))
289
290 self.__editor.setCursorPosition(cursorPosition)
291 self.__editor.ensureVisible()
292
293 def __showFind(self, text=''):
294 """
295 Private method to display this widget in find mode.
296
297 @param text text to be shown in the findtext edit (string)
298 """
299 self.__replace = False
300
301 self.__ui.findtextCombo.clear()
302 self.__ui.findtextCombo.addItems(self.__findHistory)
303 self.__ui.findtextCombo.setEditText(text)
304 self.__ui.findtextCombo.lineEdit().selectAll()
305 self.__ui.findtextCombo.setFocus()
306 self.on_findtextCombo_editTextChanged(text)
307
308 self.__havefound = True
309 self.__findBackwards = False
310
311 def __showReplace(self, text=''):
312 """
313 Private slot to display this widget in replace mode.
314
315 @param text text to be shown in the findtext edit
316 """
317 self.__replace = True
318
319 self.__ui.findtextCombo.clear()
320 self.__ui.findtextCombo.addItems(self.__findHistory)
321 self.__ui.findtextCombo.setEditText(text)
322 self.__ui.findtextCombo.lineEdit().selectAll()
323 self.__ui.findtextCombo.setFocus()
324 self.on_findtextCombo_editTextChanged(text)
325
326 self.__ui.replacetextCombo.clear()
327 self.__ui.replacetextCombo.addItems(self.__replaceHistory)
328 self.__ui.replacetextCombo.setEditText('')
329
330 self.__havefound = True
331 self.__findBackwards = False
332
333 def show(self, text=''):
334 """
335 Public slot to show the widget.
336
337 @param text text to be shown in the findtext edit (string)
338 """
339 if self.__replace:
340 self.__showReplace(text)
341 else:
342 self.__showFind(text)
343 super(HexEditSearchReplaceWidget, self).show()
344 self.activateWindow()
345
346 @pyqtSlot()
347 def on_closeButton_clicked(self):
348 """
349 Private slot to close the widget.
350 """
351 self.__editor.setFocus(Qt.OtherFocusReason)
352 self.close()
353
354 def keyPressEvent(self, event):
355 """
356 Protected slot to handle key press events.
357
358 @param event reference to the key press event (QKeyEvent)
359 """
360 if event.key() == Qt.Key_Escape:
361 self.close()

eric ide

mercurial