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