eric7/Project/QuickFindFileDialog.py

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

eric ide

mercurial