Project/QuickFindFileDialog.py

changeset 4985
03ac1a030529
child 5389
9b1c800daff3
equal deleted inserted replaced
4982:65f9cc310213 4985:03ac1a030529
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2004 - 2016 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a quick search for files.
8
9 This is basically the FindFileNameDialog modified to support faster
10 interactions.
11 """
12
13 from __future__ import unicode_literals
14
15 import os
16
17 from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QEvent
18 from PyQt5.QtWidgets import QWidget, QHeaderView, QApplication, \
19 QDialogButtonBox, QTreeWidgetItem
20
21 from .Ui_QuickFindFile import Ui_QuickFindFile
22
23
24 class QuickFindFileDialog(QWidget, Ui_QuickFindFile):
25 """
26 Class implementing the Quick Find File by Name Dialog.
27
28 This dialog provides a slightly more streamlined behaviour
29 than the standard FindFileNameDialog in that it tries to
30 match any name in the project against (fragmentary) bits of
31 file names.
32
33 @signal sourceFile(str) emitted to open a file in the editor
34 @signal designerFile(str) emitted to open a Qt-Designer file
35 @signal linguistFile(str) emitted to open a Qt translation file
36 """
37 sourceFile = pyqtSignal(str)
38 designerFile = pyqtSignal(str)
39 linguistFile = pyqtSignal(str)
40
41 def __init__(self, project, parent=None):
42 """
43 Constructor
44
45 @param project reference to the project object
46 @type Project
47 @param parent parent widget of this dialog
48 @type QWidget
49 """
50 super(QuickFindFileDialog, self).__init__(parent)
51 self.setupUi(self)
52
53 self.fileList.headerItem().setText(self.fileList.columnCount(), "")
54 self.fileNameEdit.returnPressed.connect(
55 self.on_fileNameEdit_returnPressed)
56 self.installEventFilter(self)
57
58 self.stopButton = self.buttonBox.addButton(
59 self.tr("Stop"), QDialogButtonBox.ActionRole)
60 self.project = project
61
62 def eventFilter(self, source, event):
63 """
64 Public method to handle event for another object.
65
66 @param source object to handle events for
67 @type QObject
68 @param event event to handle
69 @type QEvent
70 @return flag indicating that the event was handled
71 @rtype bool
72 """
73 if event.type() == QEvent.KeyPress:
74
75 # Anywhere in the dialog, make hitting escape cancel it
76 if event.key() == Qt.Key_Escape:
77 self.close()
78
79 # Anywhere in the dialog, make hitting up/down choose next item
80 # Note: This doesn't really do anything, as other than the text
81 # input there's nothing that doesn't handle up/down already.
82 elif event.key() == Qt.Key_Up or event.key() == Qt.Key_Down:
83 current = self.fileList.currentItem()
84 index = self.fileList.indexOfTopLevelItem(current)
85 if event.key() == Qt.Key_Up:
86 if index != 0:
87 self.fileList.setCurrentItem(
88 self.fileList.topLevelItem(index - 1))
89 else:
90 if index < (self.fileList.topLevelItemCount() - 1):
91 self.fileList.setCurrentItem(
92 self.fileList.topLevelItem(index + 1))
93 return QWidget.eventFilter(self, source, event)
94
95 def on_buttonBox_clicked(self, button):
96 """
97 Private slot called by a button of the button box clicked.
98
99 @param button button that was clicked (QAbstractButton)
100 """
101 if button == self.stopButton:
102 self.shouldStop = True
103 elif button == self.buttonBox.button(QDialogButtonBox.Open):
104 self.__openFile()
105
106 def __openFile(self, itm=None):
107 """
108 Private slot to open a file.
109
110 It emits the signal sourceFile or designerFile depending on the
111 file extension.
112
113 @param itm item to be opened
114 @type QTreeWidgetItem
115 @return flag indicating a file was opened
116 @rtype bool
117 """
118 if itm is None:
119 itm = self.fileList.currentItem()
120 if itm is not None:
121 filePath = itm.text(1)
122 fileName = itm.text(0)
123 fullPath = os.path.join(self.project.ppath, filePath, fileName)
124
125 if fullPath.endswith('.ui'):
126 self.designerFile.emit(fullPath)
127 elif fullPath.endswith(('.ts', '.qm')):
128 self.linguistFile.emit(fullPath)
129 else:
130 self.sourceFile.emit(fullPath)
131 return True
132
133 return False
134
135 def __generateLocations(self):
136 """
137 Private method to generate a set of locations that can be searched.
138
139 @return yields set of files in our project...
140 @rtype str
141 """
142 for typ in ["SOURCES", "FORMS", "INTERFACES", "RESOURCES",
143 "TRANSLATIONS", "OTHERS"]:
144 entries = self.project.pdata.get(typ)
145 for entry in entries[:]:
146 yield entry
147
148 def __sortedMatches(self, items, searchTerm):
149 """
150 Private method to find the subset of items which match a search term.
151
152 @param items list of items to be scanned for the search term
153 @type list of str
154 @param searchTerm search term to be searched for
155 @type str
156 @return sorted subset of items which match searchTerm in
157 relevance order (i.e. the most likely match first)
158 @rtype list of tuple of bool, int and str
159 """
160 fragments = searchTerm.split()
161
162 possible = [
163 # matches, in_order, file name
164 ]
165
166 for entry in items:
167 count = 0
168 match_order = []
169 for fragment in fragments:
170 index = entry.find(fragment)
171 if index == -1:
172 # try case-insensitive match
173 index = entry.lower().find(fragment.lower())
174 if index != -1:
175 count += 1
176 match_order.append(index)
177 if count:
178 record = (count, match_order == sorted(match_order), entry)
179 if possible and count < possible[0][0]:
180 # ignore...
181 continue
182 elif possible and count > possible[0][0]:
183 # better than all previous matches, discard them and
184 # keep this
185 del possible[:]
186 possible.append(record)
187
188 ordered = []
189 for (_, in_order, name) in possible:
190 try:
191 age = os.stat(os.path.join(self.project.ppath, name)).st_mtime
192 except (IOError, OSError):
193 # skipping, because it doesn't appear to exist...
194 continue
195 ordered.append((
196 in_order, # we want closer match first
197 - age, # then approximately "most recently edited"
198 name
199 ))
200 ordered.sort()
201 return ordered
202
203 def __searchFile(self):
204 """
205 Private slot to handle the search.
206 """
207 fileName = self.fileNameEdit.text().strip()
208 if not fileName:
209 self.fileList.clear()
210 return
211
212 ordered = self.__sortedMatches(self.__generateLocations(), fileName)
213
214 found = False
215 self.fileList.clear()
216 locations = {}
217
218 for in_order, age, name in ordered:
219 found = True
220 QTreeWidgetItem(self.fileList, [os.path.basename(name),
221 os.path.dirname(name)])
222 QApplication.processEvents()
223
224 del locations
225 self.stopButton.setEnabled(False)
226 self.fileList.header().resizeSections(QHeaderView.ResizeToContents)
227 self.fileList.header().setStretchLastSection(True)
228
229 if found:
230 self.fileList.setCurrentItem(self.fileList.topLevelItem(0))
231
232 def on_fileNameEdit_textChanged(self, text):
233 """
234 Private slot to handle the textChanged signal of the file name edit.
235
236 @param text (ignored)
237 """
238 self.__searchFile()
239
240 def on_fileNameEdit_returnPressed(self):
241 """
242 Private slot to handle enter being pressed on the file name edit box.
243 """
244 if self.__openFile():
245 self.close()
246
247 def on_fileList_itemActivated(self, itm, column):
248 """
249 Private slot to handle the double click on a file item.
250
251 It emits the signal sourceFile or designerFile depending on the
252 file extension.
253
254 @param itm the double clicked listview item (QTreeWidgetItem)
255 @param column column that was double clicked (integer) (ignored)
256 """
257 self.__openFile(itm)
258
259 @pyqtSlot(QTreeWidgetItem, QTreeWidgetItem)
260 def on_fileList_currentItemChanged(self, current, previous):
261 """
262 Private slot handling a change of the current item.
263
264 @param current current item (QTreeWidgetItem)
265 @param previous prevoius current item (QTreeWidgetItem)
266 """
267 self.buttonBox.button(QDialogButtonBox.Open).setEnabled(
268 current is not None)
269
270 def show(self):
271 """
272 Public method to enable/disable the project checkbox.
273 """
274 self.fileNameEdit.selectAll()
275 self.fileNameEdit.setFocus()
276
277 super(QuickFindFileDialog, self).show()

eric ide

mercurial