widgets: flake8 and pylint fixes
[git-cola.git] / cola / widgets / completion.py
blob9444fbaeb05c502b1b90eb3319c7901a6a65eed2
1 from __future__ import absolute_import, division, print_function, 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 ..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(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 # pylint: disable=no-member
113 self.textChanged.connect(self._text_changed)
114 self._completer.activated.connect(self.choose_completion)
115 self._completion_model.updated.connect(
116 self._completions_updated, type=Qt.QueuedConnection
118 self.destroyed.connect(self.dispose)
120 def __del__(self):
121 self.dispose()
123 # pylint: disable=unused-argument
124 def dispose(self, *args):
125 self._completer.dispose()
127 def completion_selection(self):
128 """Return the last completion's selection"""
129 return self._selection
131 def complete(self):
132 """Trigger the completion popup to appear and offer completions"""
133 self._completer.complete()
135 def refresh(self):
136 """Refresh the completion model"""
137 return self._completer.model().update()
139 def popup(self):
140 """Return the completer's popup"""
141 return self._completer.popup()
143 def _text_changed(self, full_text):
144 match_text = self._last_word()
145 self._do_text_changed(full_text, match_text)
146 self.complete_last_word()
148 def _do_text_changed(self, full_text, match_text):
149 case_sensitive = _is_case_sensitive(match_text)
150 if case_sensitive:
151 self._completer.setCaseSensitivity(Qt.CaseSensitive)
152 else:
153 self._completer.setCaseSensitivity(Qt.CaseInsensitive)
154 self._delegate.set_highlight_text(match_text, case_sensitive)
155 self._completer.set_match_text(full_text, match_text, case_sensitive)
157 def update_matches(self):
158 text = self._last_word()
159 case_sensitive = _is_case_sensitive(text)
160 self._completer.setCompletionPrefix(text)
161 self._completer.model().update_matches(case_sensitive)
163 def choose_completion(self, completion):
165 This is the event handler for the QCompleter.activated(QString) signal,
166 it is called when the user selects an item in the completer popup.
168 if not completion:
169 self._do_text_changed('', '')
170 return
171 words = self._words()
172 if words and not self._ends_with_whitespace():
173 words.pop()
175 words.append(completion)
176 text = core.list2cmdline(words)
177 self.setText(text)
178 self.changed.emit()
179 self._do_text_changed(text, '')
180 self.popup().hide()
182 def _words(self):
183 return utils.shell_split(self.value())
185 def _ends_with_whitespace(self):
186 value = self.value()
187 return value != value.rstrip()
189 def _last_word(self):
190 if self._ends_with_whitespace():
191 return ''
192 words = self._words()
193 if not words:
194 return self.value()
195 if not words[-1]:
196 return ''
197 return words[-1]
199 def complete_last_word(self):
200 self.update_matches()
201 self.complete()
203 def close_popup(self):
204 if self.popup().isVisible():
205 self.popup().close()
207 def _completions_updated(self):
208 popup = self.popup()
209 if not popup.isVisible():
210 return
211 # Select the first item
212 idx = self._completion_model.index(0, 0)
213 selection = QtCore.QItemSelection(idx, idx)
214 mode = QtCore.QItemSelectionModel.Select
215 popup.selectionModel().select(selection, mode)
217 def selected_completion(self):
218 """Return the selected completion item"""
219 popup = self.popup()
220 if not popup.isVisible():
221 return None
222 model = popup.selectionModel()
223 indexes = model.selectedIndexes()
224 if not indexes:
225 return None
226 idx = indexes[0]
227 item = self._completion_model.itemFromIndex(idx)
228 if not item:
229 return None
230 return item.text()
232 def select_completion(self):
233 """Choose the selected completion option from the completion popup"""
234 result = False
235 visible = self.popup().isVisible()
236 if visible:
237 selection = self.selected_completion()
238 if selection:
239 self.choose_completion(selection)
240 result = True
241 return result
243 # Qt overrides
244 def event(self, event):
245 """Override QWidget::event() for tab completion"""
246 event_type = event.type()
248 if (
249 event_type == QtCore.QEvent.KeyPress
250 and event.key() == Qt.Key_Tab
251 and self.select_completion()
253 return True
255 # Make sure the popup goes away during teardown
256 if event_type == QtCore.QEvent.Hide:
257 self.close_popup()
259 return super(CompletionLineEdit, self).event(event)
261 def keyPressEvent(self, event):
262 """Process completion and navigation events"""
263 super(CompletionLineEdit, self).keyPressEvent(event)
264 visible = self.popup().isVisible()
266 # Hide the popup when the field is empty
267 is_empty = not self.value()
268 if is_empty:
269 self.cleared.emit()
270 if visible:
271 self.popup().hide()
273 # Activation keys select the completion when pressed and emit the
274 # activated signal. Navigation keys have lower priority, and only
275 # emit when it wasn't already handled as an activation event.
276 key = event.key()
277 if key in self.ACTIVATION_KEYS and visible:
278 if self.select_completion():
279 self.activated.emit()
280 return
282 navigation = self.NAVIGATION_KEYS.get(key, None)
283 if navigation:
284 signal = getattr(self, navigation)
285 signal.emit()
288 class GatherCompletionsThread(QtCore.QThread):
290 items_gathered = Signal(object)
292 def __init__(self, model):
293 QtCore.QThread.__init__(self)
294 self.model = model
295 self.case_sensitive = False
296 self.running = False
298 def dispose(self):
299 self.running = False
300 try:
301 self.wait()
302 except RuntimeError:
303 # The C++ object may have already been deleted by python while
304 # the application is tearing down. This is fine.
305 pass
307 def run(self):
308 text = None
309 self.running = True
310 # Loop when the matched text changes between the start and end time.
311 # This happens when gather_matches() takes too long and the
312 # model's match_text changes in-between.
313 while self.running and text != self.model.match_text:
314 text = self.model.match_text
315 items = self.model.gather_matches(self.case_sensitive)
317 if self.running and text is not None:
318 self.items_gathered.emit(items)
321 class HighlightDelegate(QtWidgets.QStyledItemDelegate):
322 """A delegate used for auto-completion to give formatted completion"""
324 def __init__(self, parent):
325 QtWidgets.QStyledItemDelegate.__init__(self, parent)
326 self.widget = parent
327 self.highlight_text = ''
328 self.case_sensitive = False
330 self.doc = QtGui.QTextDocument()
331 # older PyQt4 does not have setDocumentMargin
332 if hasattr(self.doc, 'setDocumentMargin'):
333 self.doc.setDocumentMargin(0)
335 def set_highlight_text(self, text, case_sensitive):
336 """Sets the text that will be made bold when displayed"""
337 self.highlight_text = text
338 self.case_sensitive = case_sensitive
340 def paint(self, painter, option, index):
341 """Overloaded Qt method for custom painting of a model index"""
342 if not self.highlight_text:
343 QtWidgets.QStyledItemDelegate.paint(self, painter, option, index)
344 return
345 text = index.data()
346 if self.case_sensitive:
347 html = text.replace(
348 self.highlight_text, '<strong>%s</strong>' % self.highlight_text
350 else:
351 match = re.match(
352 r'(.*)(%s)(.*)' % re.escape(self.highlight_text), text, re.IGNORECASE
354 if match:
355 start = match.group(1) or ''
356 middle = match.group(2) or ''
357 end = match.group(3) or ''
358 html = start + ('<strong>%s</strong>' % middle) + end
359 else:
360 html = text
361 self.doc.setHtml(html)
363 # Painting item without text, Text Document will paint the text
364 params = QtWidgets.QStyleOptionViewItem(option)
365 self.initStyleOption(params, index)
366 params.text = ''
368 style = QtWidgets.QApplication.style()
369 style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, params, painter)
370 ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
372 # Highlighting text if item is selected
373 if params.state & QtWidgets.QStyle.State_Selected:
374 color = params.palette.color(
375 QtGui.QPalette.Active, QtGui.QPalette.HighlightedText
377 ctx.palette.setColor(QtGui.QPalette.Text, color)
379 # translate the painter to where the text is drawn
380 item_text = QtWidgets.QStyle.SE_ItemViewItemText
381 rect = style.subElementRect(item_text, params, self.widget)
382 painter.save()
384 start = rect.topLeft() + QtCore.QPoint(defs.margin, 0)
385 painter.translate(start)
387 # tell the text document to draw the html for us
388 self.doc.documentLayout().draw(painter, ctx)
389 painter.restore()
392 def ref_sort_key(ref):
393 """Sort key function that causes shorter refs to sort first, but
394 alphabetizes refs of equal length (in order to make local branches sort
395 before remote ones)."""
396 return len(ref), ref
399 class CompletionModel(QtGui.QStandardItemModel):
401 updated = Signal()
402 items_gathered = Signal(object)
403 model_updated = Signal()
405 def __init__(self, context, parent):
406 QtGui.QStandardItemModel.__init__(self, parent)
407 self.context = context
408 self.match_text = ''
409 self.full_text = ''
410 self.case_sensitive = False
412 self.update_thread = GatherCompletionsThread(self)
413 self.update_thread.items_gathered.connect(
414 self.apply_matches, type=Qt.QueuedConnection
417 def update(self):
418 case_sensitive = self.update_thread.case_sensitive
419 self.update_matches(case_sensitive)
421 def set_match_text(self, full_text, match_text, case_sensitive):
422 self.full_text = full_text
423 self.match_text = match_text
424 self.update_matches(case_sensitive)
426 def update_matches(self, case_sensitive):
427 self.case_sensitive = case_sensitive
428 self.update_thread.case_sensitive = case_sensitive
429 if not self.update_thread.isRunning():
430 self.update_thread.start()
432 # pylint: disable=unused-argument,no-self-use
433 def gather_matches(self, case_sensitive):
434 return ((), (), set())
436 def apply_matches(self, match_tuple):
437 matched_refs, matched_paths, dirs = match_tuple
438 QStandardItem = QtGui.QStandardItem
440 dir_icon = icons.directory()
441 git_icon = icons.cola()
443 items = []
444 for ref in matched_refs:
445 item = QStandardItem()
446 item.setText(ref)
447 item.setIcon(git_icon)
448 items.append(item)
450 from_filename = icons.from_filename
451 for match in matched_paths:
452 item = QStandardItem()
453 item.setText(match)
454 if match in dirs:
455 item.setIcon(dir_icon)
456 else:
457 item.setIcon(from_filename(match))
458 items.append(item)
460 try:
461 self.clear()
462 self.invisibleRootItem().appendRows(items)
463 self.updated.emit()
464 except RuntimeError: # C++ object has been deleted
465 pass
467 def dispose(self):
468 self.update_thread.dispose()
471 def _identity(x):
472 return x
475 def _lower(x):
476 return x.lower()
479 def filter_matches(match_text, candidates, case_sensitive, sort_key=lambda x: x):
480 """Filter candidates and return the matches"""
482 if case_sensitive:
483 case_transform = _identity
484 else:
485 case_transform = _lower
487 if match_text:
488 match_text = case_transform(match_text)
489 matches = [r for r in candidates if match_text in case_transform(r)]
490 else:
491 matches = list(candidates)
493 matches.sort(key=lambda x: sort_key(case_transform(x)))
494 return matches
497 def filter_path_matches(match_text, file_list, case_sensitive):
498 """Return matching completions from a list of candidate files"""
500 files = set(file_list)
501 files_and_dirs = utils.add_parents(files)
502 dirs = files_and_dirs.difference(files)
504 paths = filter_matches(match_text, files_and_dirs, case_sensitive)
505 return (paths, dirs)
508 class Completer(QtWidgets.QCompleter):
509 def __init__(self, model, parent):
510 QtWidgets.QCompleter.__init__(self, parent)
511 self._model = model
512 self.setCompletionMode(QtWidgets.QCompleter.UnfilteredPopupCompletion)
513 self.setCaseSensitivity(Qt.CaseInsensitive)
515 model.model_updated.connect(self.update, type=Qt.QueuedConnection)
516 self.setModel(model)
518 def update(self):
519 self._model.update()
521 def dispose(self):
522 self._model.dispose()
524 def set_match_text(self, full_text, match_text, case_sensitive):
525 self._model.set_match_text(full_text, match_text, case_sensitive)
528 class GitCompletionModel(CompletionModel):
529 def __init__(self, context, parent):
530 CompletionModel.__init__(self, context, parent)
531 self.context = context
532 context.model.updated.connect(self.model_updated)
534 def gather_matches(self, case_sensitive):
535 refs = filter_matches(
536 self.match_text, self.matches(), case_sensitive, sort_key=ref_sort_key
538 return (refs, (), set())
540 # pylint: disable=no-self-use
541 def matches(self):
542 return []
545 class GitRefCompletionModel(GitCompletionModel):
546 """Completer for branches and tags"""
548 def __init__(self, context, parent):
549 GitCompletionModel.__init__(self, context, parent)
550 context.model.refs_updated.connect(self.model_updated)
552 def matches(self):
553 model = self.context.model
554 return model.local_branches + model.remote_branches + model.tags
557 def find_potential_branches(model):
558 remotes = model.remotes
559 remote_branches = model.remote_branches
561 ambiguous = set()
562 allnames = set(model.local_branches)
563 potential = []
565 for remote_branch in remote_branches:
566 branch = gitcmds.strip_remote(remotes, remote_branch)
567 if branch in allnames or branch == remote_branch:
568 ambiguous.add(branch)
569 continue
570 potential.append(branch)
571 allnames.add(branch)
573 potential_branches = [p for p in potential if p not in ambiguous]
574 return potential_branches
577 class GitCreateBranchCompletionModel(GitCompletionModel):
578 """Completer for naming new branches"""
580 def matches(self):
581 model = self.context.model
582 potential_branches = find_potential_branches(model)
583 return model.local_branches + potential_branches + model.tags
586 class GitCheckoutBranchCompletionModel(GitCompletionModel):
587 """Completer for git checkout <branch>"""
589 def matches(self):
590 model = self.context.model
591 potential_branches = find_potential_branches(model)
592 return (
593 model.local_branches
594 + potential_branches
595 + model.remote_branches
596 + model.tags
600 class GitBranchCompletionModel(GitCompletionModel):
601 """Completer for local branches"""
603 def __init__(self, context, parent):
604 GitCompletionModel.__init__(self, context, parent)
606 def matches(self):
607 model = self.context.model
608 return model.local_branches
611 class GitRemoteBranchCompletionModel(GitCompletionModel):
612 """Completer for remote branches"""
614 def __init__(self, context, parent):
615 GitCompletionModel.__init__(self, context, parent)
617 def matches(self):
618 model = self.context.model
619 return model.remote_branches
622 class GitPathCompletionModel(GitCompletionModel):
623 """Base class for path completion"""
625 def __init__(self, context, parent):
626 GitCompletionModel.__init__(self, context, parent)
628 # pylint: disable=no-self-use
629 def candidate_paths(self):
630 return []
632 def gather_matches(self, case_sensitive):
633 paths, dirs = filter_path_matches(
634 self.match_text, self.candidate_paths(), case_sensitive
636 return ((), paths, dirs)
639 class GitStatusFilterCompletionModel(GitPathCompletionModel):
640 """Completer for modified files and folders for status filtering"""
642 def __init__(self, context, parent):
643 GitPathCompletionModel.__init__(self, context, parent)
645 def candidate_paths(self):
646 model = self.context.model
647 return model.staged + model.unmerged + 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, case_sensitive)
668 return (refs, paths, dirs)
671 class GitLogCompletionModel(GitRefCompletionModel):
672 """Completer for arguments suitable for git-log like commands"""
674 def __init__(self, context, parent):
675 GitRefCompletionModel.__init__(self, context, parent)
676 self.model_updated.connect(self.gather_paths, type=Qt.QueuedConnection)
677 self._paths = []
678 self._model = context.model
680 def gather_paths(self):
681 if not self._model.cfg.get(prefs.AUTOCOMPLETE_PATHS, True):
682 self._paths = []
683 return
684 context = self.context
685 self._paths = gitcmds.tracked_files(context)
687 def gather_matches(self, case_sensitive):
688 if not self._paths:
689 self.gather_paths()
690 refs = filter_matches(
691 self.match_text, self.matches(), case_sensitive, sort_key=ref_sort_key
693 paths, dirs = filter_path_matches(self.match_text, self._paths, case_sensitive)
694 has_doubledash = (
695 self.match_text == '--'
696 or self.full_text.startswith('-- ')
697 or ' -- ' in self.full_text
699 if has_doubledash:
700 refs = []
701 elif refs and paths:
702 paths.insert(0, '--')
704 return (refs, paths, dirs)
707 def bind_lineedit(model, hint=''):
708 """Create a line edit bound against a specific model"""
710 class BoundLineEdit(CompletionLineEdit):
711 def __init__(self, context, hint=hint, parent=None):
712 CompletionLineEdit.__init__(self, context, model, hint=hint, parent=parent)
713 self.context = context
715 return BoundLineEdit
718 # Concrete classes
719 GitLogLineEdit = bind_lineedit(GitLogCompletionModel, hint='<ref>')
720 GitRefLineEdit = bind_lineedit(GitRefCompletionModel, hint='<ref>')
721 GitCheckoutBranchLineEdit = bind_lineedit(
722 GitCheckoutBranchCompletionModel, hint='<branch>'
724 GitCreateBranchLineEdit = bind_lineedit(GitCreateBranchCompletionModel, hint='<branch>')
725 GitBranchLineEdit = bind_lineedit(GitBranchCompletionModel, hint='<branch>')
726 GitRemoteBranchLineEdit = bind_lineedit(
727 GitRemoteBranchCompletionModel, hint='<remote-branch>'
729 GitStatusFilterLineEdit = bind_lineedit(GitStatusFilterCompletionModel, hint='<path>')
730 GitTrackedLineEdit = bind_lineedit(GitTrackedCompletionModel, hint='<path>')
733 class GitDialog(QtWidgets.QDialog):
734 def __init__(self, lineedit, context, title, text, parent, icon=None):
735 QtWidgets.QDialog.__init__(self, parent)
736 self.context = context
737 self.setWindowTitle(title)
738 self.setWindowModality(Qt.WindowModal)
739 self.setMinimumWidth(333)
741 self.label = QtWidgets.QLabel()
742 self.label.setText(title)
743 self.lineedit = lineedit(context)
744 self.ok_button = qtutils.ok_button(text, icon=icon, enabled=False)
745 self.close_button = qtutils.close_button()
747 self.button_layout = qtutils.hbox(
748 defs.no_margin,
749 defs.button_spacing,
750 qtutils.STRETCH,
751 self.close_button,
752 self.ok_button,
755 self.main_layout = qtutils.vbox(
756 defs.margin, defs.spacing, self.label, self.lineedit, self.button_layout
758 self.setLayout(self.main_layout)
760 self.lineedit.textChanged.connect(self.text_changed)
761 self.lineedit.enter.connect(self.accept)
762 qtutils.connect_button(self.ok_button, self.accept)
763 qtutils.connect_button(self.close_button, self.reject)
765 self.setFocusProxy(self.lineedit)
766 self.lineedit.setFocus()
768 def text(self):
769 return self.lineedit.text()
771 def text_changed(self, _txt):
772 self.ok_button.setEnabled(bool(self.text()))
774 def set_text(self, ref):
775 self.lineedit.setText(ref)
777 @classmethod
778 def get(cls, context, title, text, parent, default=None, icon=None):
779 dlg = cls(context, title, text, parent, icon=icon)
780 if default:
781 dlg.set_text(default)
783 dlg.show()
785 def show_popup():
786 x = dlg.lineedit.x()
787 y = dlg.lineedit.y() + dlg.lineedit.height()
788 point = QtCore.QPoint(x, y)
789 mapped = dlg.mapToGlobal(point)
790 dlg.lineedit.popup().move(mapped.x(), mapped.y())
791 dlg.lineedit.popup().show()
792 dlg.lineedit.refresh()
793 dlg.lineedit.setFocus()
795 QtCore.QTimer().singleShot(100, show_popup)
797 if dlg.exec_() == cls.Accepted:
798 return dlg.text()
799 return None
802 class GitRefDialog(GitDialog):
803 def __init__(self, context, title, text, parent, icon=None):
804 GitDialog.__init__(
805 self, GitRefLineEdit, context, title, text, parent, icon=icon
809 class GitCheckoutBranchDialog(GitDialog):
810 def __init__(self, context, title, text, parent, icon=None):
811 GitDialog.__init__(
812 self, GitCheckoutBranchLineEdit, context, title, text, parent, icon=icon
816 class GitBranchDialog(GitDialog):
817 def __init__(self, context, title, text, parent, icon=None):
818 GitDialog.__init__(
819 self, GitBranchLineEdit, context, title, text, parent, icon=icon
823 class GitRemoteBranchDialog(GitDialog):
824 def __init__(self, context, title, text, parent, icon=None):
825 GitDialog.__init__(
826 self, GitRemoteBranchLineEdit, context, title, text, parent, icon=icon