|
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() |