4 from PyQt4
import QtCore
5 from PyQt4
import QtGui
6 from PyQt4
.QtCore
import Qt
7 from PyQt4
.QtCore
import SIGNAL
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
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
,
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)'),
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
)
48 self
._completer
.setCaseSensitivity(Qt
.CaseSensitive
)
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
):
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.
67 if words
and not self
._ends
_with
_whitespace
():
69 words
.append(unicode(completion
))
70 self
.setText(subprocess
.list2cmdline(words
))
71 self
.emit(SIGNAL('ref_changed'))
74 return utils
.shell_usplit(unicode(self
.text()))
76 def _ends_with_whitespace(self
):
77 text
= unicode(self
.text())
78 return text
.rstrip() != text
81 if self
._ends
_with
_whitespace
():
85 return unicode(self
.text())
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()):
96 if (event
.key() in (Qt
.Key_Return
, Qt
.Key_Enter
) and
97 not self
._completer
.popup().isVisible()):
98 self
.emit(SIGNAL('returnPressed()'))
101 if event
.type() == QtCore
.QEvent
.Hide
:
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
:
114 self
._complete
(self
._last
_word
())
117 elif (event
.key() == Qt
.Key_Down
and
118 self
._completer
.completionCount() > 0):
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
):
136 return QtGui
.QLineEdit
.mousePressEvent(self
, event
)
138 def mouseMoveEvent(self
, event
):
141 return QtGui
.QLineEdit
.mouseMoveEvent(self
, event
)
143 def mouseReleaseEvent(self
, event
):
144 if self
._drag
!= 2 and event
.button() != Qt
.RightButton
:
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))
166 self
._completer
.dispose()
169 class GatherCompletionsThread(QtCore
.QThread
):
170 def __init__(self
, model
):
171 QtCore
.QThread
.__init
__(self
)
173 self
.case_sensitive
= False
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
)
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()
197 self
.doc
.setDocumentMargin(0)
198 except: # older PyQt4
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
)
216 match
= re
.match('(.*)(' + self
.highlight_text
+ ')(.*)',
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
)
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
)
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
)
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'),
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('.',''))
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
295 for ref
in matched_refs
:
296 item
= QStandardItem()
298 item
.setIcon(git_icon
)
301 if matched_paths
and (not matched_text
or matched_text
in '--'):
302 item
= QStandardItem()
304 item
.setIcon(file_icon
)
307 for match
in matched_paths
:
308 item
= QStandardItem()
311 item
.setIcon(dir_icon
)
313 item
.setIcon(file_icon
)
317 self
.invisibleRootItem().appendRows(items
)
320 class Completer(QtGui
.QCompleter
):
321 def __init__(self
, parent
):
322 QtGui
.QCompleter
.__init
__(self
, parent
)
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
)
337 self
.model().update()
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()'))
357 model
= self
.cola_model
358 return model
.local_branches
+ model
.remote_branches
+ model
.tags
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
367 if not case_sensitive
:
368 matched_refs
= [r
for r
in refs
if matched_text
in r
]
370 matched_refs
= [r
for r
in refs
371 if matched_text
.lower() in r
.lower()]
375 if self
.case_sensitive
:
376 matched_refs
.sort(cmp=self
.completion_cmp
)
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
398 matched_paths
= [f
for f
in files_and_dirs
399 if matched_text
in f
]
401 matched_paths
= [f
for f
in files_and_dirs
402 if matched_text
.lower() in f
.lower()]
404 matched_paths
= list(files_and_dirs
)
406 if self
.case_sensitive
:
407 matched_paths
.sort(cmp=self
.completion_cmp
)
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
))