UI/FindFileDialog.py

changeset 0
de9c2efb9d02
child 6
52e8c820d0dd
equal deleted inserted replaced
-1:000000000000 0:de9c2efb9d02
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)

eric ide

mercurial