text: defer calls to setStyleSheet()
[git-cola.git] / cola / widgets / completion.py
blobe443935172e7d8e0ae08d7d8eae5261cdcb1c831
1 from __future__ import division, absolute_import, unicode_literals
2 import re
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 .. import core
11 from .. import gitcmds
12 from .. import icons
13 from .. import qtutils
14 from .. import utils
15 from . import defs
16 from .text import HintedLineEdit
19 class ValidateRegex(object):
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(RemoteValidator, self).__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(BranchValidator, self).__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__(self, context, model_factory, hint='', parent=None):
94 HintedLineEdit.__init__(self, context, hint, parent=parent)
95 # Tracks when the completion popup was active during key events
97 self.context = context
98 # The most recently selected completion item
99 self._selection = None
101 # Create a completion model
102 completion_model = model_factory(context, self)
103 completer = Completer(completion_model, self)
104 completer.setWidget(self)
105 self._completer = completer
106 self._completion_model = completion_model
108 # The delegate highlights matching completion text in the popup widget
109 self._delegate = HighlightDelegate(self)
110 completer.popup().setItemDelegate(self._delegate)
112 self.textChanged.connect(self._text_changed)
113 self._completer.activated.connect(self.choose_completion)
114 self._completion_model.updated.connect(self._completions_updated,
115 type=Qt.QueuedConnection)
116 self.destroyed.connect(self.dispose)
118 def __del__(self):
119 self.dispose()
121 # pylint: disable=unused-argument
122 def dispose(self, *args):
123 self._completer.dispose()
125 def completion_selection(self):
126 """Return the last completion's selection"""
127 return self._selection
129 def complete(self):
130 """Trigger the completion popup to appear and offer completions"""
131 self._completer.complete()
133 def refresh(self):
134 """Refresh the completion model"""
135 return self._completer.model().update()
137 def popup(self):
138 """Return the completer's popup"""
139 return self._completer.popup()
141 def _text_changed(self, full_text):
142 match_text = self._last_word()
143 self._do_text_changed(full_text, match_text)
144 self.complete_last_word()
146 def _do_text_changed(self, full_text, match_text):
147 case_sensitive = _is_case_sensitive(match_text)
148 if case_sensitive:
149 self._completer.setCaseSensitivity(Qt.CaseSensitive)
150 else:
151 self._completer.setCaseSensitivity(Qt.CaseInsensitive)
152 self._delegate.set_highlight_text(match_text, case_sensitive)
153 self._completer.set_match_text(full_text, match_text, case_sensitive)
155 def update_matches(self):
156 text = self._last_word()
157 case_sensitive = _is_case_sensitive(text)
158 self._completer.setCompletionPrefix(text)
159 self._completer.model().update_matches(case_sensitive)
161 def choose_completion(self, completion):
163 This is the event handler for the QCompleter.activated(QString) signal,
164 it is called when the user selects an item in the completer popup.
166 if not completion:
167 self._do_text_changed('', '')
168 return
169 words = self._words()
170 if words and not self._ends_with_whitespace():
171 words.pop()
173 words.append(completion)
174 text = core.list2cmdline(words)
175 self.setText(text)
176 self.changed.emit()
177 self._do_text_changed(text, '')
178 self.popup().hide()
180 def _words(self):
181 return utils.shell_split(self.value())
183 def _ends_with_whitespace(self):
184 value = self.value()
185 return value != value.rstrip()
187 def _last_word(self):
188 if self._ends_with_whitespace():
189 return ''
190 words = self._words()
191 if not words:
192 return self.value()
193 if not words[-1]:
194 return ''
195 return words[-1]
197 def complete_last_word(self):
198 self.update_matches()
199 self.complete()
201 def close_popup(self):
202 if self.popup().isVisible():
203 self.popup().close()
205 def _completions_updated(self):
206 popup = self.popup()
207 if not popup.isVisible():
208 return
209 # Select the first item
210 idx = self._completion_model.index(0, 0)
211 selection = QtCore.QItemSelection(idx, idx)
212 mode = QtCore.QItemSelectionModel.Select
213 popup.selectionModel().select(selection, mode)
215 def selected_completion(self):
216 """Return the selected completion item"""
217 popup = self.popup()
218 if not popup.isVisible():
219 return None
220 model = popup.selectionModel()
221 indexes = model.selectedIndexes()
222 if not indexes:
223 return None
224 idx = indexes[0]
225 item = self._completion_model.itemFromIndex(idx)
226 if not item:
227 return None
228 return item.text()
230 def select_completion(self):
231 """Choose the selected completion option from the completion popup"""
232 result = False
233 visible = self.popup().isVisible()
234 if visible:
235 selection = self.selected_completion()
236 if selection:
237 self.choose_completion(selection)
238 result = True
239 return result
241 # Qt overrides
242 def event(self, event):
243 """Override QWidget::event() for tab completion"""
244 event_type = event.type()
246 if (event_type == QtCore.QEvent.KeyPress
247 and event.key() == Qt.Key_Tab
248 and self.select_completion()):
249 return True
251 # Make sure the popup goes away during teardown
252 if event_type == QtCore.QEvent.Hide:
253 self.close_popup()
255 return super(CompletionLineEdit, self).event(event)
257 def keyPressEvent(self, event):
258 """Process completion and navigation events"""
259 super(CompletionLineEdit, self).keyPressEvent(event)
260 visible = self.popup().isVisible()
262 # Hide the popup when the field is empty
263 is_empty = not self.value()
264 if is_empty:
265 self.cleared.emit()
266 if visible:
267 self.popup().hide()
269 # Activation keys select the completion when pressed and emit the
270 # activated signal. Navigation keys have lower priority, and only
271 # emit when it wasn't already handled as an activation event.
272 key = event.key()
273 if key in self.ACTIVATION_KEYS and visible:
274 if self.select_completion():
275 self.activated.emit()
276 return
278 navigation = self.NAVIGATION_KEYS.get(key, None)
279 if navigation:
280 signal = getattr(self, navigation)
281 signal.emit()
284 class GatherCompletionsThread(QtCore.QThread):
286 items_gathered = Signal(object)
288 def __init__(self, model):
289 QtCore.QThread.__init__(self)
290 self.model = model
291 self.case_sensitive = False
292 self.running = False
294 def dispose(self):
295 self.running = False
296 self.wait()
298 def run(self):
299 text = None
300 self.running = True
301 # Loop when the matched text changes between the start and end time.
302 # This happens when gather_matches() takes too long and the
303 # model's match_text changes in-between.
304 while self.running and text != self.model.match_text:
305 text = self.model.match_text
306 items = self.model.gather_matches(self.case_sensitive)
308 if self.running and text is not None:
309 self.items_gathered.emit(items)
312 class HighlightDelegate(QtWidgets.QStyledItemDelegate):
313 """A delegate used for auto-completion to give formatted completion"""
315 def __init__(self, parent):
316 QtWidgets.QStyledItemDelegate.__init__(self, parent)
317 self.widget = parent
318 self.highlight_text = ''
319 self.case_sensitive = False
321 self.doc = QtGui.QTextDocument()
322 # older PyQt4 does not have setDocumentMargin
323 if hasattr(self.doc, 'setDocumentMargin'):
324 self.doc.setDocumentMargin(0)
326 def set_highlight_text(self, text, case_sensitive):
327 """Sets the text that will be made bold when displayed"""
328 self.highlight_text = text
329 self.case_sensitive = case_sensitive
331 def paint(self, painter, option, index):
332 """Overloaded Qt method for custom painting of a model index"""
333 if not self.highlight_text:
334 QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
335 return
336 text = index.data()
337 if self.case_sensitive:
338 html = text.replace(self.highlight_text,
339 '<strong>%s</strong>' % self.highlight_text)
340 else:
341 match = re.match(r'(.*)(%s)(.*)' % re.escape(self.highlight_text),
342 text, re.IGNORECASE)
343 if match:
344 start = match.group(1) or ''
345 middle = match.group(2) or ''
346 end = match.group(3) or ''
347 html = (start + ('<strong>%s</strong>' % middle) + end)
348 else:
349 html = text
350 self.doc.setHtml(html)
352 # Painting item without text, Text Document will paint the text
353 params = QtWidgets.QStyleOptionViewItem(option)
354 self.initStyleOption(params, index)
355 params.text = ''
357 style = QtWidgets.QApplication.style()
358 style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, params, painter)
359 ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
361 # Highlighting text if item is selected
362 if params.state & QtWidgets.QStyle.State_Selected:
363 color = params.palette.color(QtGui.QPalette.Active,
364 QtGui.QPalette.HighlightedText)
365 ctx.palette.setColor(QtGui.QPalette.Text, color)
367 # translate the painter to where the text is drawn
368 item_text = QtWidgets.QStyle.SE_ItemViewItemText
369 rect = style.subElementRect(item_text, params, self.widget)
370 painter.save()
372 start = rect.topLeft() + QtCore.QPoint(defs.margin, 0)
373 painter.translate(start)
375 # tell the text document to draw the html for us
376 self.doc.documentLayout().draw(painter, ctx)
377 painter.restore()
380 def ref_sort_key(ref):
381 """Sort key function that causes shorter refs to sort first, but
382 alphabetizes refs of equal length (in order to make local branches sort
383 before remote ones)."""
384 return len(ref), ref
387 class CompletionModel(QtGui.QStandardItemModel):
389 updated = Signal()
390 items_gathered = Signal(object)
391 model_updated = Signal()
393 def __init__(self, context, parent):
394 QtGui.QStandardItemModel.__init__(self, parent)
395 self.context = context
396 self.match_text = ''
397 self.full_text = ''
398 self.case_sensitive = False
400 self.update_thread = GatherCompletionsThread(self)
401 self.update_thread.items_gathered.connect(self.apply_matches,
402 type=Qt.QueuedConnection)
404 def update(self):
405 case_sensitive = self.update_thread.case_sensitive
406 self.update_matches(case_sensitive)
408 def set_match_text(self, full_text, match_text, case_sensitive):
409 self.full_text = full_text
410 self.match_text = match_text
411 self.update_matches(case_sensitive)
413 def update_matches(self, case_sensitive):
414 self.case_sensitive = case_sensitive
415 self.update_thread.case_sensitive = case_sensitive
416 if not self.update_thread.isRunning():
417 self.update_thread.start()
419 # pylint: disable=unused-argument,no-self-use
420 def gather_matches(self, case_sensitive):
421 return ((), (), set())
423 def apply_matches(self, match_tuple):
424 matched_refs, matched_paths, dirs = match_tuple
425 QStandardItem = QtGui.QStandardItem
427 dir_icon = icons.directory()
428 git_icon = icons.cola()
430 items = []
431 for ref in matched_refs:
432 item = QStandardItem()
433 item.setText(ref)
434 item.setIcon(git_icon)
435 items.append(item)
437 from_filename = icons.from_filename
438 for match in matched_paths:
439 item = QStandardItem()
440 item.setText(match)
441 if match in dirs:
442 item.setIcon(dir_icon)
443 else:
444 item.setIcon(from_filename(match))
445 items.append(item)
447 try:
448 self.clear()
449 self.invisibleRootItem().appendRows(items)
450 self.updated.emit()
451 except RuntimeError: # C++ object has been deleted
452 pass
454 def dispose(self):
455 self.update_thread.dispose()
458 def _identity(x):
459 return x
462 def _lower(x):
463 return x.lower()
466 def filter_matches(match_text, candidates, case_sensitive,
467 sort_key=lambda x: x):
468 """Filter candidates and return the matches"""
470 if case_sensitive:
471 case_transform = _identity
472 else:
473 case_transform = _lower
475 if match_text:
476 match_text = case_transform(match_text)
477 matches = [r for r in candidates if match_text in case_transform(r)]
478 else:
479 matches = list(candidates)
481 matches.sort(key=lambda x: sort_key(case_transform(x)))
482 return matches
485 def filter_path_matches(match_text, file_list, case_sensitive):
486 """Return matching completions from a list of candidate files"""
488 files = set(file_list)
489 files_and_dirs = utils.add_parents(files)
490 dirs = files_and_dirs.difference(files)
492 paths = filter_matches(match_text, files_and_dirs, case_sensitive)
493 return (paths, dirs)
496 class Completer(QtWidgets.QCompleter):
498 def __init__(self, model, parent):
499 QtWidgets.QCompleter.__init__(self, parent)
500 self._model = model
501 self.setCompletionMode(QtWidgets.QCompleter.UnfilteredPopupCompletion)
502 self.setCaseSensitivity(Qt.CaseInsensitive)
504 model.model_updated.connect(self.update, type=Qt.QueuedConnection)
505 self.setModel(model)
507 def update(self):
508 self._model.update()
510 def dispose(self):
511 self._model.dispose()
513 def set_match_text(self, full_text, match_text, case_sensitive):
514 self._model.set_match_text(full_text, match_text, case_sensitive)
517 class GitCompletionModel(CompletionModel):
519 def __init__(self, context, parent):
520 CompletionModel.__init__(self, context, parent)
521 self.main_model = model = context.model
522 msg = model.message_updated
523 model.add_observer(msg, self.emit_model_updated)
525 def gather_matches(self, case_sensitive):
526 refs = filter_matches(self.match_text, self.matches(), case_sensitive,
527 sort_key=ref_sort_key)
528 return (refs, (), set())
530 def emit_model_updated(self):
531 try:
532 self.model_updated.emit()
533 except RuntimeError: # C++ object has been deleted
534 self.dispose()
536 # pylint: disable=no-self-use
537 def matches(self):
538 return []
540 def dispose(self):
541 super(GitCompletionModel, self).dispose()
542 self.main_model.remove_observer(self.emit_model_updated)
545 class GitRefCompletionModel(GitCompletionModel):
546 """Completer for branches and tags"""
548 def __init__(self, context, parent):
549 GitCompletionModel.__init__(self, context, parent)
551 def matches(self):
552 model = self.main_model
553 return model.local_branches + model.remote_branches + model.tags
556 def find_potential_branches(model):
557 remotes = model.remotes
558 remote_branches = model.remote_branches
560 ambiguous = set()
561 allnames = set(model.local_branches)
562 potential = []
564 for remote_branch in remote_branches:
565 branch = gitcmds.strip_remote(remotes, remote_branch)
566 if branch in allnames or branch == remote_branch:
567 ambiguous.add(branch)
568 continue
569 potential.append(branch)
570 allnames.add(branch)
572 potential_branches = [p for p in potential if p not in ambiguous]
573 return potential_branches
576 class GitCreateBranchCompletionModel(GitCompletionModel):
577 """Completer for naming new branches"""
579 def matches(self):
580 model = self.main_model
581 potential_branches = find_potential_branches(model)
582 return (model.local_branches +
583 potential_branches +
584 model.tags)
587 class GitCheckoutBranchCompletionModel(GitCompletionModel):
588 """Completer for git checkout <branch>"""
590 def matches(self):
591 model = self.main_model
592 potential_branches = find_potential_branches(model)
593 return (model.local_branches +
594 potential_branches +
595 model.remote_branches +
596 model.tags)
599 class GitBranchCompletionModel(GitCompletionModel):
600 """Completer for local branches"""
602 def __init__(self, context, parent):
603 GitCompletionModel.__init__(self, context, parent)
605 def matches(self):
606 model = self.main_model
607 return model.local_branches
610 class GitRemoteBranchCompletionModel(GitCompletionModel):
611 """Completer for remote branches"""
613 def __init__(self, context, parent):
614 GitCompletionModel.__init__(self, context, parent)
616 def matches(self):
617 model = self.main_model
618 return model.remote_branches
621 class GitPathCompletionModel(GitCompletionModel):
622 """Base class for path completion"""
624 def __init__(self, context, parent):
625 GitCompletionModel.__init__(self, context, parent)
627 # pylint: disable=no-self-use
628 def candidate_paths(self):
629 return []
631 def gather_matches(self, case_sensitive):
632 paths, dirs = filter_path_matches(self.match_text,
633 self.candidate_paths(),
634 case_sensitive)
635 return ((), paths, dirs)
638 class GitStatusFilterCompletionModel(GitPathCompletionModel):
639 """Completer for modified files and folders for status filtering"""
641 def __init__(self, context, parent):
642 GitPathCompletionModel.__init__(self, context, parent)
644 def candidate_paths(self):
645 model = self.main_model
646 return (model.staged + model.unmerged +
647 model.modified + model.untracked)
650 class GitTrackedCompletionModel(GitPathCompletionModel):
651 """Completer for tracked files and folders"""
653 def __init__(self, context, parent):
654 GitPathCompletionModel.__init__(self, context, parent)
655 self.model_updated.connect(self.gather_paths, type=Qt.QueuedConnection)
656 self._paths = []
658 def gather_paths(self):
659 context = self.context
660 self._paths = gitcmds.tracked_files(context)
662 def gather_matches(self, case_sensitive):
663 if not self._paths:
664 self.gather_paths()
666 refs = []
667 paths, dirs = filter_path_matches(self.match_text, self._paths,
668 case_sensitive)
669 return (refs, paths, dirs)
672 class GitLogCompletionModel(GitRefCompletionModel):
673 """Completer for arguments suitable for git-log like commands"""
675 def __init__(self, context, parent):
676 GitRefCompletionModel.__init__(self, context, parent)
677 self.model_updated.connect(self.gather_paths, type=Qt.QueuedConnection)
678 self._paths = []
680 def gather_paths(self):
681 context = self.context
682 self._paths = gitcmds.tracked_files(context)
684 def gather_matches(self, case_sensitive):
685 if not self._paths:
686 self.gather_paths()
687 refs = filter_matches(self.match_text, self.matches(), case_sensitive,
688 sort_key=ref_sort_key)
689 paths, dirs = filter_path_matches(self.match_text, self._paths,
690 case_sensitive)
691 has_doubledash = (self.match_text == '--' or
692 self.full_text.startswith('-- ') or
693 ' -- ' in self.full_text)
694 if has_doubledash:
695 refs = []
696 elif refs and paths:
697 paths.insert(0, '--')
699 return (refs, paths, dirs)
702 def bind_lineedit(model, hint=''):
703 """Create a line edit bound against a specific model"""
705 class BoundLineEdit(CompletionLineEdit):
707 def __init__(self, context, hint=hint, parent=None):
708 CompletionLineEdit.__init__(self, context, model,
709 hint=hint, parent=parent)
710 self.context = context
712 return BoundLineEdit
715 # Concrete classes
716 GitLogLineEdit = bind_lineedit(GitLogCompletionModel, hint='<ref>')
717 GitRefLineEdit = bind_lineedit(GitRefCompletionModel, hint='<ref>')
718 GitCheckoutBranchLineEdit = bind_lineedit(GitCheckoutBranchCompletionModel,
719 hint='<branch>')
720 GitCreateBranchLineEdit = bind_lineedit(GitCreateBranchCompletionModel,
721 hint='<branch>')
722 GitBranchLineEdit = bind_lineedit(GitBranchCompletionModel, hint='<branch>')
723 GitRemoteBranchLineEdit = bind_lineedit(GitRemoteBranchCompletionModel,
724 hint='<remote-branch>')
725 GitStatusFilterLineEdit = bind_lineedit(GitStatusFilterCompletionModel,
726 hint='<path>')
727 GitTrackedLineEdit = bind_lineedit(GitTrackedCompletionModel, hint='<path>')
730 class GitDialog(QtWidgets.QDialog):
732 def __init__(self, lineedit, context, title, text, parent, icon=None):
733 QtWidgets.QDialog.__init__(self, parent)
734 self.context = context
735 self.setWindowTitle(title)
736 self.setWindowModality(Qt.WindowModal)
737 self.setMinimumWidth(333)
739 self.label = QtWidgets.QLabel()
740 self.label.setText(title)
741 self.lineedit = lineedit(context)
742 self.ok_button = qtutils.ok_button(text, icon=icon, enabled=False)
743 self.close_button = qtutils.close_button()
745 self.button_layout = qtutils.hbox(defs.no_margin, defs.button_spacing,
746 qtutils.STRETCH,
747 self.ok_button, self.close_button)
749 self.main_layout = qtutils.vbox(defs.margin, defs.spacing,
750 self.label, self.lineedit,
751 self.button_layout)
752 self.setLayout(self.main_layout)
754 self.lineedit.textChanged.connect(self.text_changed)
755 self.lineedit.enter.connect(self.accept)
756 qtutils.connect_button(self.ok_button, self.accept)
757 qtutils.connect_button(self.close_button, self.reject)
759 self.setFocusProxy(self.lineedit)
760 self.lineedit.setFocus(True)
762 def text(self):
763 return self.lineedit.text()
765 def text_changed(self, _txt):
766 self.ok_button.setEnabled(bool(self.text()))
768 def set_text(self, ref):
769 self.lineedit.setText(ref)
771 @classmethod
772 def get(cls, context, title, text, parent, default=None, icon=None):
773 dlg = cls(context, title, text, parent, icon=icon)
774 if default:
775 dlg.set_text(default)
777 dlg.show()
779 def show_popup():
780 x = dlg.lineedit.x()
781 y = dlg.lineedit.y() + dlg.lineedit.height()
782 point = QtCore.QPoint(x, y)
783 mapped = dlg.mapToGlobal(point)
784 dlg.lineedit.popup().move(mapped.x(), mapped.y())
785 dlg.lineedit.popup().show()
786 dlg.lineedit.refresh()
787 dlg.lineedit.setFocus(True)
789 QtCore.QTimer().singleShot(100, show_popup)
791 if dlg.exec_() == cls.Accepted:
792 return dlg.text()
793 return None
796 class GitRefDialog(GitDialog):
798 def __init__(self, context, title, text, parent, icon=None):
799 GitDialog.__init__(
800 self, GitRefLineEdit, context, title, text, parent, icon=icon)
803 class GitCheckoutBranchDialog(GitDialog):
805 def __init__(self, context, title, text, parent, icon=None):
806 GitDialog.__init__(self, GitCheckoutBranchLineEdit,
807 context, title, text, parent, icon=icon)
810 class GitBranchDialog(GitDialog):
812 def __init__(self, context, title, text, parent, icon=None):
813 GitDialog.__init__(
814 self, GitBranchLineEdit, context, title, text, parent, icon=icon)
817 class GitRemoteBranchDialog(GitDialog):
819 def __init__(self, context, title, text, parent, icon=None):
820 GitDialog.__init__(self, GitRemoteBranchLineEdit,
821 context, title, text, parent, icon=icon)