commitmsg: move the progress bar into the dock title
[git-cola.git] / cola / widgets / branch.py
blob98e3b07cadf8345eb291efb5eb6a4ddefe0c7a0a
1 """Provides widgets related to branches"""
2 from functools import partial
4 from qtpy import QtWidgets
5 from qtpy.QtCore import Qt
6 from qtpy.QtCore import Signal
8 from ..compat import uchr
9 from ..git import STDOUT
10 from ..i18n import N_
11 from ..interaction import Interaction
12 from ..widgets import defs
13 from ..widgets import standard
14 from ..qtutils import get
15 from .. import cmds
16 from .. import gitcmds
17 from .. import hotkeys
18 from .. import icons
19 from .. import qtutils
20 from . import log
21 from . import text
24 def defer_func(parent, title, func, *args, **kwargs):
25 """Return a QAction bound against a partial func with arguments"""
26 return qtutils.add_action(parent, title, partial(func, *args, **kwargs))
29 def add_branch_to_menu(menu, branch, remote_branch, remote, upstream, func):
30 """Add a remote branch to the context menu"""
31 branch_remote, _ = gitcmds.parse_remote_branch(remote_branch)
32 if branch_remote != remote:
33 menu.addSeparator()
34 action = defer_func(menu, remote_branch, func, branch, remote_branch)
35 if remote_branch == upstream:
36 action.setIcon(icons.star())
37 menu.addAction(action)
38 return branch_remote
41 class AsyncGitActionTask(qtutils.Task):
42 """Run git action asynchronously"""
44 def __init__(self, git_helper, action, args, kwarg, update_refs):
45 qtutils.Task.__init__(self)
46 self.git_helper = git_helper
47 self.action = action
48 self.args = args
49 self.kwarg = kwarg
50 self.update_refs = update_refs
52 def task(self):
53 """Runs action and captures the result"""
54 git_action = getattr(self.git_helper, self.action)
55 return git_action(*self.args, **self.kwarg)
58 class BranchesWidget(QtWidgets.QFrame):
59 """A widget for displaying and performing operations on branches"""
61 def __init__(self, context, parent):
62 QtWidgets.QFrame.__init__(self, parent)
63 self.model = model = context.model
65 tooltip = N_('Toggle the branches filter')
66 icon = icons.ellipsis()
67 self.filter_button = qtutils.create_action_button(tooltip=tooltip, icon=icon)
69 self.order_icons = (
70 icons.alphabetical(),
71 icons.reverse_chronological(),
73 tooltip_order = N_(
74 'Set the sort order for branches and tags.\n'
75 'Toggle between date-based and version-name-based sorting.'
77 icon = self.order_icon(model.ref_sort)
78 self.sort_order_button = qtutils.create_action_button(
79 tooltip=tooltip_order, icon=icon
82 self.tree = BranchesTreeWidget(context, parent=self)
83 self.filter_widget = BranchesFilterWidget(self.tree)
84 self.filter_widget.hide()
86 self.setFocusProxy(self.tree)
87 self.setToolTip(N_('Branches'))
89 self.main_layout = qtutils.vbox(
90 defs.no_margin, defs.spacing, self.filter_widget, self.tree
92 self.setLayout(self.main_layout)
94 self.toggle_action = qtutils.add_action(
95 self, tooltip, self.toggle_filter, hotkeys.FILTER
97 qtutils.connect_button(self.filter_button, self.toggle_filter)
98 qtutils.connect_button(
99 self.sort_order_button, cmds.run(cmds.CycleReferenceSort, context)
102 model.refs_updated.connect(self.refresh, Qt.QueuedConnection)
104 def toggle_filter(self):
105 shown = not self.filter_widget.isVisible()
106 self.filter_widget.setVisible(shown)
107 if shown:
108 self.filter_widget.setFocus()
109 else:
110 self.tree.setFocus()
112 def order_icon(self, idx):
113 return self.order_icons[idx % len(self.order_icons)]
115 def refresh(self):
116 icon = self.order_icon(self.model.ref_sort)
117 self.sort_order_button.setIcon(icon)
120 # pylint: disable=too-many-ancestors
121 class BranchesTreeWidget(standard.TreeWidget):
122 """A tree widget for displaying branches"""
124 updated = Signal()
126 def __init__(self, context, parent=None):
127 standard.TreeWidget.__init__(self, parent)
129 self.context = context
131 self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
132 self.setHeaderHidden(True)
133 self.setAlternatingRowColors(False)
134 self.setColumnCount(1)
135 self.setExpandsOnDoubleClick(False)
137 self.current_branch = None
138 self.tree_helper = BranchesTreeHelper(self)
139 self.git_helper = GitHelper(context)
140 self.runtask = qtutils.RunTask(parent=self)
142 self._visible = False
143 self._needs_refresh = False
144 self._tree_states = None
146 self.updated.connect(self.refresh, type=Qt.QueuedConnection)
147 context.model.updated.connect(self.updated)
149 # Expand items when they are clicked
150 # pylint: disable=no-member
151 self.clicked.connect(self._toggle_expanded)
153 # Checkout branch when double clicked
154 self.doubleClicked.connect(self.checkout_action)
156 def refresh(self):
157 """Refresh the UI widgets to match the current state"""
158 self._needs_refresh = True
159 self._refresh()
161 def _refresh(self):
162 """Refresh the UI to match the updated state"""
163 # There is no need to refresh the UI when this widget is inactive.
164 if not self._visible:
165 return
166 model = self.context.model
167 self.current_branch = model.currentbranch
169 self._tree_states = self._save_tree_state()
170 ellipsis = icons.ellipsis()
172 local_tree = create_tree_entries(model.local_branches)
173 local_tree.basename = N_('Local')
174 local = create_toplevel_item(local_tree, icon=icons.branch(), ellipsis=ellipsis)
176 remote_tree = create_tree_entries(model.remote_branches)
177 remote_tree.basename = N_('Remote')
178 remote = create_toplevel_item(
179 remote_tree, icon=icons.branch(), ellipsis=ellipsis
182 tags_tree = create_tree_entries(model.tags)
183 tags_tree.basename = N_('Tags')
184 tags = create_toplevel_item(tags_tree, icon=icons.tag(), ellipsis=ellipsis)
186 self.clear()
187 self.addTopLevelItems([local, remote, tags])
189 if self._tree_states:
190 self._load_tree_state(self._tree_states)
191 self._tree_states = None
193 self._update_branches()
195 def showEvent(self, event):
196 """Defer updating widgets until the widget is visible"""
197 if not self._visible:
198 self._visible = True
199 if self._needs_refresh:
200 self.refresh()
201 return super().showEvent(event)
203 def _toggle_expanded(self, index):
204 """Toggle expanded/collapsed state when items are clicked"""
205 item = self.itemFromIndex(index)
206 if item and item.childCount():
207 self.setExpanded(index, not self.isExpanded(index))
209 def contextMenuEvent(self, event):
210 """Build and execute the context menu"""
211 context = self.context
212 selected = self.selected_item()
213 if not selected:
214 return
215 # Only allow actions on leaf nodes that have a valid refname.
216 if not selected.refname:
217 return
219 root = get_toplevel_item(selected)
220 full_name = selected.refname
221 menu = qtutils.create_menu(N_('Actions'), self)
223 visualize_action = qtutils.add_action(
224 menu, N_('Visualize'), self.visualize_branch_action
226 visualize_action.setIcon(icons.visualize())
227 menu.addAction(visualize_action)
228 menu.addSeparator()
230 # all branches except current the current branch
231 if full_name != self.current_branch:
232 menu.addAction(
233 qtutils.add_action(menu, N_('Checkout'), self.checkout_action)
235 # remote branch
236 if root.name == N_('Remote'):
237 label = N_('Checkout as new branch')
238 action = self.checkout_new_branch_action
239 menu.addAction(qtutils.add_action(menu, label, action))
241 merge_menu_action = qtutils.add_action(
242 menu, N_('Merge into current branch'), self.merge_action
244 merge_menu_action.setIcon(icons.merge())
245 menu.addAction(merge_menu_action)
247 # local and remote branch
248 if root.name != N_('Tags'):
249 # local branch
250 if root.name == N_('Local'):
251 remote = gitcmds.tracked_branch(context, full_name)
252 if remote is not None:
253 menu.addSeparator()
255 pull_menu_action = qtutils.add_action(
256 menu, N_('Pull'), self.pull_action
258 pull_menu_action.setIcon(icons.pull())
259 menu.addAction(pull_menu_action)
261 push_menu_action = qtutils.add_action(
262 menu, N_('Push'), self.push_action
264 push_menu_action.setIcon(icons.push())
265 menu.addAction(push_menu_action)
267 rename_menu_action = qtutils.add_action(
268 menu, N_('Rename Branch'), self.rename_action
270 rename_menu_action.setIcon(icons.edit())
272 menu.addSeparator()
273 menu.addAction(rename_menu_action)
275 # not current branch
276 if full_name != self.current_branch:
277 delete_label = N_('Delete Branch')
278 if root.name == N_('Remote'):
279 delete_label = N_('Delete Remote Branch')
281 delete_menu_action = qtutils.add_action(
282 menu, delete_label, self.delete_action
284 delete_menu_action.setIcon(icons.discard())
286 menu.addSeparator()
287 menu.addAction(delete_menu_action)
289 # manage upstreams for local branches
290 if root.name == N_('Local'):
291 upstream_menu = menu.addMenu(N_('Set Upstream Branch'))
292 upstream_menu.setIcon(icons.branch())
293 self.build_upstream_menu(upstream_menu)
295 menu.exec_(self.mapToGlobal(event.pos()))
297 def build_upstream_menu(self, menu):
298 """Build the "Set Upstream Branch" sub-menu"""
299 context = self.context
300 model = context.model
301 selected_branch = self.selected_refname()
302 remote = None
303 upstream = None
305 branches = []
306 other_branches = []
308 if selected_branch:
309 remote = gitcmds.upstream_remote(context, selected_branch)
310 upstream = gitcmds.tracked_branch(context, branch=selected_branch)
312 if not remote and 'origin' in model.remotes:
313 remote = 'origin'
315 if remote:
316 prefix = remote + '/'
317 for branch in model.remote_branches:
318 if branch.startswith(prefix):
319 branches.append(branch)
320 else:
321 other_branches.append(branch)
322 else:
323 # This can be a pretty big list, let's try to split it apart
324 branch_remote = ''
325 target = branches
326 for branch in model.remote_branches:
327 new_branch_remote, _ = gitcmds.parse_remote_branch(branch)
328 if branch_remote and branch_remote != new_branch_remote:
329 target = other_branches
330 branch_remote = new_branch_remote
331 target.append(branch)
333 limit = 16
334 if not other_branches and len(branches) > limit:
335 branches, other_branches = (branches[:limit], branches[limit:])
337 # Add an action for each remote branch
338 current_remote = remote
340 for branch in branches:
341 current_remote = add_branch_to_menu(
342 menu,
343 selected_branch,
344 branch,
345 current_remote,
346 upstream,
347 self.set_upstream,
350 # This list could be longer so we tuck it away in a sub-menu.
351 # Selecting a branch from the non-default remote is less common.
352 if other_branches:
353 menu.addSeparator()
354 sub_menu = menu.addMenu(N_('Other branches'))
355 for branch in other_branches:
356 current_remote = add_branch_to_menu(
357 sub_menu,
358 selected_branch,
359 branch,
360 current_remote,
361 upstream,
362 self.set_upstream,
365 def set_upstream(self, branch, remote_branch):
366 """Configure the upstream for a branch"""
367 context = self.context
368 remote, r_branch = gitcmds.parse_remote_branch(remote_branch)
369 if remote and r_branch:
370 cmds.do(cmds.SetUpstreamBranch, context, branch, remote, r_branch)
372 def _save_tree_state(self):
373 """Save the tree state into a dictionary"""
374 states = {}
375 for item in self.items():
376 states.update(self.tree_helper.save_state(item))
377 return states
379 def _load_tree_state(self, states):
380 """Restore expanded items after rebuilding UI widgets"""
381 # Disable animations to eliminate redraw flicker.
382 animated = self.isAnimated()
383 self.setAnimated(False)
385 for item in self.items():
386 self.tree_helper.load_state(item, states.get(item.name, {}))
387 self.tree_helper.set_current_item()
389 self.setAnimated(animated)
391 def _update_branches(self):
392 """Query branch details using a background task"""
393 context = self.context
394 current_branch = self.current_branch
395 top_item = self.topLevelItem(0)
396 item = find_by_refname(top_item, current_branch)
398 if item is not None:
399 expand_item_parents(item)
400 item.setIcon(0, icons.star())
402 branch_details_task = BranchDetailsTask(
403 context, current_branch, self.git_helper
405 self.runtask.start(
406 branch_details_task, finish=self._update_branches_finished
409 def _update_branches_finished(self, task):
410 """Update the UI with the branch details once the background task completes"""
411 current_branch, tracked_branch, ahead, behind = task.result
412 top_item = self.topLevelItem(0)
413 item = find_by_refname(top_item, current_branch)
414 if current_branch and tracked_branch and item is not None:
415 status_str = ''
416 if ahead > 0:
417 status_str += f'{uchr(0x2191)}{ahead}'
419 if behind > 0:
420 status_str += f' {uchr(0x2193)}{behind}'
422 if status_str:
423 item.setText(0, f'{item.text(0)}\t{status_str}')
425 def git_action_async(
426 self, action, args, kwarg=None, update_refs=False, remote_messages=False
428 """Execute a git action in a background task"""
429 if kwarg is None:
430 kwarg = {}
431 task = AsyncGitActionTask(self.git_helper, action, args, kwarg, update_refs)
432 progress = standard.progress(
433 N_('Executing action %s') % action, N_('Updating'), self
435 if remote_messages:
436 result_handler = log.show_remote_messages(self.context, self)
437 else:
438 result_handler = None
440 self.runtask.start(
441 task,
442 progress=progress,
443 finish=self.git_action_completed,
444 result=result_handler,
447 def git_action_completed(self, task):
448 """Update the with the results of an async git action"""
449 status, out, err = task.result
450 self.git_helper.show_result(task.action, status, out, err)
451 if task.update_refs:
452 self.context.model.update_refs()
454 def push_action(self):
455 """Push the selected branch to its upstream remote"""
456 context = self.context
457 branch = self.selected_refname()
458 remote_branch = gitcmds.tracked_branch(context, branch)
459 context.settings.load()
460 push_settings = context.settings.get_gui_state_by_name('push')
461 remote_messages = push_settings.get('remote_messages', False)
462 if remote_branch:
463 remote, branch_name = gitcmds.parse_remote_branch(remote_branch)
464 if remote and branch_name:
465 # we assume that user wants to "Push" the selected local
466 # branch to a remote with same name
467 self.git_action_async(
468 'push',
469 [remote, branch_name],
470 update_refs=True,
471 remote_messages=remote_messages,
474 def rename_action(self):
475 """Rename the selected branch"""
476 branch = self.selected_refname()
477 new_branch, ok = qtutils.prompt(
478 N_('Enter New Branch Name'), title=N_('Rename branch'), text=branch
480 if ok and new_branch:
481 self.git_action_async('rename', [branch, new_branch], update_refs=True)
483 def pull_action(self):
484 """Pull the selected branch into the current branch"""
485 context = self.context
486 branch = self.selected_refname()
487 if not branch:
488 return
489 remote_branch = gitcmds.tracked_branch(context, branch)
490 context.settings.load()
491 pull_settings = context.settings.get_gui_state_by_name('pull')
492 remote_messages = pull_settings.get('remote_messages', False)
493 if remote_branch:
494 remote, branch_name = gitcmds.parse_remote_branch(remote_branch)
495 if remote and branch_name:
496 self.git_action_async(
497 'pull',
498 [remote, branch_name],
499 update_refs=True,
500 remote_messages=remote_messages,
503 def delete_action(self):
504 """Delete the selected branch"""
505 branch = self.selected_refname()
506 if not branch or branch == self.current_branch:
507 return
509 remote = False
510 root = get_toplevel_item(self.selected_item())
511 if not root:
512 return
513 if root.name == N_('Remote'):
514 remote = True
516 if remote:
517 remote, branch = gitcmds.parse_remote_branch(branch)
518 if remote and branch:
519 cmds.do(cmds.DeleteRemoteBranch, self.context, remote, branch)
520 else:
521 cmds.do(cmds.DeleteBranch, self.context, branch)
523 def merge_action(self):
524 """Merge the selected branch into the current branch"""
525 branch = self.selected_refname()
526 if branch and branch != self.current_branch:
527 self.git_action_async('merge', [branch])
529 def checkout_action(self):
530 """Checkout the selected branch"""
531 branch = self.selected_refname()
532 if branch and branch != self.current_branch:
533 self.git_action_async('checkout', [branch], update_refs=True)
535 def checkout_new_branch_action(self):
536 """Checkout a new branch"""
537 branch = self.selected_refname()
538 if branch and branch != self.current_branch:
539 _, new_branch = gitcmds.parse_remote_branch(branch)
540 self.git_action_async(
541 'checkout', ['-b', new_branch, branch], update_refs=True
544 def visualize_branch_action(self):
545 """Visualize the selected branch"""
546 branch = self.selected_refname()
547 if branch:
548 cmds.do(cmds.VisualizeRevision, self.context, branch)
550 def selected_refname(self):
551 return getattr(self.selected_item(), 'refname', None)
554 class BranchDetailsTask(qtutils.Task):
555 """Lookup branch details in a background task"""
557 def __init__(self, context, current_branch, git_helper):
558 super().__init__()
559 self.context = context
560 self.current_branch = current_branch
561 self.git_helper = git_helper
563 def task(self):
564 """Query git for branch details"""
565 tracked_branch = gitcmds.tracked_branch(self.context, self.current_branch)
566 ahead = 0
567 behind = 0
569 if self.current_branch and tracked_branch:
570 origin = tracked_branch + '..' + self.current_branch
571 our_commits = self.git_helper.log(origin)[STDOUT]
572 ahead = our_commits.count('\n')
573 if our_commits:
574 ahead += 1
576 origin = self.current_branch + '..' + tracked_branch
577 their_commits = self.git_helper.log(origin)[STDOUT]
578 behind = their_commits.count('\n')
579 if their_commits:
580 behind += 1
582 return self.current_branch, tracked_branch, ahead, behind
585 class BranchTreeWidgetItem(QtWidgets.QTreeWidgetItem):
586 def __init__(self, name, refname=None, icon=None):
587 QtWidgets.QTreeWidgetItem.__init__(self)
588 self.name = name
589 self.refname = refname
590 self.setText(0, name)
591 self.setToolTip(0, name)
592 if icon is not None:
593 self.setIcon(0, icon)
594 self.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
597 class TreeEntry:
598 """Tree representation for the branches widget
600 The branch widget UI displays the basename. For intermediate names, e.g.
601 "xxx" in the "xxx/abc" and "xxx/def" branches, the 'refname' will be None.
602 'children' contains a list of TreeEntry, and is empty when refname is
603 defined.
607 def __init__(self, basename, refname, children):
608 self.basename = basename
609 self.refname = refname
610 self.children = children
613 def create_tree_entries(names):
614 """Create a nested tree structure with a single root TreeEntry.
616 When names == ['xxx/abc', 'xxx/def'] the result will be::
618 TreeEntry(
619 basename=None,
620 refname=None,
621 children=[
622 TreeEntry(
623 basename='xxx',
624 refname=None,
625 children=[
626 TreeEntry(
627 basename='abc',
628 refname='xxx/abc',
629 children=[]
631 TreeEntry(
632 basename='def',
633 refname='xxx/def',
634 children=[]
642 # Phase 1: build a nested dictionary representing the intermediate
643 # names in the branches. e.g. {'xxx': {'abc': {}, 'def': {}}}
644 tree_names = create_name_dict(names)
646 # Loop over the names again, this time we'll create tree entries
647 entries = {}
648 root = TreeEntry(None, None, [])
649 for item in names:
650 cur_names = tree_names
651 cur_entries = entries
652 tree = root
653 children = root.children
654 for part in item.split('/'):
655 if cur_names[part]:
656 # This has children
657 try:
658 tree, _ = cur_entries[part]
659 except KeyError:
660 # New entry
661 tree = TreeEntry(part, None, [])
662 cur_entries[part] = (tree, {})
663 # Append onto the parent children list only once
664 children.append(tree)
665 else:
666 # This is the actual branch
667 tree = TreeEntry(part, item, [])
668 children.append(tree)
669 cur_entries[part] = (tree, {})
671 # Advance into the nested child list
672 children = tree.children
673 # Advance into the inner dict
674 cur_names = cur_names[part]
675 _, cur_entries = cur_entries[part]
677 return root
680 def create_name_dict(names):
681 # Phase 1: build a nested dictionary representing the intermediate
682 # names in the branches. e.g. {'xxx': {'abc': {}, 'def': {}}}
683 tree_names = {}
684 for item in names:
685 part_names = tree_names
686 for part in item.split('/'):
687 # Descend into the inner names dict.
688 part_names = part_names.setdefault(part, {})
689 return tree_names
692 def create_toplevel_item(tree, icon=None, ellipsis=None):
693 """Create a top-level BranchTreeWidgetItem and its children"""
695 item = BranchTreeWidgetItem(tree.basename, icon=ellipsis)
696 children = create_tree_items(tree.children, icon=icon, ellipsis=ellipsis)
697 if children:
698 item.addChildren(children)
699 return item
702 def create_tree_items(entries, icon=None, ellipsis=None):
703 """Create children items for a tree item"""
704 result = []
705 for tree in entries:
706 item = BranchTreeWidgetItem(tree.basename, refname=tree.refname, icon=icon)
707 children = create_tree_items(tree.children, icon=icon, ellipsis=ellipsis)
708 if children:
709 item.addChildren(children)
710 if ellipsis is not None:
711 item.setIcon(0, ellipsis)
712 result.append(item)
714 return result
717 def expand_item_parents(item):
718 """Expand tree parents from item"""
719 parent = item.parent()
720 while parent is not None:
721 if not parent.isExpanded():
722 parent.setExpanded(True)
723 parent = parent.parent()
726 def find_by_refname(item, refname):
727 """Find child by full name recursive"""
728 result = None
730 for i in range(item.childCount()):
731 child = item.child(i)
732 if child.refname and child.refname == refname:
733 return child
735 result = find_by_refname(child, refname)
736 if result is not None:
737 return result
739 return result
742 def get_toplevel_item(item):
743 """Returns top-most item found by traversing up the specified item"""
744 parents = [item]
745 parent = item.parent()
747 while parent is not None:
748 parents.append(parent)
749 parent = parent.parent()
751 return parents[-1]
754 class BranchesTreeHelper:
755 """Save and restore the tree state"""
757 def __init__(self, tree):
758 self.tree = tree
759 self.current_item = None
761 def set_current_item(self):
762 """Reset the current item"""
763 if self.current_item is not None:
764 self.tree.setCurrentItem(self.current_item)
765 self.current_item = None
767 def load_state(self, item, state):
768 """Load expanded items from a dict"""
769 if not state:
770 return
771 if state.get('expanded', False) and not item.isExpanded():
772 item.setExpanded(True)
773 if state.get('selected', False) and not item.isSelected():
774 item.setSelected(True)
775 self.current_item = item
777 children_state = state.get('children', {})
778 if not children_state:
779 return
780 for i in range(item.childCount()):
781 child = item.child(i)
782 self.load_state(child, children_state.get(child.name, {}))
784 def save_state(self, item):
785 """Save the selected and expanded item state into a dict"""
786 expanded = item.isExpanded()
787 selected = item.isSelected()
788 children = {}
789 entry = {
790 'children': children,
791 'expanded': expanded,
792 'selected': selected,
794 result = {item.name: entry}
795 for i in range(item.childCount()):
796 child = item.child(i)
797 children.update(self.save_state(child))
799 return result
802 class GitHelper:
803 def __init__(self, context):
804 self.context = context
805 self.git = context.git
807 def log(self, origin):
808 return self.git.log(origin, abbrev=7, pretty='format:%h', _readonly=True)
810 def push(self, remote, branch):
811 return self.git.push(remote, branch, verbose=True)
813 def pull(self, remote, branch):
814 return self.git.pull(remote, branch, no_ff=True, verbose=True)
816 def merge(self, branch):
817 return self.git.merge(branch, no_commit=True)
819 def rename(self, branch, new_branch):
820 return self.git.branch(branch, new_branch, m=True)
822 def checkout(self, *args, **options):
823 return self.git.checkout(*args, **options)
825 @staticmethod
826 def show_result(command, status, out, err):
827 Interaction.log_status(status, out, err)
828 if status != 0:
829 Interaction.command_error(N_('Error'), command, status, out, err)
832 class BranchesFilterWidget(QtWidgets.QWidget):
833 def __init__(self, tree, parent=None):
834 QtWidgets.QWidget.__init__(self, parent)
835 self.tree = tree
837 hint = N_('Filter branches...')
838 self.text = text.LineEdit(parent=self, clear_button=True)
839 self.text.setToolTip(hint)
840 self.setFocusProxy(self.text)
841 self._filter = None
843 self.main_layout = qtutils.hbox(defs.no_margin, defs.spacing, self.text)
844 self.setLayout(self.main_layout)
846 # pylint: disable=no-member
847 self.text.textChanged.connect(self.apply_filter)
848 self.tree.updated.connect(self.apply_filter, type=Qt.QueuedConnection)
850 def apply_filter(self):
851 value = get(self.text)
852 if value == self._filter:
853 return
854 self._apply_bold(self._filter, False)
855 self._filter = value
856 if value:
857 self._apply_bold(value, True)
859 def _apply_bold(self, value, is_bold):
860 match = Qt.MatchContains | Qt.MatchRecursive
861 children = self.tree.findItems(value, match)
863 for child in children:
864 if child.childCount() == 0:
865 font = child.font(0)
866 font.setBold(is_bold)
867 child.setFont(0, font)