Merge pull request #1387 from davvid/remote-dialog
[git-cola.git] / cola / widgets / branch.py
blobe90eb565f5cdc8a072d7f022c75bc5340f043895
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 class BranchesTreeWidget(standard.TreeWidget):
121 """A tree widget for displaying branches"""
123 updated = Signal()
125 def __init__(self, context, parent=None):
126 standard.TreeWidget.__init__(self, parent)
128 self.context = context
130 self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
131 self.setHeaderHidden(True)
132 self.setAlternatingRowColors(False)
133 self.setColumnCount(1)
134 self.setExpandsOnDoubleClick(False)
136 self.current_branch = None
137 self.tree_helper = BranchesTreeHelper(self)
138 self.git_helper = GitHelper(context)
139 self.runtask = qtutils.RunTask(parent=self)
141 self._visible = False
142 self._needs_refresh = False
143 self._tree_states = None
145 self.updated.connect(self.refresh, type=Qt.QueuedConnection)
146 context.model.updated.connect(self.updated)
148 # Expand items when they are clicked
149 self.clicked.connect(self._toggle_expanded)
151 # Checkout branch when double clicked
152 self.doubleClicked.connect(self.checkout_action)
154 def refresh(self):
155 """Refresh the UI widgets to match the current state"""
156 self._needs_refresh = True
157 self._refresh()
159 def _refresh(self):
160 """Refresh the UI to match the updated state"""
161 # There is no need to refresh the UI when this widget is inactive.
162 if not self._visible:
163 return
164 model = self.context.model
165 self.current_branch = model.currentbranch
167 self._tree_states = self._save_tree_state()
168 ellipsis = icons.ellipsis()
170 local_tree = create_tree_entries(model.local_branches)
171 local_tree.basename = N_('Local')
172 local = create_toplevel_item(local_tree, icon=icons.branch(), ellipsis=ellipsis)
174 remote_tree = create_tree_entries(model.remote_branches)
175 remote_tree.basename = N_('Remote')
176 remote = create_toplevel_item(
177 remote_tree, icon=icons.branch(), ellipsis=ellipsis
180 tags_tree = create_tree_entries(model.tags)
181 tags_tree.basename = N_('Tags')
182 tags = create_toplevel_item(tags_tree, icon=icons.tag(), ellipsis=ellipsis)
184 self.clear()
185 self.addTopLevelItems([local, remote, tags])
187 if self._tree_states:
188 self._load_tree_state(self._tree_states)
189 self._tree_states = None
191 self._update_branches()
193 def showEvent(self, event):
194 """Defer updating widgets until the widget is visible"""
195 if not self._visible:
196 self._visible = True
197 if self._needs_refresh:
198 self.refresh()
199 return super().showEvent(event)
201 def _toggle_expanded(self, index):
202 """Toggle expanded/collapsed state when items are clicked"""
203 item = self.itemFromIndex(index)
204 if item and item.childCount():
205 self.setExpanded(index, not self.isExpanded(index))
207 def contextMenuEvent(self, event):
208 """Build and execute the context menu"""
209 context = self.context
210 selected = self.selected_item()
211 if not selected:
212 return
213 # Only allow actions on leaf nodes that have a valid refname.
214 if not selected.refname:
215 return
217 root = get_toplevel_item(selected)
218 full_name = selected.refname
219 menu = qtutils.create_menu(N_('Actions'), self)
221 visualize_action = qtutils.add_action(
222 menu, N_('Visualize'), self.visualize_branch_action
224 visualize_action.setIcon(icons.visualize())
225 menu.addAction(visualize_action)
226 menu.addSeparator()
228 # all branches except current the current branch
229 if full_name != self.current_branch:
230 menu.addAction(
231 qtutils.add_action(menu, N_('Checkout'), self.checkout_action)
233 # remote branch
234 if root.name == N_('Remote'):
235 label = N_('Checkout as new branch')
236 action = self.checkout_new_branch_action
237 menu.addAction(qtutils.add_action(menu, label, action))
239 merge_menu_action = qtutils.add_action(
240 menu, N_('Merge into current branch'), self.merge_action
242 merge_menu_action.setIcon(icons.merge())
243 menu.addAction(merge_menu_action)
245 # local and remote branch
246 if root.name != N_('Tags'):
247 # local branch
248 if root.name == N_('Local'):
249 remote = gitcmds.tracked_branch(context, full_name)
250 if remote is not None:
251 menu.addSeparator()
253 pull_menu_action = qtutils.add_action(
254 menu, N_('Pull'), self.pull_action
256 pull_menu_action.setIcon(icons.pull())
257 menu.addAction(pull_menu_action)
259 push_menu_action = qtutils.add_action(
260 menu, N_('Push'), self.push_action
262 push_menu_action.setIcon(icons.push())
263 menu.addAction(push_menu_action)
265 rename_menu_action = qtutils.add_action(
266 menu, N_('Rename Branch'), self.rename_action
268 rename_menu_action.setIcon(icons.edit())
270 menu.addSeparator()
271 menu.addAction(rename_menu_action)
273 # not current branch
274 if full_name != self.current_branch:
275 delete_label = N_('Delete Branch')
276 if root.name == N_('Remote'):
277 delete_label = N_('Delete Remote Branch')
279 delete_menu_action = qtutils.add_action(
280 menu, delete_label, self.delete_action
282 delete_menu_action.setIcon(icons.discard())
284 menu.addSeparator()
285 menu.addAction(delete_menu_action)
287 # manage upstream branches for local branches
288 if root.name == N_('Local'):
289 upstream_menu = menu.addMenu(N_('Set Upstream Branch'))
290 upstream_menu.setIcon(icons.branch())
291 self.build_upstream_menu(upstream_menu)
293 menu.exec_(self.mapToGlobal(event.pos()))
295 def build_upstream_menu(self, menu):
296 """Build the "Set Upstream Branch" sub-menu"""
297 context = self.context
298 model = context.model
299 selected_branch = self.selected_refname()
300 remote = None
301 upstream = None
303 branches = []
304 other_branches = []
306 if selected_branch:
307 remote = gitcmds.upstream_remote(context, selected_branch)
308 upstream = gitcmds.tracked_branch(context, branch=selected_branch)
310 if not remote and 'origin' in model.remotes:
311 remote = 'origin'
313 if remote:
314 prefix = remote + '/'
315 for branch in model.remote_branches:
316 if branch.startswith(prefix):
317 branches.append(branch)
318 else:
319 other_branches.append(branch)
320 else:
321 # This can be a pretty big list, let's try to split it apart
322 branch_remote = ''
323 target = branches
324 for branch in model.remote_branches:
325 new_branch_remote, _ = gitcmds.parse_remote_branch(branch)
326 if branch_remote and branch_remote != new_branch_remote:
327 target = other_branches
328 branch_remote = new_branch_remote
329 target.append(branch)
331 limit = 16
332 if not other_branches and len(branches) > limit:
333 branches, other_branches = (branches[:limit], branches[limit:])
335 # Add an action for each remote branch
336 current_remote = remote
338 for branch in branches:
339 current_remote = add_branch_to_menu(
340 menu,
341 selected_branch,
342 branch,
343 current_remote,
344 upstream,
345 self.set_upstream,
348 # This list could be longer so we tuck it away in a sub-menu.
349 # Selecting a branch from the non-default remote is less common.
350 if other_branches:
351 menu.addSeparator()
352 sub_menu = menu.addMenu(N_('Other branches'))
353 for branch in other_branches:
354 current_remote = add_branch_to_menu(
355 sub_menu,
356 selected_branch,
357 branch,
358 current_remote,
359 upstream,
360 self.set_upstream,
363 def set_upstream(self, branch, remote_branch):
364 """Configure the upstream for a branch"""
365 context = self.context
366 remote, r_branch = gitcmds.parse_remote_branch(remote_branch)
367 if remote and r_branch:
368 cmds.do(cmds.SetUpstreamBranch, context, branch, remote, r_branch)
370 def _save_tree_state(self):
371 """Save the tree state into a dictionary"""
372 states = {}
373 for item in self.items():
374 states.update(self.tree_helper.save_state(item))
375 return states
377 def _load_tree_state(self, states):
378 """Restore expanded items after rebuilding UI widgets"""
379 # Disable animations to eliminate redraw flicker.
380 animated = self.isAnimated()
381 self.setAnimated(False)
383 for item in self.items():
384 self.tree_helper.load_state(item, states.get(item.name, {}))
385 self.tree_helper.set_current_item()
387 self.setAnimated(animated)
389 def _update_branches(self):
390 """Query branch details using a background task"""
391 context = self.context
392 current_branch = self.current_branch
393 top_item = self.topLevelItem(0)
394 item = find_by_refname(top_item, current_branch)
396 if item is not None:
397 expand_item_parents(item)
398 item.setIcon(0, icons.star())
400 branch_details_task = BranchDetailsTask(
401 context, current_branch, self.git_helper
403 self.runtask.start(
404 branch_details_task, finish=self._update_branches_finished
407 def _update_branches_finished(self, task):
408 """Update the UI with the branch details once the background task completes"""
409 current_branch, tracked_branch, ahead, behind = task.result
410 top_item = self.topLevelItem(0)
411 item = find_by_refname(top_item, current_branch)
412 if current_branch and tracked_branch and item is not None:
413 status_str = ''
414 if ahead > 0:
415 status_str += f'{uchr(0x2191)}{ahead}'
417 if behind > 0:
418 status_str += f' {uchr(0x2193)}{behind}'
420 if status_str:
421 item.setText(0, f'{item.text(0)}\t{status_str}')
423 def git_action_async(
424 self, action, args, kwarg=None, update_refs=False, remote_messages=False
426 """Execute a git action in a background task"""
427 if kwarg is None:
428 kwarg = {}
429 task = AsyncGitActionTask(self.git_helper, action, args, kwarg, update_refs)
430 progress = standard.progress(
431 N_('Executing action %s') % action, N_('Updating'), self
433 if remote_messages:
434 result_handler = log.show_remote_messages(self.context, self)
435 else:
436 result_handler = None
438 self.runtask.start(
439 task,
440 progress=progress,
441 finish=self.git_action_completed,
442 result=result_handler,
445 def git_action_completed(self, task):
446 """Update the with the results of an async git action"""
447 status, out, err = task.result
448 self.git_helper.show_result(task.action, status, out, err)
449 if task.update_refs:
450 self.context.model.update_refs()
452 def push_action(self):
453 """Push the selected branch to its upstream remote"""
454 context = self.context
455 branch = self.selected_refname()
456 remote_branch = gitcmds.tracked_branch(context, branch)
457 context.settings.load()
458 push_settings = context.settings.get_gui_state_by_name('push')
459 remote_messages = push_settings.get('remote_messages', False)
460 if remote_branch:
461 remote, branch_name = gitcmds.parse_remote_branch(remote_branch)
462 if remote and branch_name:
463 # we assume that user wants to "Push" the selected local
464 # branch to a remote with same name
465 self.git_action_async(
466 'push',
467 [remote, branch_name],
468 update_refs=True,
469 remote_messages=remote_messages,
472 def rename_action(self):
473 """Rename the selected branch"""
474 branch = self.selected_refname()
475 new_branch, ok = qtutils.prompt(
476 N_('Enter New Branch Name'), title=N_('Rename branch'), text=branch
478 if ok and new_branch:
479 self.git_action_async('rename', [branch, new_branch], update_refs=True)
481 def pull_action(self):
482 """Pull the selected branch into the current branch"""
483 context = self.context
484 branch = self.selected_refname()
485 if not branch:
486 return
487 remote_branch = gitcmds.tracked_branch(context, branch)
488 context.settings.load()
489 pull_settings = context.settings.get_gui_state_by_name('pull')
490 remote_messages = pull_settings.get('remote_messages', False)
491 if remote_branch:
492 remote, branch_name = gitcmds.parse_remote_branch(remote_branch)
493 if remote and branch_name:
494 self.git_action_async(
495 'pull',
496 [remote, branch_name],
497 update_refs=True,
498 remote_messages=remote_messages,
501 def delete_action(self):
502 """Delete the selected branch"""
503 branch = self.selected_refname()
504 if not branch or branch == self.current_branch:
505 return
507 remote = False
508 root = get_toplevel_item(self.selected_item())
509 if not root:
510 return
511 if root.name == N_('Remote'):
512 remote = True
514 if remote:
515 remote, branch = gitcmds.parse_remote_branch(branch)
516 if remote and branch:
517 cmds.do(cmds.DeleteRemoteBranch, self.context, remote, branch)
518 else:
519 cmds.do(cmds.DeleteBranch, self.context, branch)
521 def merge_action(self):
522 """Merge the selected branch into the current branch"""
523 branch = self.selected_refname()
524 if branch and branch != self.current_branch:
525 self.git_action_async('merge', [branch])
527 def checkout_action(self):
528 """Checkout the selected branch"""
529 branch = self.selected_refname()
530 if branch and branch != self.current_branch:
531 self.git_action_async('checkout', [branch], update_refs=True)
533 def checkout_new_branch_action(self):
534 """Checkout a new branch"""
535 branch = self.selected_refname()
536 if branch and branch != self.current_branch:
537 _, new_branch = gitcmds.parse_remote_branch(branch)
538 self.git_action_async(
539 'checkout', ['-b', new_branch, branch], update_refs=True
542 def visualize_branch_action(self):
543 """Visualize the selected branch"""
544 branch = self.selected_refname()
545 if branch:
546 cmds.do(cmds.VisualizeRevision, self.context, branch)
548 def selected_refname(self):
549 return getattr(self.selected_item(), 'refname', None)
552 class BranchDetailsTask(qtutils.Task):
553 """Lookup branch details in a background task"""
555 def __init__(self, context, current_branch, git_helper):
556 super().__init__()
557 self.context = context
558 self.current_branch = current_branch
559 self.git_helper = git_helper
561 def task(self):
562 """Query git for branch details"""
563 tracked_branch = gitcmds.tracked_branch(self.context, self.current_branch)
564 ahead = 0
565 behind = 0
567 if self.current_branch and tracked_branch:
568 origin = tracked_branch + '..' + self.current_branch
569 our_commits = self.git_helper.log(origin)[STDOUT]
570 ahead = our_commits.count('\n')
571 if our_commits:
572 ahead += 1
574 origin = self.current_branch + '..' + tracked_branch
575 their_commits = self.git_helper.log(origin)[STDOUT]
576 behind = their_commits.count('\n')
577 if their_commits:
578 behind += 1
580 return self.current_branch, tracked_branch, ahead, behind
583 class BranchTreeWidgetItem(QtWidgets.QTreeWidgetItem):
584 def __init__(self, name, refname=None, icon=None):
585 QtWidgets.QTreeWidgetItem.__init__(self)
586 self.name = name
587 self.refname = refname
588 self.setText(0, name)
589 self.setToolTip(0, name)
590 if icon is not None:
591 self.setIcon(0, icon)
592 self.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
595 class TreeEntry:
596 """Tree representation for the branches widget
598 The branch widget UI displays the basename. For intermediate names, e.g.
599 "xxx" in the "xxx/abc" and "xxx/def" branches, the 'refname' will be None.
600 'children' contains a list of TreeEntry, and is empty when refname is
601 defined.
605 def __init__(self, basename, refname, children):
606 self.basename = basename
607 self.refname = refname
608 self.children = children
611 def create_tree_entries(names):
612 """Create a nested tree structure with a single root TreeEntry.
614 When names == ['xxx/abc', 'xxx/def'] the result will be::
616 TreeEntry(
617 basename=None,
618 refname=None,
619 children=[
620 TreeEntry(
621 basename='xxx',
622 refname=None,
623 children=[
624 TreeEntry(
625 basename='abc',
626 refname='xxx/abc',
627 children=[]
629 TreeEntry(
630 basename='def',
631 refname='xxx/def',
632 children=[]
640 # Phase 1: build a nested dictionary representing the intermediate
641 # names in the branches, e.g. {'xxx': {'abc': {}, 'def': {}}}
642 tree_names = create_name_dict(names)
644 # Loop over the names again, this time we'll create tree entries
645 entries = {}
646 root = TreeEntry(None, None, [])
647 for item in names:
648 cur_names = tree_names
649 cur_entries = entries
650 tree = root
651 children = root.children
652 for part in item.split('/'):
653 if cur_names[part]:
654 # This has children
655 try:
656 tree, _ = cur_entries[part]
657 except KeyError:
658 # New entry
659 tree = TreeEntry(part, None, [])
660 cur_entries[part] = (tree, {})
661 # Append onto the parent children list only once
662 children.append(tree)
663 else:
664 # This is the actual branch
665 tree = TreeEntry(part, item, [])
666 children.append(tree)
667 cur_entries[part] = (tree, {})
669 # Advance into the nested child list
670 children = tree.children
671 # Advance into the inner dict
672 cur_names = cur_names[part]
673 _, cur_entries = cur_entries[part]
675 return root
678 def create_name_dict(names):
679 # Phase 1: build a nested dictionary representing the intermediate
680 # names in the branches, e.g. {'xxx': {'abc': {}, 'def': {}}}
681 tree_names = {}
682 for item in names:
683 part_names = tree_names
684 for part in item.split('/'):
685 # Descend into the inner names dict.
686 part_names = part_names.setdefault(part, {})
687 return tree_names
690 def create_toplevel_item(tree, icon=None, ellipsis=None):
691 """Create a top-level BranchTreeWidgetItem and its children"""
693 item = BranchTreeWidgetItem(tree.basename, icon=ellipsis)
694 children = create_tree_items(tree.children, icon=icon, ellipsis=ellipsis)
695 if children:
696 item.addChildren(children)
697 return item
700 def create_tree_items(entries, icon=None, ellipsis=None):
701 """Create children items for a tree item"""
702 result = []
703 for tree in entries:
704 item = BranchTreeWidgetItem(tree.basename, refname=tree.refname, icon=icon)
705 children = create_tree_items(tree.children, icon=icon, ellipsis=ellipsis)
706 if children:
707 item.addChildren(children)
708 if ellipsis is not None:
709 item.setIcon(0, ellipsis)
710 result.append(item)
712 return result
715 def expand_item_parents(item):
716 """Expand tree parents from item"""
717 parent = item.parent()
718 while parent is not None:
719 if not parent.isExpanded():
720 parent.setExpanded(True)
721 parent = parent.parent()
724 def find_by_refname(item, refname):
725 """Find child by full name recursive"""
726 result = None
728 for i in range(item.childCount()):
729 child = item.child(i)
730 if child.refname and child.refname == refname:
731 return child
733 result = find_by_refname(child, refname)
734 if result is not None:
735 return result
737 return result
740 def get_toplevel_item(item):
741 """Returns top-most item found by traversing up the specified item"""
742 parents = [item]
743 parent = item.parent()
745 while parent is not None:
746 parents.append(parent)
747 parent = parent.parent()
749 return parents[-1]
752 class BranchesTreeHelper:
753 """Save and restore the tree state"""
755 def __init__(self, tree):
756 self.tree = tree
757 self.current_item = None
759 def set_current_item(self):
760 """Reset the current item"""
761 if self.current_item is not None:
762 self.tree.setCurrentItem(self.current_item)
763 self.current_item = None
765 def load_state(self, item, state):
766 """Load expanded items from a dict"""
767 if not state:
768 return
769 if state.get('expanded', False) and not item.isExpanded():
770 item.setExpanded(True)
771 if state.get('selected', False) and not item.isSelected():
772 item.setSelected(True)
773 self.current_item = item
775 children_state = state.get('children', {})
776 if not children_state:
777 return
778 for i in range(item.childCount()):
779 child = item.child(i)
780 self.load_state(child, children_state.get(child.name, {}))
782 def save_state(self, item):
783 """Save the selected and expanded item state into a dict"""
784 expanded = item.isExpanded()
785 selected = item.isSelected()
786 children = {}
787 entry = {
788 'children': children,
789 'expanded': expanded,
790 'selected': selected,
792 result = {item.name: entry}
793 for i in range(item.childCount()):
794 child = item.child(i)
795 children.update(self.save_state(child))
797 return result
800 class GitHelper:
801 def __init__(self, context):
802 self.context = context
803 self.git = context.git
805 def log(self, origin):
806 return self.git.log(origin, abbrev=7, pretty='format:%h', _readonly=True)
808 def push(self, remote, branch):
809 return self.git.push(remote, branch, verbose=True)
811 def pull(self, remote, branch):
812 return self.git.pull(remote, branch, no_ff=True, verbose=True)
814 def merge(self, branch):
815 return self.git.merge(branch, no_commit=True)
817 def rename(self, branch, new_branch):
818 return self.git.branch(branch, new_branch, m=True)
820 def checkout(self, *args, **options):
821 return self.git.checkout(*args, **options)
823 @staticmethod
824 def show_result(command, status, out, err):
825 Interaction.log_status(status, out, err)
826 if status != 0:
827 Interaction.command_error(N_('Error'), command, status, out, err)
830 class BranchesFilterWidget(QtWidgets.QWidget):
831 def __init__(self, tree, parent=None):
832 QtWidgets.QWidget.__init__(self, parent)
833 self.tree = tree
835 hint = N_('Filter branches...')
836 self.text = text.LineEdit(parent=self, clear_button=True)
837 self.text.setToolTip(hint)
838 self.setFocusProxy(self.text)
839 self._filter = None
841 self.main_layout = qtutils.hbox(defs.no_margin, defs.spacing, self.text)
842 self.setLayout(self.main_layout)
844 self.text.textChanged.connect(self.apply_filter)
845 self.tree.updated.connect(self.apply_filter, type=Qt.QueuedConnection)
847 def apply_filter(self):
848 value = get(self.text)
849 if value == self._filter:
850 return
851 self._apply_bold(self._filter, False)
852 self._filter = value
853 if value:
854 self._apply_bold(value, True)
856 def _apply_bold(self, value, is_bold):
857 match = Qt.MatchContains | Qt.MatchRecursive
858 children = self.tree.findItems(value, match)
860 for child in children:
861 if child.childCount() == 0:
862 font = child.font(0)
863 font.setBold(is_bold)
864 child.setFont(0, font)