resources: remove unnecessary os.path.dirname import
[git-cola.git] / cola / widgets / completion.py
blobad1222f7d24492ce4b98fe4f0cd86996c2ed7c55
1 import re
2 import time
4 from qtpy import QtCore
5 from qtpy import QtGui
6 from qtpy import QtWidgets
7 from qtpy.QtCore import Qt
8 from qtpy.QtCore import Signal
10 from ..models import prefs
11 from .. import core
12 from .. import gitcmds
13 from .. import icons
14 from .. import qtutils
15 from .. import utils
16 from . import defs
17 from .text import HintedLineEdit
20 class ValidateRegex:
21 def __init__(self, regex):
22 self.regex = re.compile(regex) # regex to scrub
24 def validate(self, string, idx):
25 """Scrub and validate the user-supplied input"""
26 state = QtGui.QValidator.Acceptable
27 if self.regex.search(string):
28 string = self.regex.sub('', string) # scrub matching bits
29 idx = min(idx - 1, len(string))
30 return (state, string, idx)
33 class RemoteValidator(QtGui.QValidator):
34 """Prevent invalid remote names"""
36 def __init__(self, parent=None):
37 super().__init__(parent)
38 self._validate = ValidateRegex(r'[ \t\\/]')
40 def validate(self, string, idx):
41 return self._validate.validate(string, idx)
44 class BranchValidator(QtGui.QValidator):
45 """Prevent invalid branch names"""
47 def __init__(self, git, parent=None):
48 super().__init__(parent)
49 self._git = git
50 self._validate = ValidateRegex(r'[ \t\\]') # forward-slash is okay
52 def validate(self, string, idx):
53 """Scrub and validate the user-supplied input"""
54 state, string, idx = self._validate.validate(string, idx)
55 if string: # Allow empty strings
56 status, _, _ = self._git.check_ref_format(string, branch=True)
57 if status != 0:
58 # The intermediate string, when deleting characters, might
59 # end in a name that is invalid to Git, but we must allow it
60 # otherwise we won't be able to delete it using backspace.
61 if string.endswith('/') or string.endswith('.'):
62 state = self.Intermediate
63 else:
64 state = self.Invalid
65 return (state, string, idx)
68 def _is_case_sensitive(text):
69 return bool([char for char in text if char.isupper()])
72 class CompletionLineEdit(HintedLineEdit):
73 """A lineedit with advanced completion abilities"""
75 activated = Signal()
76 changed = Signal()
77 cleared = Signal()
78 enter = Signal()
79 up = Signal()
80 down = Signal()
82 # Activation keys will cause a selected completion item to be chosen
83 ACTIVATION_KEYS = (Qt.Key_Return, Qt.Key_Enter)
85 # Navigation keys trigger signals that widgets can use for customization
86 NAVIGATION_KEYS = {
87 Qt.Key_Return: 'enter',
88 Qt.Key_Enter: 'enter',
89 Qt.Key_Up: 'up',
90 Qt.Key_Down: 'down',
93 def __init__(
94 self, context, model_factory, hint='', show_all_completions=False, parent=None
96 HintedLineEdit.__init__(self, context, hint, parent=parent)
97 # Tracks when the completion popup was active during key events
99 self.context = context
100 # The most recently selected completion item
101 self._selection = None
102 self._show_all_completions = show_all_completions
104 # Create a completion model
105 completion_model = model_factory(context, self)
106 completer = Completer(completion_model, self)
107 completer.setWidget(self)
108 self._completer = completer
109 self._completion_model = completion_model
111 # The delegate highlights matching completion text in the popup widget
112 self._delegate = HighlightDelegate(self)
113 completer.popup().setItemDelegate(self._delegate)
115 self.textChanged.connect(self._text_changed)
116 self._completer.activated.connect(self.choose_completion)
117 self._completion_model.updated.connect(
118 self._completions_updated, type=Qt.QueuedConnection
120 self.destroyed.connect(self.dispose)
122 def __del__(self):
123 self.dispose()
125 def dispose(self, *args):
126 self._completer.dispose()
128 def completion_selection(self):
129 """Return the last completion's selection"""
130 return self._selection
132 def complete(self):
133 """Trigger the completion popup to appear and offer completions"""
134 self._completer.complete()
136 def refresh(self):
137 """Refresh the completion model"""
138 return self._completer.model().update()
140 def popup(self):
141 """Return the completer's popup"""
142 return self._completer.popup()
144 def _text_changed(self, full_text):
145 match_text = self._last_word()
146 self._do_text_changed(full_text, match_text)
147 self.complete_last_word()
149 def _do_text_changed(self, full_text, match_text):
150 case_sensitive = _is_case_sensitive(match_text)
151 if case_sensitive:
152 self._completer.setCaseSensitivity(Qt.CaseSensitive)
153 else:
154 self._completer.setCaseSensitivity(Qt.CaseInsensitive)
155 self._delegate.set_highlight_text(match_text, case_sensitive)
156 self._completer.set_match_text(full_text, match_text, case_sensitive)
158 def update_matches(self):
159 text = self._last_word()
160 case_sensitive = _is_case_sensitive(text)
161 self._completer.setCompletionPrefix(text)
162 self._completer.model().update_matches(case_sensitive)
164 def choose_completion(self, completion):
166 This is the event handler for the QCompleter.activated(QString) signal,
167 it is called when the user selects an item in the completer popup.
169 if not completion:
170 self._do_text_changed('', '')
171 return
172 words = self._words()
173 if words and not self._ends_with_whitespace():
174 words.pop()
176 words.append(completion)
177 text = core.list2cmdline(words)
178 self.setText(text)
179 self.changed.emit()
180 self._do_text_changed(text, '')
181 self.popup().hide()
183 def _words(self):
184 return utils.shell_split(self.value())
186 def _ends_with_whitespace(self):
187 value = self.value()
188 return value != value.rstrip()
190 def _last_word(self):
191 if self._ends_with_whitespace():
192 return ''
193 words = self._words()
194 if not words:
195 return self.value()
196 if not words[-1]:
197 return ''
198 return words[-1]
200 def complete_last_word(self):
201 self.update_matches()
202 self.complete()
204 def close_popup(self):
205 """Close the completion popup"""
206 self.popup().close()
208 def _completions_updated(self):
209 """Select the first completion item when completions are updated"""
210 popup = self.popup()
211 if self._completion_model.rowCount() == 0:
212 popup.hide()
213 return
214 if not popup.isVisible():
215 if not self.hasFocus() or not self._show_all_completions:
216 return
217 self.select_first_completion()
219 def select_first_completion(self):
220 """Select the first item in the completion model"""
221 idx = self._completion_model.index(0, 0)
222 mode = (
223 QtCore.QItemSelectionModel.Rows | QtCore.QItemSelectionModel.SelectCurrent
225 self.popup().selectionModel().setCurrentIndex(idx, mode)
227 def selected_completion(self):
228 """Return the selected completion item"""
229 popup = self.popup()
230 if not popup.isVisible():
231 return None
232 model = popup.selectionModel()
233 indexes = model.selectedIndexes()
234 if not indexes:
235 return None
236 idx = indexes[0]
237 item = self._completion_model.itemFromIndex(idx)
238 if not item:
239 return None
240 return item.text()
242 def select_completion(self):
243 """Choose the selected completion option from the completion popup"""
244 result = False
245 visible = self.popup().isVisible()
246 if visible:
247 selection = self.selected_completion()
248 if selection:
249 self.choose_completion(selection)
250 result = True
251 return result
253 def show_popup(self):
254 """Display the completion popup"""
255 self.refresh()
256 x_val = self.x()
257 y_val = self.y() + self.height()
258 point = QtCore.QPoint(x_val, y_val)
259 mapped = self.parent().mapToGlobal(point)
260 popup = self.popup()
261 popup.move(mapped.x(), mapped.y())
262 popup.show()
264 # Qt overrides
265 def event(self, event):
266 """Override QWidget::event() for tab completion"""
267 event_type = event.type()
269 if (
270 event_type == QtCore.QEvent.KeyPress
271 and event.key() == Qt.Key_Tab
272 and self.select_completion()
274 return True
276 # Make sure the popup goes away during teardown
277 if event_type == QtCore.QEvent.Close:
278 self.close_popup()
279 elif event_type == QtCore.QEvent.Hide:
280 self.popup().hide()
282 return super().event(event)
284 def keyPressEvent(self, event):
285 """Process completion and navigation events"""
286 super().keyPressEvent(event)
288 popup = self.popup()
289 visible = popup.isVisible()
291 # Hide the popup when the field becomes empty.
292 is_empty = not self.value()
293 if is_empty and event.modifiers() != Qt.ControlModifier:
294 self.cleared.emit()
295 if visible:
296 popup.hide()
298 # Activation keys select the completion when pressed and emit the
299 # activated signal. Navigation keys have lower priority, and only
300 # emit when it wasn't already handled as an activation event.
301 key = event.key()
302 if key in self.ACTIVATION_KEYS and visible:
303 if self.select_completion():
304 self.activated.emit()
305 return
307 navigation = self.NAVIGATION_KEYS.get(key, None)
308 if navigation:
309 signal = getattr(self, navigation)
310 signal.emit()
311 return
313 # Show the popup when Ctrl-Space is pressed.
314 if (
315 not visible
316 and key == Qt.Key_Space
317 and event.modifiers() == Qt.ControlModifier
319 self.show_popup()
322 class GatherCompletionsThread(QtCore.QThread):
323 items_gathered = Signal(object)
325 def __init__(self, model):
326 QtCore.QThread.__init__(self)
327 self.model = model
328 self.case_sensitive = False
329 self.running = False
331 def dispose(self):
332 self.running = False
333 utils.catch_runtime_error(self.wait)
335 def run(self):
336 text = None
337 items = []
338 self.running = True
339 # Loop when the matched text changes between the start and end time.
340 # This happens when gather_matches() takes too long and the
341 # model's match_text changes in-between.
342 while self.running and text != self.model.match_text:
343 text = self.model.match_text
344 items = self.model.gather_matches(self.case_sensitive)
346 if self.running and text is not None:
347 self.items_gathered.emit(items)
350 class HighlightDelegate(QtWidgets.QStyledItemDelegate):
351 """A delegate used for auto-completion to give formatted completion"""
353 def __init__(self, parent):
354 QtWidgets.QStyledItemDelegate.__init__(self, parent)
355 self.widget = parent
356 self.highlight_text = ''
357 self.case_sensitive = False
359 self.doc = QtGui.QTextDocument()
360 # older PyQt4 does not have setDocumentMargin
361 if hasattr(self.doc, 'setDocumentMargin'):
362 self.doc.setDocumentMargin(0)
364 def set_highlight_text(self, text, case_sensitive):
365 """Sets the text that will be made bold when displayed"""
366 self.highlight_text = text
367 self.case_sensitive = case_sensitive
369 def paint(self, painter, option, index):
370 """Overloaded Qt method for custom painting of a model index"""
371 if not self.highlight_text:
372 QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
373 return
374 text = index.data()
375 if self.case_sensitive:
376 html = text.replace(
377 self.highlight_text, '<strong>%s</strong>' % self.highlight_text
379 else:
380 match = re.match(
381 r'(.*)(%s)(.*)' % re.escape(self.highlight_text), text, re.IGNORECASE
383 if match:
384 start = match.group(1) or ''
385 middle = match.group(2) or ''
386 end = match.group(3) or ''
387 html = start + ('<strong>%s</strong>' % middle) + end
388 else:
389 html = text
390 self.doc.setHtml(html)
392 # Painting item without text, Text Document will paint the text
393 params = QtWidgets.QStyleOptionViewItem(option)
394 self.initStyleOption(params, index)
395 params.text = ''
397 style = QtWidgets.QApplication.style()
398 style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, params, painter)
399 ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
401 # Highlighting text if item is selected
402 if params.state & QtWidgets.QStyle.State_Selected:
403 color = params.palette.color(
404 QtGui.QPalette.Active, QtGui.QPalette.HighlightedText
406 ctx.palette.setColor(QtGui.QPalette.Text, color)
408 # translate the painter to where the text is drawn
409 item_text = QtWidgets.QStyle.SE_ItemViewItemText
410 rect = style.subElementRect(item_text, params, self.widget)
411 painter.save()
413 start = rect.topLeft() + QtCore.QPoint(defs.margin, 0)
414 painter.translate(start)
416 # tell the text document to draw the html for us
417 self.doc.documentLayout().draw(painter, ctx)
418 painter.restore()
421 def ref_sort_key(ref):
422 """Sort key function that causes shorter refs to sort first, but
423 alphabetizes refs of equal length (in order to make local branches sort
424 before remote ones)."""
425 return len(ref), ref
428 class CompletionModel(QtGui.QStandardItemModel):
429 updated = Signal()
430 items_gathered = Signal(object)
431 model_updated = Signal()
433 def __init__(self, context, parent):
434 QtGui.QStandardItemModel.__init__(self, parent)
435 self.context = context
436 self.match_text = ''
437 self.full_text = ''
438 self.case_sensitive = False
440 self.update_thread = GatherCompletionsThread(self)
441 self.update_thread.items_gathered.connect(
442 self.apply_matches, type=Qt.QueuedConnection
445 def update(self):
446 case_sensitive = self.update_thread.case_sensitive
447 self.update_matches(case_sensitive)
449 def set_match_text(self, full_text, match_text, case_sensitive):
450 self.full_text = full_text
451 self.match_text = match_text
452 self.update_matches(case_sensitive)
454 def update_matches(self, case_sensitive):
455 self.case_sensitive = case_sensitive
456 self.update_thread.case_sensitive = case_sensitive
457 if not self.update_thread.isRunning():
458 self.update_thread.start()
460 def gather_matches(self, case_sensitive):
461 return ((), (), set())
463 def apply_matches(self, match_tuple):
464 """Build widgets for all of the matching items"""
465 if not match_tuple:
466 # Results from background tasks may arrive after the widget
467 # has been destroyed.
468 utils.catch_runtime_error(self.set_items, [])
469 return
470 matched_refs, matched_paths, dirs = match_tuple
471 QStandardItem = QtGui.QStandardItem
473 dir_icon = icons.directory()
474 git_icon = icons.cola()
476 items = []
477 for ref in matched_refs:
478 item = QStandardItem()
479 item.setText(ref)
480 item.setIcon(git_icon)
481 items.append(item)
483 from_filename = icons.from_filename
484 for match in matched_paths:
485 item = QStandardItem()
486 item.setText(match)
487 if match in dirs:
488 item.setIcon(dir_icon)
489 else:
490 item.setIcon(from_filename(match))
491 items.append(item)
493 # Results from background tasks can arrive after the widget has been destroyed.
494 utils.catch_runtime_error(self.set_items, items)
496 def set_items(self, items):
497 """Clear the widget and add items to the model"""
498 self.clear()
499 self.invisibleRootItem().appendRows(items)
500 self.updated.emit()
502 def dispose(self):
503 self.update_thread.dispose()
506 def _identity(value):
507 return value
510 def _lower(value):
511 return value.lower()
514 def filter_matches(match_text, candidates, case_sensitive, sort_key=None):
515 """Filter candidates and return the matches"""
516 if case_sensitive:
517 case_transform = _identity
518 else:
519 case_transform = _lower
521 if match_text:
522 match_text = case_transform(match_text)
523 matches = [r for r in candidates if match_text in case_transform(r)]
524 else:
525 matches = list(candidates)
527 if case_sensitive:
528 if sort_key is None:
529 matches.sort()
530 else:
531 matches.sort(key=sort_key)
532 else:
533 if sort_key is None:
534 matches.sort(key=_lower)
535 else:
536 matches.sort(key=lambda x: sort_key(_lower(x)))
537 return matches
540 def filter_path_matches(match_text, file_list, case_sensitive):
541 """Return matching completions from a list of candidate files"""
542 files = set(file_list)
543 files_and_dirs = utils.add_parents(files)
544 dirs = files_and_dirs.difference(files)
546 paths = filter_matches(match_text, files_and_dirs, case_sensitive)
547 return (paths, dirs)
550 class Completer(QtWidgets.QCompleter):
551 def __init__(self, model, parent):
552 QtWidgets.QCompleter.__init__(self, parent)
553 self._model = model
554 self.setCompletionMode(QtWidgets.QCompleter.UnfilteredPopupCompletion)
555 self.setCaseSensitivity(Qt.CaseInsensitive)
556 self.setFilterMode(QtCore.Qt.MatchContains)
558 model.model_updated.connect(self.update, type=Qt.QueuedConnection)
559 self.setModel(model)
561 def update(self):
562 self._model.update()
564 def dispose(self):
565 self._model.dispose()
567 def set_match_text(self, full_text, match_text, case_sensitive):
568 self._model.set_match_text(full_text, match_text, case_sensitive)
571 class GitCompletionModel(CompletionModel):
572 def __init__(self, context, parent):
573 CompletionModel.__init__(self, context, parent)
574 self.context = context
575 context.model.updated.connect(self.model_updated, type=Qt.QueuedConnection)
577 def gather_matches(self, case_sensitive):
578 refs = filter_matches(
579 self.match_text, self.matches(), case_sensitive, sort_key=ref_sort_key
581 return (refs, (), set())
583 def matches(self):
584 return []
587 class GitRefCompletionModel(GitCompletionModel):
588 """Completer for branches and tags"""
590 def __init__(self, context, parent):
591 GitCompletionModel.__init__(self, context, parent)
592 context.model.refs_updated.connect(self.model_updated, type=Qt.QueuedConnection)
594 def matches(self):
595 model = self.context.model
596 return model.local_branches + model.remote_branches + model.tags
599 def find_potential_branches(model):
600 remotes = model.remotes
601 remote_branches = model.remote_branches
603 ambiguous = set()
604 allnames = set(model.local_branches)
605 potential = []
607 for remote_branch in remote_branches:
608 branch = gitcmds.strip_remote(remotes, remote_branch)
609 if branch in allnames or branch == remote_branch:
610 ambiguous.add(branch)
611 continue
612 potential.append(branch)
613 allnames.add(branch)
615 potential_branches = [p for p in potential if p not in ambiguous]
616 return potential_branches
619 class GitCreateBranchCompletionModel(GitCompletionModel):
620 """Completer for naming new branches"""
622 def matches(self):
623 model = self.context.model
624 potential_branches = find_potential_branches(model)
625 return model.local_branches + potential_branches + model.tags
628 class GitCheckoutBranchCompletionModel(GitCompletionModel):
629 """Completer for git checkout <branch>"""
631 def matches(self):
632 model = self.context.model
633 potential_branches = find_potential_branches(model)
634 return (
635 model.local_branches
636 + potential_branches
637 + model.remote_branches
638 + model.tags
642 class GitBranchCompletionModel(GitCompletionModel):
643 """Completer for local branches"""
645 def __init__(self, context, parent):
646 GitCompletionModel.__init__(self, context, parent)
648 def matches(self):
649 model = self.context.model
650 return model.local_branches
653 class GitRemoteBranchCompletionModel(GitCompletionModel):
654 """Completer for remote branches"""
656 def __init__(self, context, parent):
657 GitCompletionModel.__init__(self, context, parent)
659 def matches(self):
660 model = self.context.model
661 return model.remote_branches
664 class GitPathCompletionModel(GitCompletionModel):
665 """Base class for path completion"""
667 def __init__(self, context, parent):
668 GitCompletionModel.__init__(self, context, parent)
670 def candidate_paths(self):
671 return []
673 def gather_matches(self, case_sensitive):
674 paths, dirs = filter_path_matches(
675 self.match_text, self.candidate_paths(), case_sensitive
677 return ((), paths, dirs)
680 class GitStatusFilterCompletionModel(GitPathCompletionModel):
681 """Completer for modified files and folders for status filtering"""
683 def __init__(self, context, parent):
684 GitPathCompletionModel.__init__(self, context, parent)
686 def candidate_paths(self):
687 model = self.context.model
688 return model.staged + model.unmerged + model.modified + model.untracked
691 class GitTrackedCompletionModel(GitPathCompletionModel):
692 """Completer for tracked files and folders"""
694 def __init__(self, context, parent):
695 GitPathCompletionModel.__init__(self, context, parent)
696 self.model_updated.connect(self.gather_paths, type=Qt.QueuedConnection)
697 self._paths = []
699 def gather_paths(self):
700 context = self.context
701 self._paths = gitcmds.tracked_files(context)
703 def gather_matches(self, case_sensitive):
704 if not self._paths:
705 self.gather_paths()
707 refs = []
708 paths, dirs = filter_path_matches(self.match_text, self._paths, case_sensitive)
709 return (refs, paths, dirs)
712 class GitLogCompletionModel(GitRefCompletionModel):
713 """Completer for arguments suitable for git-log like commands"""
715 def __init__(self, context, parent):
716 GitRefCompletionModel.__init__(self, context, parent)
717 self._paths = []
718 self._model = context.model
719 self._runtask = qtutils.RunTask(parent=self)
720 self._time = 0.0 # ensure that the first event runs a task.
721 self.model_updated.connect(
722 self._start_gathering_paths, type=Qt.QueuedConnection
725 def matches(self):
726 """Return candidate values for completion"""
727 matches = super().matches()
728 return [
729 '--all',
730 '--all-match',
731 '--author',
732 '--after=two.days.ago',
733 '--basic-regexp',
734 '--before=two.days.ago',
735 '--branches',
736 '--committer',
737 '--exclude',
738 '--extended-regexp',
739 '--find-object',
740 '--first-parent',
741 '--fixed-strings',
742 '--full-diff',
743 '--grep',
744 '--invert-grep',
745 '--merges',
746 '--no-merges',
747 '--not',
748 '--perl-regexp',
749 '--pickaxe-all',
750 '--pickaxe-regex',
751 '--regexp-ignore-case',
752 '--tags',
753 '-D',
754 '-E',
755 '-F',
756 '-G',
757 '-P',
758 '-S',
759 '@{upstream}',
760 ] + matches
762 def _start_gathering_paths(self):
763 """Gather paths when the model changes"""
764 # Debounce updates that land within 1 second of each other.
765 if time.time() - self._time > 1.0:
766 self._runtask.start(qtutils.SimpleTask(self.gather_paths))
767 self._time = time.time()
769 def gather_paths(self):
770 """Gather paths and store them in the model"""
771 self._time = time.time()
772 if self._model.cfg.get(prefs.AUTOCOMPLETE_PATHS, True):
773 self._paths = gitcmds.tracked_files(self.context)
774 else:
775 self._paths = []
776 self._time = time.time()
778 def gather_matches(self, case_sensitive):
779 """Filter paths and refs to find matching entries"""
780 if not self._paths:
781 self.gather_paths()
782 refs = filter_matches(
783 self.match_text, self.matches(), case_sensitive, sort_key=ref_sort_key
785 paths, dirs = filter_path_matches(self.match_text, self._paths, case_sensitive)
786 has_doubledash = (
787 self.match_text == '--'
788 or self.full_text.startswith('-- ')
789 or ' -- ' in self.full_text
791 if has_doubledash:
792 refs = []
793 elif refs and paths:
794 paths.insert(0, '--')
796 return (refs, paths, dirs)
799 def bind_lineedit(model, hint='', show_all_completions=False):
800 """Create a line edit bound against a specific model"""
802 class BoundLineEdit(CompletionLineEdit):
803 def __init__(self, context, hint=hint, parent=None):
804 CompletionLineEdit.__init__(
805 self,
806 context,
807 model,
808 hint=hint,
809 show_all_completions=show_all_completions,
810 parent=parent,
812 self.context = context
814 return BoundLineEdit
817 # Concrete classes
818 GitLogLineEdit = bind_lineedit(GitLogCompletionModel, hint='<ref>')
819 GitRefLineEdit = bind_lineedit(GitRefCompletionModel, hint='<ref>')
820 GitCheckoutBranchLineEdit = bind_lineedit(
821 GitCheckoutBranchCompletionModel,
822 hint='<branch>',
823 show_all_completions=True,
825 GitCreateBranchLineEdit = bind_lineedit(GitCreateBranchCompletionModel, hint='<branch>')
826 GitBranchLineEdit = bind_lineedit(GitBranchCompletionModel, hint='<branch>')
827 GitRemoteBranchLineEdit = bind_lineedit(
828 GitRemoteBranchCompletionModel, hint='<remote-branch>'
830 GitStatusFilterLineEdit = bind_lineedit(GitStatusFilterCompletionModel, hint='<path>')
831 GitTrackedLineEdit = bind_lineedit(GitTrackedCompletionModel, hint='<path>')
834 class GitDialog(QtWidgets.QDialog):
835 # The "lineedit" argument is provided by the derived class constructor.
836 def __init__(self, lineedit, context, title, text, parent, icon=None):
837 QtWidgets.QDialog.__init__(self, parent)
838 self.context = context
839 self.setWindowTitle(title)
840 self.setWindowModality(Qt.WindowModal)
841 self.setMinimumWidth(333)
843 self.label = QtWidgets.QLabel()
844 self.label.setText(title)
845 self.lineedit = lineedit(context)
846 self.ok_button = qtutils.ok_button(text, icon=icon, enabled=False)
847 self.close_button = qtutils.close_button()
849 self.button_layout = qtutils.hbox(
850 defs.no_margin,
851 defs.button_spacing,
852 qtutils.STRETCH,
853 self.close_button,
854 self.ok_button,
857 self.main_layout = qtutils.vbox(
858 defs.margin, defs.spacing, self.label, self.lineedit, self.button_layout
860 self.setLayout(self.main_layout)
862 self.lineedit.textChanged.connect(self.text_changed)
863 self.lineedit.enter.connect(self.accept)
864 qtutils.connect_button(self.ok_button, self.accept)
865 qtutils.connect_button(self.close_button, self.reject)
867 self.setFocusProxy(self.lineedit)
868 self.lineedit.setFocus()
870 def text(self):
871 return self.lineedit.text()
873 def text_changed(self, _txt):
874 self.ok_button.setEnabled(bool(self.text()))
876 def set_text(self, ref):
877 self.lineedit.setText(ref)
879 @classmethod
880 def get(cls, context, title, text, parent, default=None, icon=None):
881 dlg = cls(context, title, text, parent, icon=icon)
882 if default:
883 dlg.set_text(default)
885 dlg.show()
886 QtCore.QTimer().singleShot(250, dlg.lineedit.show_popup)
888 if dlg.exec_() == cls.Accepted:
889 return dlg.text()
890 return None
893 class GitRefDialog(GitDialog):
894 def __init__(self, context, title, text, parent, icon=None):
895 GitDialog.__init__(
896 self, GitRefLineEdit, context, title, text, parent, icon=icon
900 class GitCheckoutBranchDialog(GitDialog):
901 def __init__(self, context, title, text, parent, icon=None):
902 GitDialog.__init__(
903 self, GitCheckoutBranchLineEdit, context, title, text, parent, icon=icon
907 class GitBranchDialog(GitDialog):
908 def __init__(self, context, title, text, parent, icon=None):
909 GitDialog.__init__(
910 self, GitBranchLineEdit, context, title, text, parent, icon=icon
914 class GitRemoteBranchDialog(GitDialog):
915 def __init__(self, context, title, text, parent, icon=None):
916 GitDialog.__init__(
917 self, GitRemoteBranchLineEdit, context, title, text, parent, icon=icon