dag: Search when 'enter' is pressed
[git-cola.git] / cola / widgets / completion.py
bloba2ca604750cf47da94954ea2d2b93f8a454f490b
1 import re
2 import subprocess
4 from PyQt4 import QtCore
5 from PyQt4 import QtGui
6 from PyQt4.QtCore import Qt
7 from PyQt4.QtCore import SIGNAL
9 import cola
10 from cola import qtutils
11 from cola import utils
12 from cola.compat import set
15 class CompletionLineEdit(QtGui.QLineEdit):
16 def __init__(self, parent=None):
17 from cola.prefs import diff_font
19 QtGui.QLineEdit.__init__(self, parent)
21 self.setFont(diff_font())
22 # used to hide the completion popup after a drag-select
23 self._drag = 0
25 self._completer = None
26 self._delegate = HighlightDelegate(self)
27 self.connect(self, SIGNAL('textChanged(QString)'), self._text_changed)
28 self._keys_to_ignore = set([Qt.Key_Enter, Qt.Key_Return,
29 Qt.Key_Escape])
31 def value(self):
32 return unicode(self.text())
34 def setCompleter(self, completer):
35 self._completer = completer
36 completer.setWidget(self)
37 completer.popup().setItemDelegate(self._delegate)
38 self.connect(self._completer, SIGNAL('activated(QString)'),
39 self._complete)
41 def _is_case_sensitive(self, text):
42 return bool([char for char in text if char.isupper()])
44 def _text_changed(self, text):
45 text = self._last_word()
46 case_sensitive = self._is_case_sensitive(text)
47 if case_sensitive:
48 self._completer.setCaseSensitivity(Qt.CaseSensitive)
49 else:
50 self._completer.setCaseSensitivity(Qt.CaseInsensitive)
51 self._delegate.set_highlight_text(text, case_sensitive)
52 self._completer.set_match_text(text, case_sensitive)
54 def update_matches(self):
55 text = self._last_word()
56 case_sensitive = self._is_case_sensitive(text)
57 self._completer.model().update_matches(case_sensitive)
59 def _complete(self, completion):
60 """
61 This is the event handler for the QCompleter.activated(QString) signal,
62 it is called when the user selects an item in the completer popup.
63 """
64 if not completion:
65 return
66 words = self._words()
67 if words and not self._ends_with_whitespace():
68 words.pop()
69 words.append(unicode(completion))
70 self.setText(subprocess.list2cmdline(words))
71 self.emit(SIGNAL('ref_changed'))
73 def _words(self):
74 return utils.shell_usplit(unicode(self.text()))
76 def _ends_with_whitespace(self):
77 text = unicode(self.text())
78 return text.rstrip() != text
80 def _last_word(self):
81 if self._ends_with_whitespace():
82 return u''
83 words = self._words()
84 if not words:
85 return unicode(self.text())
86 if not words[-1]:
87 return u''
88 return words[-1]
90 def event(self, event):
91 if event.type() == QtCore.QEvent.KeyPress:
92 if (event.key() == Qt.Key_Tab and
93 self._completer.popup().isVisible()):
94 event.ignore()
95 return True
96 if (event.key() in (Qt.Key_Return, Qt.Key_Enter) and
97 not self._completer.popup().isVisible()):
98 self.emit(SIGNAL('returnPressed()'))
99 event.accept()
100 return True
101 if event.type() == QtCore.QEvent.Hide:
102 self.close_popup()
103 return QtGui.QLineEdit.event(self, event)
105 def do_completion(self):
106 self._completer.popup().setCurrentIndex(
107 self._completer.model().index(0,0))
108 self._completer.complete()
110 def keyPressEvent(self, event):
111 if self._completer.popup().isVisible():
112 if event.key() in self._keys_to_ignore:
113 event.ignore()
114 self._complete(self._last_word())
115 return
117 elif (event.key() == Qt.Key_Down and
118 self._completer.completionCount() > 0):
119 event.accept()
120 self.do_completion()
121 return
123 QtGui.QLineEdit.keyPressEvent(self, event)
125 prefix = self._last_word()
126 if prefix != unicode(self._completer.completionPrefix()):
127 self._update_popup_items(prefix)
128 if len(event.text()) > 0 and len(prefix) > 0:
129 self._completer.complete()
130 #if len(prefix) == 0:
131 # self._completer.popup().hide()
133 #: _drag: 0 - unclicked, 1 - clicked, 2 - dragged
134 def mousePressEvent(self, event):
135 self._drag = 1
136 return QtGui.QLineEdit.mousePressEvent(self, event)
138 def mouseMoveEvent(self, event):
139 if self._drag == 1:
140 self._drag = 2
141 return QtGui.QLineEdit.mouseMoveEvent(self, event)
143 def mouseReleaseEvent(self, event):
144 if self._drag != 2 and event.button() != Qt.RightButton:
145 self.do_completion()
146 self._drag = 0
147 return QtGui.QLineEdit.mouseReleaseEvent(self, event)
149 def close_popup(self):
150 if self._completer.popup().isVisible():
151 self._completer.popup().close()
153 def _update_popup_items(self, prefix):
155 Filters the completer's popup items to only show items
156 with the given prefix.
158 self._completer.setCompletionPrefix(prefix)
159 self._completer.popup().setCurrentIndex(
160 self._completer.model().index(0,0))
162 def __del__(self):
163 self.dispose()
165 def dispose(self):
166 self._completer.dispose()
169 class GatherCompletionsThread(QtCore.QThread):
170 def __init__(self, model):
171 QtCore.QThread.__init__(self)
172 self.model = model
173 self.case_sensitive = False
175 def run(self):
176 text = None
177 # Loop when the matched text changes between the start and end time.
178 # This happens when gather_matches() takes too long and the
179 # model's matched_text changes in-between.
180 while text != self.model.matched_text:
181 text = self.model.matched_text
182 items = self.model.gather_matches(self.case_sensitive)
184 if text is not None:
185 self.emit(SIGNAL('items_gathered'), items)
188 class HighlightDelegate(QtGui.QStyledItemDelegate):
189 """A delegate used for auto-completion to give formatted completion"""
190 def __init__(self, parent=None): # model, parent=None):
191 QtGui.QStyledItemDelegate.__init__(self, parent)
192 self.highlight_text = ''
193 self.case_sensitive = False
195 self.doc = QtGui.QTextDocument()
196 try:
197 self.doc.setDocumentMargin(0)
198 except: # older PyQt4
199 pass
201 def set_highlight_text(self, text, case_sensitive):
202 """Sets the text that will be made bold in the term name when displayed"""
203 self.highlight_text = text
204 self.case_sensitive = case_sensitive
206 def paint(self, painter, option, index):
207 """Overloaded Qt method for custom painting of a model index"""
208 if not self.highlight_text:
209 return QtGui.QStyledItemDelegate.paint(self, painter, option, index)
211 text = unicode(index.data().toPyObject())
212 if self.case_sensitive:
213 html = text.replace(self.highlight_text,
214 '<strong>%s</strong>' % self.highlight_text)
215 else:
216 match = re.match('(.*)(' + self.highlight_text + ')(.*)',
217 text, re.IGNORECASE)
218 if match:
219 start = match.group(1) or ''
220 middle = match.group(2) or ''
221 end = match.group(3) or ''
222 html = (start + ('<strong>%s</strong>' % middle) + end)
223 else:
224 html = text
225 self.doc.setHtml(html)
227 # Painting item without text, Text Document will paint the text
228 optionV4 = QtGui.QStyleOptionViewItemV4(option)
229 self.initStyleOption(optionV4, index)
230 optionV4.text = QtCore.QString()
232 style = QtGui.QApplication.style()
233 style.drawControl(QtGui.QStyle.CE_ItemViewItem, optionV4, painter)
234 ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
236 # Highlighting text if item is selected
237 if (optionV4.state & QtGui.QStyle.State_Selected):
238 ctx.palette.setColor(QtGui.QPalette.Text, optionV4.palette.color(QtGui.QPalette.Active, QtGui.QPalette.HighlightedText))
240 # translate the painter to where the text is drawn
241 textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText, optionV4)
242 painter.save()
244 start = textRect.topLeft() + QtCore.QPoint(3, 0)
245 painter.translate(start)
246 painter.setClipRect(textRect.translated(-start))
248 # tell the text document to draw the html for us
249 self.doc.documentLayout().draw(painter, ctx)
250 painter.restore()
253 class CompletionModel(QtGui.QStandardItemModel):
254 def __init__(self, parent):
255 QtGui.QStandardItemModel.__init__(self, parent)
256 self.matched_text = ''
257 self.case_sensitive = False
259 self.update_thread = GatherCompletionsThread(self)
260 self.connect(self.update_thread, SIGNAL('items_gathered'),
261 self.apply_matches)
263 def lower_completion_cmp(self, a, b):
264 return cmp(a.replace('.','').lower(), a.replace('.','').lower())
266 def completion_cmp(self, a, b):
267 return cmp(a.replace('.',''), a.replace('.',''))
269 def update(self):
270 case_sensitive = self.update_thread.case_sensitive
271 self.update_matches(case_sensitive)
273 def set_match_text(self, matched_text, case_sensitive):
274 self.matched_text = matched_text
275 self.update_matches(case_sensitive)
277 def update_matches(self, case_sensitive):
278 self.case_sensitive = case_sensitive
279 self.update_thread.case_sensitive = case_sensitive
280 if not self.update_thread.isRunning():
281 self.update_thread.start()
283 def gather_matches(self, case_sensitive):
284 return ((), (), set())
286 def apply_matches(self, match_tuple):
287 matched_refs, matched_paths, dirs = match_tuple
288 QStandardItem = QtGui.QStandardItem
289 file_icon = qtutils.file_icon()
290 dir_icon = qtutils.dir_icon()
291 git_icon = qtutils.git_icon()
293 matched_text = self.matched_text
294 items = []
295 for ref in matched_refs:
296 item = QStandardItem()
297 item.setText(ref)
298 item.setIcon(git_icon)
299 items.append(item)
301 if matched_paths and (not matched_text or matched_text in '--'):
302 item = QStandardItem()
303 item.setText('--')
304 item.setIcon(file_icon)
305 items.append(item)
307 for match in matched_paths:
308 item = QStandardItem()
309 item.setText(match)
310 if match in dirs:
311 item.setIcon(dir_icon)
312 else:
313 item.setIcon(file_icon)
314 items.append(item)
316 self.clear()
317 self.invisibleRootItem().appendRows(items)
320 class Completer(QtGui.QCompleter):
321 def __init__(self, parent):
322 QtGui.QCompleter.__init__(self, parent)
323 self._model = None
324 self.setWidget(parent)
325 self.setCompletionMode(QtGui.QCompleter.UnfilteredPopupCompletion)
326 self.setCaseSensitivity(Qt.CaseInsensitive)
328 def setModel(self, model):
329 QtGui.QCompleter.setModel(self, model)
330 self.connect(model, SIGNAL('updated()'), self.updated)
331 self._model = model
333 def model(self):
334 return self._model
336 def updated(self):
337 self.model().update()
339 def dispose(self):
340 self.model().dispose()
342 def set_match_text(self, matched_text, case_sensitive):
343 self.model().set_match_text(matched_text, case_sensitive)
346 class GitRefCompletionModel(CompletionModel):
347 def __init__(self, parent):
348 CompletionModel.__init__(self, parent)
349 self.cola_model = model = cola.model()
350 msg = model.message_updated
351 model.add_observer(msg, self.emit_updated)
353 def emit_updated(self):
354 self.emit(SIGNAL('updated()'))
356 def matches(self):
357 model = self.cola_model
358 return model.local_branches + model.remote_branches + model.tags
360 def dispose(self):
361 self.cola_model.remove_observer(self.emit_updated)
363 def gather_matches(self, case_sensitive):
364 refs = self.matches()
365 matched_text = self.matched_text
366 if matched_text:
367 if not case_sensitive:
368 matched_refs = [r for r in refs if matched_text in r]
369 else:
370 matched_refs = [r for r in refs
371 if matched_text.lower() in r.lower()]
372 else:
373 matched_refs = refs
375 if self.case_sensitive:
376 matched_refs.sort(cmp=self.completion_cmp)
377 else:
378 matched_refs.sort(cmp=self.lower_completion_cmp)
379 return (matched_refs, (), set())
382 class GitLogCompletionModel(GitRefCompletionModel):
383 def __init__(self, parent):
384 GitRefCompletionModel.__init__(self, parent)
386 def gather_matches(self, case_sensitive):
387 (matched_refs, dummy_paths, dummy_dirs) =\
388 GitRefCompletionModel.gather_matches(self, case_sensitive)
390 file_list = self.cola_model.everything()
391 files = set(file_list)
392 files_and_dirs = utils.add_parents(set(files))
394 dirs = files_and_dirs.difference(files)
395 matched_text = self.matched_text
396 if matched_text:
397 if case_sensitive:
398 matched_paths = [f for f in files_and_dirs
399 if matched_text in f]
400 else:
401 matched_paths = [f for f in files_and_dirs
402 if matched_text.lower() in f.lower()]
403 else:
404 matched_paths = list(files_and_dirs)
406 if self.case_sensitive:
407 matched_paths.sort(cmp=self.completion_cmp)
408 else:
409 matched_paths.sort(cmp=self.lower_completion_cmp)
410 return (matched_refs, matched_paths, dirs)
413 class GitLogCompleter(Completer):
414 def __init__(self, parent):
415 Completer.__init__(self, parent)
416 self.setModel(GitLogCompletionModel(self))
419 class GitRefCompleter(Completer):
420 def __init__(self, parent):
421 Completer.__init__(self, parent)
422 self.setModel(GitRefCompletionModel(self))
425 class GitLogLineEdit(CompletionLineEdit):
426 def __init__(self, parent=None):
427 CompletionLineEdit.__init__(self, parent)
428 self.setCompleter(GitLogCompleter(self))
431 class GitRefLineEdit(CompletionLineEdit):
432 def __init__(self, parent=None):
433 CompletionLineEdit.__init__(self, parent)
434 self.setCompleter(GitRefCompleter(self))