|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2002 - 2009 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to search for text in files. |
|
8 """ |
|
9 |
|
10 import os |
|
11 import re |
|
12 |
|
13 import sys |
|
14 |
|
15 from PyQt4.QtCore import * |
|
16 from PyQt4.QtGui import * |
|
17 |
|
18 from E4Gui.E4Application import e4App |
|
19 |
|
20 from Ui_FindFileDialog import Ui_FindFileDialog |
|
21 |
|
22 import Utilities |
|
23 import Preferences |
|
24 |
|
25 class FindFileDialog(QDialog, Ui_FindFileDialog): |
|
26 """ |
|
27 Class implementing a dialog to search for text in files. |
|
28 |
|
29 The occurrences found are displayed in a QTreeWidget showing the filename, the |
|
30 linenumber and the found text. The file will be opened upon a double click onto |
|
31 the respective entry of the list. |
|
32 |
|
33 @signal sourceFile(string, int, string, (int, int)) emitted to open a |
|
34 source file at a line |
|
35 @signal designerFile(string) emitted to open a Qt-Designer file |
|
36 """ |
|
37 lineRole = Qt.UserRole + 1 |
|
38 startRole = Qt.UserRole + 2 |
|
39 endRole = Qt.UserRole + 3 |
|
40 replaceRole = Qt.UserRole + 4 |
|
41 |
|
42 def __init__(self, project, replaceMode = False, parent=None): |
|
43 """ |
|
44 Constructor |
|
45 |
|
46 @param project reference to the project object |
|
47 @param parent parent widget of this dialog (QWidget) |
|
48 """ |
|
49 QDialog.__init__(self, parent) |
|
50 self.setupUi(self) |
|
51 self.setWindowFlags(Qt.WindowFlags(Qt.Window)) |
|
52 |
|
53 self.__replaceMode = replaceMode |
|
54 |
|
55 self.stopButton = \ |
|
56 self.buttonBox.addButton(self.trUtf8("Stop"), QDialogButtonBox.ActionRole) |
|
57 self.stopButton.setEnabled(False) |
|
58 |
|
59 self.findButton = \ |
|
60 self.buttonBox.addButton(self.trUtf8("Find"), QDialogButtonBox.ActionRole) |
|
61 self.findButton.setEnabled(False) |
|
62 self.findButton.setDefault(True) |
|
63 |
|
64 if self.__replaceMode: |
|
65 self.replaceButton.setEnabled(False) |
|
66 self.setWindowTitle(self.trUtf8("Replace in Files")) |
|
67 else: |
|
68 self.replaceLabel.hide() |
|
69 self.replacetextCombo.hide() |
|
70 self.replaceButton.hide() |
|
71 |
|
72 self.findProgressLabel.setMaximumWidth(550) |
|
73 |
|
74 self.searchHistory = [] |
|
75 self.replaceHistory = [] |
|
76 self.project = project |
|
77 |
|
78 self.findList.headerItem().setText(self.findList.columnCount(), "") |
|
79 self.findList.header().setSortIndicator(0, Qt.AscendingOrder) |
|
80 self.__section0Size = self.findList.header().sectionSize(0) |
|
81 self.findList.setExpandsOnDoubleClick(False) |
|
82 if self.__replaceMode: |
|
83 font = self.findList.font() |
|
84 if Utilities.isWindowsPlatform(): |
|
85 font.setFamily("Lucida Console") |
|
86 else: |
|
87 font.setFamily("Monospace") |
|
88 self.findList.setFont(font) |
|
89 |
|
90 # Qt Designer form files |
|
91 self.filterForms = r'.*\.ui$' |
|
92 self.formsExt = ['*.ui'] |
|
93 |
|
94 # Corba interface files |
|
95 self.filterInterfaces = r'.*\.idl$' |
|
96 self.interfacesExt = ['*.idl'] |
|
97 |
|
98 # Qt resources files |
|
99 self.filterResources = r'.*\.qrc$' |
|
100 self.resourcesExt = ['*.qrc'] |
|
101 |
|
102 self.__cancelSearch = False |
|
103 self.__lastFileItem = None |
|
104 self.__populating = False |
|
105 |
|
106 self.setContextMenuPolicy(Qt.CustomContextMenu) |
|
107 self.connect(self, SIGNAL("customContextMenuRequested(const QPoint &)"), |
|
108 self.__contextMenuRequested) |
|
109 |
|
110 def __createItem(self, file, line, text, start, end, replTxt = ""): |
|
111 """ |
|
112 Private method to create an entry in the file list. |
|
113 |
|
114 @param file filename of file (string) |
|
115 @param line line number (integer) |
|
116 @param text text found (string) |
|
117 @param start start position of match (integer) |
|
118 @param end end position of match (integer) |
|
119 @param replTxt text with replacements applied (string |
|
120 """ |
|
121 if self.__lastFileItem is None: |
|
122 # It's a new file |
|
123 self.__lastFileItem = QTreeWidgetItem(self.findList, [file]) |
|
124 self.__lastFileItem.setFirstColumnSpanned(True) |
|
125 self.__lastFileItem.setExpanded(True) |
|
126 if self.__replaceMode: |
|
127 self.__lastFileItem.setFlags(self.__lastFileItem.flags() | \ |
|
128 Qt.ItemFlags(Qt.ItemIsUserCheckable | Qt.ItemIsTristate)) |
|
129 # Qt bug: |
|
130 # item is not user checkable if setFirstColumnSpanned is True (< 4.5.0) |
|
131 |
|
132 itm = QTreeWidgetItem(self.__lastFileItem, [' %5d ' % line, text]) |
|
133 itm.setTextAlignment(0, Qt.AlignRight) |
|
134 itm.setData(0, self.lineRole, QVariant(line)) |
|
135 itm.setData(0, self.startRole, QVariant(start)) |
|
136 itm.setData(0, self.endRole, QVariant(end)) |
|
137 itm.setData(0, self.replaceRole, QVariant(replTxt)) |
|
138 if self.__replaceMode: |
|
139 itm.setFlags(itm.flags() | Qt.ItemFlags(Qt.ItemIsUserCheckable)) |
|
140 itm.setCheckState(0, Qt.Checked) |
|
141 self.replaceButton.setEnabled(True) |
|
142 |
|
143 def show(self, txt = ""): |
|
144 """ |
|
145 Overwritten method to enable/disable the project button. |
|
146 |
|
147 @param txt text to be shown in the searchtext combo (string) |
|
148 """ |
|
149 if self.project and self.project.isOpen(): |
|
150 self.projectButton.setEnabled(True) |
|
151 else: |
|
152 self.projectButton.setEnabled(False) |
|
153 self.dirButton.setChecked(True) |
|
154 |
|
155 self.findtextCombo.setEditText(txt) |
|
156 self.findtextCombo.lineEdit().selectAll() |
|
157 self.findtextCombo.setFocus() |
|
158 |
|
159 if self.__replaceMode: |
|
160 self.findList.clear() |
|
161 self.replacetextCombo.setEditText("") |
|
162 |
|
163 QDialog.show(self) |
|
164 |
|
165 def on_findtextCombo_editTextChanged(self, text): |
|
166 """ |
|
167 Private slot to handle the editTextChanged signal of the find text combo. |
|
168 |
|
169 @param text (ignored) |
|
170 """ |
|
171 self.__enableFindButton() |
|
172 |
|
173 def on_replacetextCombo_editTextChanged(self, text): |
|
174 """ |
|
175 Private slot to handle the editTextChanged signal of the replace text combo. |
|
176 |
|
177 @param text (ignored) |
|
178 """ |
|
179 self.__enableFindButton() |
|
180 |
|
181 def on_dirEdit_textChanged(self, text): |
|
182 """ |
|
183 Private slot to handle the textChanged signal of the directory edit. |
|
184 |
|
185 @param text (ignored) |
|
186 """ |
|
187 self.__enableFindButton() |
|
188 |
|
189 @pyqtSlot() |
|
190 def on_projectButton_clicked(self): |
|
191 """ |
|
192 Private slot to handle the selection of the project radio button. |
|
193 """ |
|
194 self.__enableFindButton() |
|
195 |
|
196 @pyqtSlot() |
|
197 def on_dirButton_clicked(self): |
|
198 """ |
|
199 Private slot to handle the selection of the project radio button. |
|
200 """ |
|
201 self.__enableFindButton() |
|
202 |
|
203 @pyqtSlot() |
|
204 def on_filterCheckBox_clicked(self): |
|
205 """ |
|
206 Private slot to handle the selection of the file filter check box. |
|
207 """ |
|
208 self.__enableFindButton() |
|
209 |
|
210 @pyqtSlot(str) |
|
211 def on_filterEdit_textEdited(self, p0): |
|
212 """ |
|
213 Private slot to handle the textChanged signal of the file filter edit. |
|
214 |
|
215 @param text (ignored) |
|
216 """ |
|
217 self.__enableFindButton() |
|
218 |
|
219 def __enableFindButton(self): |
|
220 """ |
|
221 Private slot called to enable the find button. |
|
222 """ |
|
223 if self.findtextCombo.currentText() == "" or \ |
|
224 (self.__replaceMode and self.replacetextCombo.currentText() == "") or \ |
|
225 (self.dirButton.isChecked() and \ |
|
226 (self.dirEdit.text() == "" or \ |
|
227 not os.path.exists(os.path.abspath(self.dirEdit.text())))) or \ |
|
228 (self.filterCheckBox.isChecked() and self.filterEdit.text() == ""): |
|
229 self.findButton.setEnabled(False) |
|
230 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) |
|
231 else: |
|
232 self.findButton.setEnabled(True) |
|
233 self.findButton.setDefault(True) |
|
234 |
|
235 def on_buttonBox_clicked(self, button): |
|
236 """ |
|
237 Private slot called by a button of the button box clicked. |
|
238 |
|
239 @param button button that was clicked (QAbstractButton) |
|
240 """ |
|
241 if button == self.findButton: |
|
242 self.__doSearch() |
|
243 elif button == self.stopButton: |
|
244 self.__stopSearch() |
|
245 |
|
246 def __stopSearch(self): |
|
247 """ |
|
248 Private slot to handle the stop button being pressed. |
|
249 """ |
|
250 self.__cancelSearch = True |
|
251 |
|
252 def __doSearch(self): |
|
253 """ |
|
254 Private slot to handle the find button being pressed. |
|
255 """ |
|
256 if self.__replaceMode and not e4App().getObject("ViewManager").checkAllDirty(): |
|
257 return |
|
258 |
|
259 self.__cancelSearch = False |
|
260 self.stopButton.setEnabled(True) |
|
261 self.stopButton.setDefault(True) |
|
262 self.findButton.setEnabled(False) |
|
263 |
|
264 if self.filterCheckBox.isChecked(): |
|
265 fileFilter = self.filterEdit.text() |
|
266 fileFilterList = ["^%s$" % filter.replace(".", "\.").replace("*", ".*") \ |
|
267 for filter in fileFilter.split(";")] |
|
268 filterRe = re.compile("|".join(fileFilterList)) |
|
269 |
|
270 if self.projectButton.isChecked(): |
|
271 if self.filterCheckBox.isChecked(): |
|
272 files = [file.replace(self.project.ppath + os.sep, "") \ |
|
273 for file in self.__getFileList(self.project.ppath, filterRe)] |
|
274 else: |
|
275 files = [] |
|
276 if self.sourcesCheckBox.isChecked(): |
|
277 files += self.project.pdata["SOURCES"] |
|
278 if self.formsCheckBox.isChecked(): |
|
279 files += self.project.pdata["FORMS"] |
|
280 if self.interfacesCheckBox.isChecked(): |
|
281 files += self.project.pdata["INTERFACES"] |
|
282 if self.resourcesCheckBox.isChecked(): |
|
283 files += self.project.pdata["RESOURCES"] |
|
284 elif self.dirButton.isChecked(): |
|
285 if not self.filterCheckBox.isChecked(): |
|
286 filters = [] |
|
287 if self.sourcesCheckBox.isChecked(): |
|
288 filters.extend( |
|
289 ["^%s$" % assoc.replace(".", "\.").replace("*", ".*") \ |
|
290 for assoc in Preferences.getEditorLexerAssocs().keys() \ |
|
291 if assoc not in self.formsExt + self.interfacesExt]) |
|
292 if self.formsCheckBox.isChecked(): |
|
293 filters.append(self.filterForms) |
|
294 if self.interfacesCheckBox.isChecked(): |
|
295 filters.append(self.filterInterfaces) |
|
296 if self.resourcesCheckBox.isChecked(): |
|
297 filters.append(self.filterResources) |
|
298 filterString = "|".join(filters) |
|
299 filterRe = re.compile(filterString) |
|
300 files = self.__getFileList(os.path.abspath(self.dirEdit.text()), |
|
301 filterRe) |
|
302 elif self.openFilesButton.isChecked(): |
|
303 files = e4App().getObject("ViewManager").getOpenFilenames() |
|
304 |
|
305 self.findList.clear() |
|
306 QApplication.processEvents() |
|
307 QApplication.processEvents() |
|
308 self.findProgress.setMaximum(len(files)) |
|
309 |
|
310 # retrieve the values |
|
311 reg = self.regexpCheckBox.isChecked() |
|
312 wo = self.wordCheckBox.isChecked() |
|
313 cs = self.caseCheckBox.isChecked() |
|
314 ct = self.findtextCombo.currentText() |
|
315 if reg: |
|
316 txt = ct |
|
317 else: |
|
318 txt = re.escape(ct) |
|
319 if wo: |
|
320 txt = "\\b%s\\b" % txt |
|
321 flags = re.UNICODE | re.LOCALE |
|
322 if not cs: |
|
323 flags |= re.IGNORECASE |
|
324 search = re.compile(txt, flags) |
|
325 |
|
326 # reset the findtextCombo |
|
327 if ct in self.searchHistory: |
|
328 self.searchHistory.remove(ct) |
|
329 self.searchHistory.insert(0, ct) |
|
330 self.findtextCombo.clear() |
|
331 self.findtextCombo.addItems(self.searchHistory) |
|
332 if self.__replaceMode: |
|
333 replTxt = self.replacetextCombo.currentText() |
|
334 if replTxt in self.replaceHistory: |
|
335 self.replaceHistory.remove(replTxt) |
|
336 self.replaceHistory.insert(0, replTxt) |
|
337 self.replacetextCombo.clear() |
|
338 self.replacetextCombo.addItems(self.replaceHistory) |
|
339 |
|
340 # now go through all the files |
|
341 self.__populating = True |
|
342 self.findList.setUpdatesEnabled(False) |
|
343 progress = 0 |
|
344 breakSearch = False |
|
345 for file in files: |
|
346 self.__lastFileItem = None |
|
347 if self.__cancelSearch or breakSearch: |
|
348 break |
|
349 |
|
350 self.findProgressLabel.setPath(file) |
|
351 |
|
352 if self.projectButton.isChecked(): |
|
353 fn = os.path.join(self.project.ppath, file) |
|
354 else: |
|
355 fn = file |
|
356 # read the file and split it into textlines |
|
357 try: |
|
358 f = open(fn, 'rb') |
|
359 text, encoding = Utilities.decode(f.read()) |
|
360 lines = text.splitlines() |
|
361 f.close() |
|
362 except IOError: |
|
363 progress += 1 |
|
364 self.findProgress.setValue(progress) |
|
365 continue |
|
366 |
|
367 # now perform the search and display the lines found |
|
368 count = 0 |
|
369 for line in lines: |
|
370 if self.__cancelSearch: |
|
371 break |
|
372 |
|
373 count += 1 |
|
374 contains = search.search(line) |
|
375 if contains: |
|
376 start = contains.start() |
|
377 end = contains.end() |
|
378 if self.__replaceMode: |
|
379 rline = search.sub(replTxt, line) |
|
380 else: |
|
381 rline = "" |
|
382 if len(line) > 1024: |
|
383 line = "%s ..." % line[:1024] |
|
384 if self.__replaceMode: |
|
385 if len(rline) > 1024: |
|
386 rline = "%s ..." % line[:1024] |
|
387 line = "- %s\n+ %s" % (line, rline) |
|
388 self.__createItem(file, count, line, start, end, rline) |
|
389 |
|
390 if self.feelLikeCheckBox.isChecked(): |
|
391 fn = os.path.join(self.project.ppath, file) |
|
392 self.emit(SIGNAL('sourceFile'), fn, count, "", (start, end)) |
|
393 QApplication.processEvents() |
|
394 breakSearch = True |
|
395 break |
|
396 |
|
397 QApplication.processEvents() |
|
398 |
|
399 progress += 1 |
|
400 self.findProgress.setValue(progress) |
|
401 |
|
402 self.findProgressLabel.setPath("") |
|
403 |
|
404 self.findList.setUpdatesEnabled(True) |
|
405 self.findList.sortItems(self.findList.sortColumn(), |
|
406 self.findList.header().sortIndicatorOrder()) |
|
407 self.findList.resizeColumnToContents(1) |
|
408 if self.__replaceMode: |
|
409 self.findList.header().resizeSection(0, self.__section0Size + 30) |
|
410 self.findList.header().setStretchLastSection(True) |
|
411 self.__populating = False |
|
412 |
|
413 self.stopButton.setEnabled(False) |
|
414 self.findButton.setEnabled(True) |
|
415 self.findButton.setDefault(True) |
|
416 |
|
417 if breakSearch: |
|
418 self.close() |
|
419 |
|
420 def on_findList_itemDoubleClicked(self, itm, column): |
|
421 """ |
|
422 Private slot to handle the double click on a file item. |
|
423 |
|
424 It emits the signal |
|
425 sourceFile or designerFile depending on the file extension. |
|
426 |
|
427 @param itm the double clicked tree item (QTreeWidgetItem) |
|
428 @param column column that was double clicked (integer) (ignored) |
|
429 """ |
|
430 if itm.parent(): |
|
431 file = itm.parent().text(0) |
|
432 line = itm.data(0, self.lineRole).toInt()[0] |
|
433 start = itm.data(0, self.startRole).toInt()[0] |
|
434 end = itm.data(0, self.endRole).toInt()[0] |
|
435 else: |
|
436 file = itm.text(0) |
|
437 line = 1 |
|
438 start = 0 |
|
439 end = 0 |
|
440 |
|
441 if self.project: |
|
442 fn = os.path.join(self.project.ppath, file) |
|
443 else: |
|
444 fn = file |
|
445 if fn.endswith('.ui'): |
|
446 self.emit(SIGNAL('designerFile'), fn) |
|
447 else: |
|
448 self.emit(SIGNAL('sourceFile'), fn, line, "", (start, end)) |
|
449 |
|
450 @pyqtSlot() |
|
451 def on_dirSelectButton_clicked(self): |
|
452 """ |
|
453 Private slot to display a directory selection dialog. |
|
454 """ |
|
455 directory = QFileDialog.getExistingDirectory(\ |
|
456 self, |
|
457 self.trUtf8("Select directory"), |
|
458 self.dirEdit.text(), |
|
459 QFileDialog.Options(QFileDialog.ShowDirsOnly)) |
|
460 |
|
461 if directory: |
|
462 self.dirEdit.setText(Utilities.toNativeSeparators(directory)) |
|
463 |
|
464 def __getFileList(self, path, filterRe): |
|
465 """ |
|
466 Private method to get a list of files to search. |
|
467 |
|
468 @param path the root directory to search in (string) |
|
469 @param filterRe regular expression defining the filter criteria (regexp object) |
|
470 @return list of files to be processed (list of strings) |
|
471 """ |
|
472 path = os.path.abspath(path) |
|
473 files = [] |
|
474 for dirname, _, names in os.walk(path): |
|
475 files.extend([os.path.join(dirname, f) \ |
|
476 for f in names \ |
|
477 if re.match(filterRe, f)] |
|
478 ) |
|
479 return files |
|
480 |
|
481 def setSearchDirectory(self, searchDir): |
|
482 """ |
|
483 Public slot to set the name of the directory to search in. |
|
484 |
|
485 @param searchDir name of the directory to search in (string) |
|
486 """ |
|
487 self.dirButton.setChecked(True) |
|
488 self.dirEdit.setText(Utilities.toNativeSeparators(searchDir)) |
|
489 |
|
490 @pyqtSlot() |
|
491 def on_replaceButton_clicked(self): |
|
492 """ |
|
493 Private slot to perform the requested replace actions. |
|
494 """ |
|
495 self.findProgress.setMaximum(self.findList.topLevelItemCount()) |
|
496 self.findProgress.setValue(0) |
|
497 |
|
498 progress = 0 |
|
499 for index in range(self.findList.topLevelItemCount()): |
|
500 itm = self.findList.topLevelItem(index) |
|
501 if itm.checkState(0) in [Qt.PartiallyChecked, Qt.Checked]: |
|
502 file = itm.text(0) |
|
503 |
|
504 self.findProgressLabel.setPath(file) |
|
505 |
|
506 if self.projectButton.isChecked(): |
|
507 fn = os.path.join(self.project.ppath, file) |
|
508 else: |
|
509 fn = file |
|
510 |
|
511 # read the file and split it into textlines |
|
512 try: |
|
513 f = open(fn, 'rb') |
|
514 text, encoding = Utilities.decode(f.read()) |
|
515 lines = text.splitlines() |
|
516 f.close() |
|
517 except IOError, err: |
|
518 QMessageBox.critical(self, |
|
519 self.trUtf8("Replace in Files"), |
|
520 self.trUtf8("""<p>Could not read the file <b>{0}</b>.""" |
|
521 """ Skipping it.</p><p>Reason: {1}</p>""")\ |
|
522 .format(fn, unicode(err)) |
|
523 ) |
|
524 progress += 1 |
|
525 self.findProgress.setValue(progress) |
|
526 continue |
|
527 |
|
528 # replace the lines authorized by the user |
|
529 for cindex in range(itm.childCount()): |
|
530 citm = itm.child(cindex) |
|
531 if citm.checkState(0) == Qt.Checked: |
|
532 line = citm.data(0, self.lineRole).toInt()[0] |
|
533 rline = citm.data(0, self.replaceRole).toString() |
|
534 lines[line - 1] = rline |
|
535 |
|
536 # write the file |
|
537 txt = Utilities.linesep().join(lines) |
|
538 txt, encoding = Utilities.encode(txt, encoding) |
|
539 try: |
|
540 f = open(fn, 'wb') |
|
541 f.write(txt) |
|
542 f.close() |
|
543 except IOError, err: |
|
544 QMessageBox.critical(self, |
|
545 self.trUtf8("Replace in Files"), |
|
546 self.trUtf8("""<p>Could not save the file <b>{0}</b>.""" |
|
547 """ Skipping it.</p><p>Reason: {1}</p>""")\ |
|
548 .format(fn, unicode(err)) |
|
549 ) |
|
550 |
|
551 progress += 1 |
|
552 self.findProgress.setValue(progress) |
|
553 |
|
554 self.findProgressLabel.setPath("") |
|
555 |
|
556 self.findList.clear() |
|
557 self.replaceButton.setEnabled(False) |
|
558 self.findButton.setEnabled(True) |
|
559 self.findButton.setDefault(True) |
|
560 |
|
561 def __contextMenuRequested(self, pos): |
|
562 """ |
|
563 Private slot to handle the context menu request. |
|
564 |
|
565 @param pos position the context menu shall be shown (QPoint) |
|
566 """ |
|
567 menu = QMenu(self) |
|
568 |
|
569 menu.addAction(self.trUtf8("Open"), self.__openFile) |
|
570 menu.addAction(self.trUtf8("Copy Path to Clipboard"), self.__copyToClipboard) |
|
571 |
|
572 menu.exec_(QCursor.pos()) |
|
573 |
|
574 def __openFile(self): |
|
575 """ |
|
576 Private slot to open the currently selected entry. |
|
577 """ |
|
578 itm = self.findList.selectedItems()[0] |
|
579 self.on_findList_itemDoubleClicked(itm, 0) |
|
580 |
|
581 def __copyToClipboard(self): |
|
582 """ |
|
583 Private method to copy the path of an entry to the clipboard. |
|
584 """ |
|
585 itm = self.findList.selectedItems()[0] |
|
586 if itm.parent(): |
|
587 fn = itm.parent().text(0) |
|
588 else: |
|
589 fn = itm.text(0) |
|
590 |
|
591 cb = QApplication.clipboard() |
|
592 cb.setText(fn) |