git-cola v4.3.1
[git-cola.git] / cola / widgets / main.py
blob4c4c7e56117bf006cf4dec6bb46a5eb4d17cedf8
1 """Main UI for authoring commits and other Git Cola interactions"""
2 from __future__ import absolute_import, division, print_function, unicode_literals
3 import os
4 from functools import partial
6 from qtpy import QtCore
7 from qtpy import QtGui
8 from qtpy import QtWidgets
9 from qtpy.QtCore import Qt
10 from qtpy.QtCore import Signal
12 from ..compat import uchr
13 from ..compat import WIN32
14 from ..i18n import N_
15 from ..interaction import Interaction
16 from ..models import prefs
17 from ..qtutils import get
18 from .. import cmds
19 from .. import core
20 from .. import guicmds
21 from .. import git
22 from .. import gitcmds
23 from .. import hotkeys
24 from .. import icons
25 from .. import qtutils
26 from .. import resources
27 from .. import utils
28 from .. import version
29 from . import about
30 from . import action
31 from . import archive
32 from . import bookmarks
33 from . import branch
34 from . import submodules
35 from . import browse
36 from . import cfgactions
37 from . import clone
38 from . import commitmsg
39 from . import common
40 from . import compare
41 from . import createbranch
42 from . import createtag
43 from . import dag
44 from . import defs
45 from . import diff
46 from . import finder
47 from . import editremotes
48 from . import grep
49 from . import log
50 from . import merge
51 from . import patch
52 from . import prefs as prefs_widget
53 from . import recent
54 from . import remote
55 from . import search
56 from . import standard
57 from . import status
58 from . import stash
59 from . import toolbar
62 class MainView(standard.MainWindow):
63 config_actions_changed = Signal(object)
65 def __init__(self, context, parent=None):
66 # pylint: disable=too-many-statements,too-many-locals
67 standard.MainWindow.__init__(self, parent)
68 self.setAttribute(Qt.WA_DeleteOnClose)
70 self.context = context
71 self.git = context.git
72 self.dag = None
73 self.model = context.model
74 self.prefs_model = prefs_model = prefs.PreferencesModel(context)
75 self.toolbar_state = toolbar.ToolBarState(context, self)
77 # The widget version is used by import/export_state().
78 # Change this whenever dockwidgets are removed.
79 self.widget_version = 2
81 create_dock = qtutils.create_dock
82 cfg = context.cfg
83 self.browser_dockable = cfg.get('cola.browserdockable')
84 if self.browser_dockable:
85 browser = browse.worktree_browser(
86 context, parent=self, show=False, update=False
88 self.browserdock = create_dock(
89 'Browser', N_('Browser'), self, widget=browser
92 # "Actions" widget
93 self.actionswidget = action.ActionButtons(context, self)
94 self.actionsdock = create_dock(
95 'Actions', N_('Actions'), self, widget=self.actionswidget
97 qtutils.hide_dock(self.actionsdock)
99 # "Repository Status" widget
100 self.statusdock = create_dock(
101 'Status',
102 N_('Status'),
103 self,
104 func=lambda dock: status.StatusWidget(context, dock.titleBarWidget(), dock),
106 self.statuswidget = self.statusdock.widget()
108 # "Switch Repository" widgets
109 self.bookmarksdock = create_dock(
110 'Favorites',
111 N_('Favorites'),
112 self,
113 func=lambda dock: bookmarks.bookmark(context, dock),
115 bookmarkswidget = self.bookmarksdock.widget()
116 qtutils.hide_dock(self.bookmarksdock)
118 self.recentdock = create_dock(
119 'Recent',
120 N_('Recent'),
121 self,
122 func=lambda dock: bookmarks.recent(context, dock),
124 recentwidget = self.recentdock.widget()
125 qtutils.hide_dock(self.recentdock)
126 bookmarkswidget.connect_to(recentwidget)
128 # "Branch" widgets
129 self.branchdock = create_dock(
130 'Branches',
131 N_('Branches'),
132 self,
133 func=partial(branch.BranchesWidget, context),
135 self.branchwidget = self.branchdock.widget()
136 titlebar = self.branchdock.titleBarWidget()
137 titlebar.add_corner_widget(self.branchwidget.filter_button)
138 titlebar.add_corner_widget(self.branchwidget.sort_order_button)
140 # "Submodule" widgets
141 self.submodulesdock = create_dock(
142 'Submodules',
143 N_('Submodules'),
144 self,
145 func=partial(submodules.SubmodulesWidget, context),
147 self.submoduleswidget = self.submodulesdock.widget()
149 # "Commit Message Editor" widget
150 self.position_label = QtWidgets.QLabel()
151 self.position_label.setAlignment(Qt.AlignCenter)
152 font = qtutils.default_monospace_font()
153 font.setPointSize(int(font.pointSize() * 0.8))
154 self.position_label.setFont(font)
156 # make the position label fixed size to avoid layout issues
157 metrics = self.position_label.fontMetrics()
158 width = metrics.width('99:999') + defs.spacing
159 self.position_label.setMinimumWidth(width)
161 editor = commitmsg.CommitMessageEditor(context, self)
162 self.commiteditor = editor
163 self.commitdock = create_dock('Commit', N_('Commit'), self, widget=editor)
164 titlebar = self.commitdock.titleBarWidget()
165 titlebar.add_corner_widget(self.position_label)
167 # "Console" widget
168 self.logwidget = log.LogWidget(context)
169 self.logdock = create_dock(
170 'Console', N_('Console'), self, widget=self.logwidget
172 qtutils.hide_dock(self.logdock)
174 # "Diff Viewer" widget
175 self.diffdock = create_dock(
176 'Diff',
177 N_('Diff'),
178 self,
179 func=lambda dock: diff.Viewer(context, parent=dock),
181 self.diffviewer = self.diffdock.widget()
182 self.diffviewer.set_diff_type(self.model.diff_type)
184 self.diffeditor = self.diffviewer.text
185 titlebar = self.diffdock.titleBarWidget()
186 titlebar.add_corner_widget(self.diffviewer.options)
188 # All Actions
189 add_action = qtutils.add_action
190 add_action_bool = qtutils.add_action_bool
192 self.commit_amend_action = add_action_bool(
193 self,
194 N_('Amend Last Commit'),
195 partial(cmds.do, cmds.AmendMode, context),
196 False,
198 self.commit_amend_action.setIcon(icons.edit())
199 self.commit_amend_action.setShortcut(hotkeys.AMEND)
200 self.commit_amend_action.setShortcutContext(Qt.WidgetShortcut)
202 self.unstage_all_action = add_action(
203 self, N_('Unstage All'), cmds.run(cmds.UnstageAll, context)
205 self.unstage_all_action.setIcon(icons.remove())
207 self.undo_commit_action = add_action(
208 self, N_('Undo Last Commit'), cmds.run(cmds.UndoLastCommit, context)
210 self.undo_commit_action.setIcon(icons.style_dialog_discard())
212 self.unstage_selected_action = add_action(
213 self, N_('Unstage From Commit'), cmds.run(cmds.UnstageSelected, context)
215 self.unstage_selected_action.setIcon(icons.remove())
217 self.show_diffstat_action = add_action(
218 self, N_('Diffstat'), self.statuswidget.select_header, hotkeys.DIFFSTAT
220 self.show_diffstat_action.setIcon(icons.diff())
222 self.stage_modified_action = add_action(
223 self,
224 N_('Stage Changed Files To Commit'),
225 cmds.run(cmds.StageModified, context),
226 hotkeys.STAGE_MODIFIED,
228 self.stage_modified_action.setIcon(icons.add())
230 self.stage_untracked_action = add_action(
231 self,
232 N_('Stage All Untracked'),
233 cmds.run(cmds.StageUntracked, context),
234 hotkeys.STAGE_UNTRACKED,
236 self.stage_untracked_action.setIcon(icons.add())
238 self.apply_patches_action = add_action(
239 self, N_('Apply Patches...'), partial(patch.apply_patches, context)
241 self.apply_patches_action.setIcon(icons.diff())
243 self.apply_patches_abort_action = add_action(
244 self,
245 N_('Abort Applying Patches...'),
246 cmds.run(cmds.AbortApplyPatch, context),
248 self.apply_patches_abort_action.setIcon(icons.style_dialog_discard())
249 self.apply_patches_abort_action.setToolTip(
250 N_('Abort the current "git am" patch session')
253 self.apply_patches_continue_action = add_action(
254 self,
255 N_('Continue Applying Patches'),
256 cmds.run(cmds.ApplyPatchesContinue, context),
258 self.apply_patches_continue_action.setToolTip(
259 N_('Commit the current state and continue applying patches')
261 self.apply_patches_continue_action.setIcon(icons.commit())
263 self.apply_patches_skip_action = add_action(
264 self, N_('Skip Current Patch'), cmds.run(cmds.ApplyPatchesContinue, context)
266 self.apply_patches_skip_action.setToolTip(
267 N_('Skip applying the current patch and continue applying patches')
269 self.apply_patches_skip_action.setIcon(icons.discard())
271 self.export_patches_action = add_action(
272 self,
273 N_('Export Patches...'),
274 partial(guicmds.export_patches, context),
275 hotkeys.EXPORT,
277 self.export_patches_action.setIcon(icons.save())
279 self.new_repository_action = add_action(
280 self, N_('New Repository...'), partial(guicmds.open_new_repo, context)
282 self.new_repository_action.setIcon(icons.new())
284 self.new_bare_repository_action = add_action(
285 self, N_('New Bare Repository...'), partial(guicmds.new_bare_repo, context)
287 self.new_bare_repository_action.setIcon(icons.new())
289 prefs_func = partial(
290 prefs_widget.preferences, context, parent=self, model=prefs_model
292 self.preferences_action = add_action(
293 self, N_('Preferences'), prefs_func, QtGui.QKeySequence.Preferences
295 self.preferences_action.setIcon(icons.configure())
297 self.edit_remotes_action = add_action(
298 self, N_('Edit Remotes...'), partial(editremotes.editor, context)
300 self.edit_remotes_action.setIcon(icons.edit())
302 self.rescan_action = add_action(
303 self,
304 cmds.Refresh.name(),
305 cmds.run(cmds.Refresh, context),
306 *hotkeys.REFRESH_HOTKEYS
308 self.rescan_action.setIcon(icons.sync())
310 self.find_files_action = add_action(
311 self,
312 N_('Find Files'),
313 partial(finder.finder, context),
314 hotkeys.FINDER,
316 self.find_files_action.setIcon(icons.search())
318 self.browse_recently_modified_action = add_action(
319 self,
320 N_('Recently Modified Files...'),
321 partial(recent.browse_recent_files, context),
322 hotkeys.EDIT_SECONDARY,
324 self.browse_recently_modified_action.setIcon(icons.directory())
326 self.cherry_pick_action = add_action(
327 self,
328 N_('Cherry-Pick...'),
329 partial(guicmds.cherry_pick, context),
330 hotkeys.CHERRY_PICK,
332 self.cherry_pick_action.setIcon(icons.cherry_pick())
333 self.cherry_pick_abort_action = add_action(
334 self, N_('Abort Cherry-Pick...'), cmds.run(cmds.AbortCherryPick, context)
336 self.cherry_pick_abort_action.setIcon(icons.style_dialog_discard())
338 self.load_commitmsg_action = add_action(
339 self, N_('Load Commit Message...'), partial(guicmds.load_commitmsg, context)
341 self.load_commitmsg_action.setIcon(icons.file_text())
343 self.prepare_commitmsg_hook_action = add_action(
344 self,
345 N_('Prepare Commit Message'),
346 cmds.run(cmds.PrepareCommitMessageHook, context),
347 hotkeys.PREPARE_COMMIT_MESSAGE,
350 self.save_tarball_action = add_action(
351 self, N_('Save As Tarball/Zip...'), partial(archive.save_archive, context)
353 self.save_tarball_action.setIcon(icons.file_zip())
355 self.quit_action = add_action(self, N_('Quit'), self.close, hotkeys.QUIT)
357 self.grep_action = add_action(
358 self, N_('Grep'), partial(grep.grep, context), hotkeys.GREP
360 self.grep_action.setIcon(icons.search())
362 self.merge_local_action = add_action(
363 self, N_('Merge...'), partial(merge.local_merge, context), hotkeys.MERGE
365 self.merge_local_action.setIcon(icons.merge())
367 self.merge_abort_action = add_action(
368 self, N_('Abort Merge...'), cmds.run(cmds.AbortMerge, context)
370 self.merge_abort_action.setIcon(icons.style_dialog_discard())
372 self.update_submodules_action = add_action(
373 self,
374 N_('Update All Submodules...'),
375 cmds.run(cmds.SubmodulesUpdate, context),
377 self.update_submodules_action.setIcon(icons.sync())
379 self.add_submodule_action = add_action(
380 self,
381 N_('Add Submodule...'),
382 partial(submodules.add_submodule, context, parent=self),
384 self.add_submodule_action.setIcon(icons.add())
386 self.fetch_action = add_action(
387 self, N_('Fetch...'), partial(remote.fetch, context), hotkeys.FETCH
389 self.fetch_action.setIcon(icons.download())
391 self.push_action = add_action(
392 self, N_('Push...'), partial(remote.push, context), hotkeys.PUSH
394 self.push_action.setIcon(icons.push())
396 self.pull_action = add_action(
397 self, N_('Pull...'), partial(remote.pull, context), hotkeys.PULL
399 self.pull_action.setIcon(icons.pull())
401 self.open_repo_action = add_action(
402 self, N_('Open...'), partial(guicmds.open_repo, context), hotkeys.OPEN
404 self.open_repo_action.setIcon(icons.folder())
406 self.open_repo_new_action = add_action(
407 self,
408 N_('Open in New Window...'),
409 partial(guicmds.open_repo_in_new_window, context),
411 self.open_repo_new_action.setIcon(icons.folder())
413 self.stash_action = add_action(
414 self, N_('Stash...'), partial(stash.view, context), hotkeys.STASH
416 self.stash_action.setIcon(icons.commit())
418 self.reset_soft_action = add_action(
419 self, N_('Reset Branch (Soft)'), partial(guicmds.reset_soft, context)
421 self.reset_soft_action.setIcon(icons.style_dialog_reset())
422 self.reset_soft_action.setToolTip(cmds.ResetSoft.tooltip('<commit>'))
424 self.reset_mixed_action = add_action(
425 self,
426 N_('Reset Branch and Stage (Mixed)'),
427 partial(guicmds.reset_mixed, context),
429 self.reset_mixed_action.setIcon(icons.style_dialog_reset())
430 self.reset_mixed_action.setToolTip(cmds.ResetMixed.tooltip('<commit>'))
432 self.reset_keep_action = add_action(
433 self,
434 N_('Restore Worktree and Reset All (Keep Unstaged Changes)'),
435 partial(guicmds.reset_keep, context),
437 self.reset_keep_action.setIcon(icons.style_dialog_reset())
438 self.reset_keep_action.setToolTip(cmds.ResetKeep.tooltip('<commit>'))
440 self.reset_merge_action = add_action(
441 self,
442 N_('Restore Worktree and Reset All (Merge)'),
443 partial(guicmds.reset_merge, context),
445 self.reset_merge_action.setIcon(icons.style_dialog_reset())
446 self.reset_merge_action.setToolTip(cmds.ResetMerge.tooltip('<commit>'))
448 self.reset_hard_action = add_action(
449 self,
450 N_('Restore Worktree and Reset All (Hard)'),
451 partial(guicmds.reset_hard, context),
453 self.reset_hard_action.setIcon(icons.style_dialog_reset())
454 self.reset_hard_action.setToolTip(cmds.ResetHard.tooltip('<commit>'))
456 self.restore_worktree_action = add_action(
457 self, N_('Restore Worktree'), partial(guicmds.restore_worktree, context)
459 self.restore_worktree_action.setIcon(icons.edit())
460 self.restore_worktree_action.setToolTip(
461 cmds.RestoreWorktree.tooltip('<commit>')
464 self.clone_repo_action = add_action(
465 self, N_('Clone...'), partial(clone.clone, context)
467 self.clone_repo_action.setIcon(icons.repo())
469 self.help_docs_action = add_action(
470 self,
471 N_('Documentation'),
472 resources.show_html_docs,
473 QtGui.QKeySequence.HelpContents,
476 self.help_shortcuts_action = add_action(
477 self, N_('Keyboard Shortcuts'), about.show_shortcuts, hotkeys.QUESTION
480 self.visualize_current_action = add_action(
481 self,
482 N_('Visualize Current Branch...'),
483 cmds.run(cmds.VisualizeCurrent, context),
485 self.visualize_current_action.setIcon(icons.visualize())
487 self.visualize_all_action = add_action(
488 self, N_('Visualize All Branches...'), cmds.run(cmds.VisualizeAll, context)
490 self.visualize_all_action.setIcon(icons.visualize())
492 self.search_commits_action = add_action(
493 self, N_('Search...'), partial(search.search, context)
495 self.search_commits_action.setIcon(icons.search())
497 self.browse_branch_action = add_action(
498 self,
499 N_('Browse Current Branch...'),
500 partial(guicmds.browse_current, context),
502 self.browse_branch_action.setIcon(icons.directory())
504 self.browse_other_branch_action = add_action(
505 self, N_('Browse Other Branch...'), partial(guicmds.browse_other, context)
507 self.browse_other_branch_action.setIcon(icons.directory())
509 self.load_commitmsg_template_action = add_action(
510 self,
511 N_('Get Commit Message Template'),
512 cmds.run(cmds.LoadCommitMessageFromTemplate, context),
514 self.load_commitmsg_template_action.setIcon(icons.style_dialog_apply())
516 self.help_about_action = add_action(
517 self, N_('About'), partial(about.about_dialog, context)
520 self.diff_against_commit_action = add_action(
521 self,
522 N_('Against Commit... (Diff Mode)'),
523 partial(guicmds.diff_against_commit, context),
525 self.diff_against_commit_action.setIcon(icons.compare())
527 self.exit_diff_mode_action = add_action(
528 self, N_('Exit Diff Mode'), cmds.run(cmds.ResetMode, context)
530 self.exit_diff_mode_action.setIcon(icons.compare())
532 self.diff_expression_action = add_action(
533 self, N_('Expression...'), partial(guicmds.diff_expression, context)
535 self.diff_expression_action.setIcon(icons.compare())
537 self.branch_compare_action = add_action(
538 self, N_('Branches...'), partial(compare.compare_branches, context)
540 self.branch_compare_action.setIcon(icons.compare())
542 self.create_tag_action = add_action(
543 self,
544 N_('Create Tag...'),
545 partial(createtag.create_tag, context),
547 self.create_tag_action.setIcon(icons.tag())
549 self.create_branch_action = add_action(
550 self,
551 N_('Create...'),
552 partial(createbranch.create_new_branch, context),
553 hotkeys.BRANCH,
555 self.create_branch_action.setIcon(icons.branch())
557 self.delete_branch_action = add_action(
558 self, N_('Delete...'), partial(guicmds.delete_branch, context)
560 self.delete_branch_action.setIcon(icons.discard())
562 self.delete_remote_branch_action = add_action(
563 self,
564 N_('Delete Remote Branch...'),
565 partial(guicmds.delete_remote_branch, context),
567 self.delete_remote_branch_action.setIcon(icons.discard())
569 self.rename_branch_action = add_action(
570 self, N_('Rename Branch...'), partial(guicmds.rename_branch, context)
572 self.rename_branch_action.setIcon(icons.edit())
574 self.checkout_branch_action = add_action(
575 self,
576 N_('Checkout...'),
577 partial(guicmds.checkout_branch, context),
578 hotkeys.CHECKOUT,
580 self.checkout_branch_action.setIcon(icons.branch())
582 self.branch_review_action = add_action(
583 self, N_('Review...'), partial(guicmds.review_branch, context)
585 self.branch_review_action.setIcon(icons.compare())
587 self.browse_action = add_action(
588 self, N_('File Browser...'), partial(browse.worktree_browser, context)
590 self.browse_action.setIcon(icons.cola())
592 self.dag_action = add_action(self, N_('DAG...'), self.git_dag)
593 self.dag_action.setIcon(icons.cola())
595 self.rebase_start_action = add_action(
596 self,
597 N_('Start Interactive Rebase...'),
598 cmds.run(cmds.Rebase, context),
599 hotkeys.REBASE_START_AND_CONTINUE,
601 self.rebase_start_action.setIcon(icons.play())
603 self.rebase_edit_todo_action = add_action(
604 self, N_('Edit...'), cmds.run(cmds.RebaseEditTodo, context)
606 self.rebase_edit_todo_action.setIcon(icons.edit())
608 self.rebase_continue_action = add_action(
609 self,
610 N_('Continue'),
611 cmds.run(cmds.RebaseContinue, context),
612 hotkeys.REBASE_START_AND_CONTINUE,
614 self.rebase_continue_action.setIcon(icons.play())
616 self.rebase_skip_action = add_action(
617 self, N_('Skip Current Patch'), cmds.run(cmds.RebaseSkip, context)
619 self.rebase_skip_action.setIcon(icons.delete())
621 self.rebase_abort_action = add_action(
622 self, N_('Abort'), cmds.run(cmds.RebaseAbort, context)
624 self.rebase_abort_action.setIcon(icons.close())
626 # For "Start Rebase" only, reverse the first argument to setEnabled()
627 # so that we can operate on it as a group.
628 # We can do this because can_rebase == not is_rebasing
629 self.rebase_start_action_proxy = utils.Proxy(
630 self.rebase_start_action,
631 setEnabled=lambda x: self.rebase_start_action.setEnabled(not x),
634 self.rebase_group = utils.Group(
635 self.rebase_start_action_proxy,
636 self.rebase_edit_todo_action,
637 self.rebase_continue_action,
638 self.rebase_skip_action,
639 self.rebase_abort_action,
642 self.annex_init_action = qtutils.add_action(
643 self, N_('Initialize Git Annex'), cmds.run(cmds.AnnexInit, context)
646 self.lfs_init_action = qtutils.add_action(
647 self, N_('Initialize Git LFS'), cmds.run(cmds.LFSInstall, context)
650 self.lock_layout_action = add_action_bool(
651 self, N_('Lock Layout'), self.set_lock_layout, False
654 self.reset_layout_action = add_action(
655 self, N_('Reset Layout'), self.reset_layout
658 self.quick_repository_search = add_action(
659 self,
660 N_('Quick Open...'),
661 lambda: guicmds.open_quick_repo_search(self.context, parent=self),
662 hotkeys.OPEN_REPO_SEARCH,
664 self.quick_repository_search.setIcon(icons.search())
666 self.terminal_action = common.terminal_action(
667 context, self, hotkey=hotkeys.TERMINAL
670 # Create the application menu
671 self.menubar = QtWidgets.QMenuBar(self)
672 self.setMenuBar(self.menubar)
674 # File Menu
675 add_menu = qtutils.add_menu
676 self.file_menu = add_menu(N_('&File'), self.menubar)
677 self.file_menu.addAction(self.quick_repository_search)
678 # File->Open Recent menu
679 self.open_recent_menu = self.file_menu.addMenu(N_('Open Recent'))
680 self.open_recent_menu.setIcon(icons.folder())
681 self.file_menu.addAction(self.open_repo_action)
682 self.file_menu.addAction(self.open_repo_new_action)
683 self.file_menu.addSeparator()
684 self.file_menu.addAction(self.new_repository_action)
685 self.file_menu.addAction(self.new_bare_repository_action)
686 self.file_menu.addAction(self.clone_repo_action)
687 self.file_menu.addSeparator()
688 self.file_menu.addAction(self.rescan_action)
689 self.file_menu.addAction(self.find_files_action)
690 self.file_menu.addAction(self.edit_remotes_action)
691 self.file_menu.addAction(self.browse_recently_modified_action)
692 self.file_menu.addSeparator()
693 self.file_menu.addAction(self.save_tarball_action)
695 self.patches_menu = self.file_menu.addMenu(N_('Patches'))
696 self.patches_menu.setIcon(icons.diff())
697 self.patches_menu.addAction(self.export_patches_action)
698 self.patches_menu.addAction(self.apply_patches_action)
699 self.patches_menu.addAction(self.apply_patches_continue_action)
700 self.patches_menu.addAction(self.apply_patches_skip_action)
701 self.patches_menu.addAction(self.apply_patches_abort_action)
703 # Git Annex / Git LFS
704 annex = core.find_executable('git-annex')
705 lfs = core.find_executable('git-lfs')
706 if annex or lfs:
707 self.file_menu.addSeparator()
708 if annex:
709 self.file_menu.addAction(self.annex_init_action)
710 if lfs:
711 self.file_menu.addAction(self.lfs_init_action)
713 self.file_menu.addSeparator()
714 self.file_menu.addAction(self.preferences_action)
715 self.file_menu.addAction(self.quit_action)
717 # Edit Menu
718 self.edit_proxy = edit_proxy = FocusProxy(
719 editor, editor.summary, editor.description
722 copy_widgets = (
723 self,
724 editor.summary,
725 editor.description,
726 self.diffeditor,
727 bookmarkswidget.tree,
728 recentwidget.tree,
730 select_widgets = copy_widgets + (self.statuswidget.tree,)
731 edit_proxy.override('copy', copy_widgets)
732 edit_proxy.override('selectAll', select_widgets)
734 edit_menu = self.edit_menu = add_menu(N_('&Edit'), self.menubar)
735 undo = add_action(edit_menu, N_('Undo'), edit_proxy.undo, hotkeys.UNDO)
736 undo.setIcon(icons.undo())
737 redo = add_action(edit_menu, N_('Redo'), edit_proxy.redo, hotkeys.REDO)
738 redo.setIcon(icons.redo())
739 edit_menu.addSeparator()
740 cut = add_action(edit_menu, N_('Cut'), edit_proxy.cut, hotkeys.CUT)
741 cut.setIcon(icons.cut())
742 copy = add_action(edit_menu, N_('Copy'), edit_proxy.copy, hotkeys.COPY)
743 copy.setIcon(icons.copy())
744 paste = add_action(edit_menu, N_('Paste'), edit_proxy.paste, hotkeys.PASTE)
745 paste.setIcon(icons.paste())
746 delete = add_action(edit_menu, N_('Delete'), edit_proxy.delete, hotkeys.DELETE)
747 delete.setIcon(icons.delete())
748 edit_menu.addSeparator()
749 select_all = add_action(
750 edit_menu, N_('Select All'), edit_proxy.selectAll, hotkeys.SELECT_ALL
752 select_all.setIcon(icons.select_all())
753 edit_menu.addSeparator()
754 qtutils.add_menu_actions(edit_menu, self.commiteditor.menu_actions)
756 # Actions menu
757 self.actions_menu = add_menu(N_('Actions'), self.menubar)
758 if self.terminal_action is not None:
759 self.actions_menu.addAction(self.terminal_action)
760 self.actions_menu.addAction(self.fetch_action)
761 self.actions_menu.addAction(self.push_action)
762 self.actions_menu.addAction(self.pull_action)
763 self.actions_menu.addAction(self.stash_action)
764 self.actions_menu.addSeparator()
765 self.actions_menu.addAction(self.create_tag_action)
766 self.actions_menu.addAction(self.cherry_pick_action)
767 self.actions_menu.addAction(self.cherry_pick_abort_action)
768 self.actions_menu.addAction(self.merge_local_action)
769 self.actions_menu.addAction(self.merge_abort_action)
770 self.actions_menu.addSeparator()
771 self.actions_menu.addAction(self.update_submodules_action)
772 self.actions_menu.addAction(self.add_submodule_action)
773 self.actions_menu.addSeparator()
774 self.actions_menu.addAction(self.grep_action)
775 self.actions_menu.addAction(self.search_commits_action)
777 # Commit Menu
778 self.commit_menu = add_menu(N_('Commit@@verb'), self.menubar)
779 self.commit_menu.setTitle(N_('Commit@@verb'))
780 self.commit_menu.addAction(self.commiteditor.commit_action)
781 self.commit_menu.addAction(self.commit_amend_action)
782 self.commit_menu.addAction(self.undo_commit_action)
783 self.commit_menu.addSeparator()
784 self.commit_menu.addAction(self.statuswidget.tree.process_selection_action)
785 self.commit_menu.addAction(self.statuswidget.tree.stage_or_unstage_all_action)
786 self.commit_menu.addAction(self.stage_modified_action)
787 self.commit_menu.addAction(self.stage_untracked_action)
788 self.commit_menu.addSeparator()
789 self.commit_menu.addAction(self.unstage_all_action)
790 self.commit_menu.addAction(self.unstage_selected_action)
791 self.commit_menu.addSeparator()
792 self.commit_menu.addAction(self.load_commitmsg_action)
793 self.commit_menu.addAction(self.load_commitmsg_template_action)
794 self.commit_menu.addAction(self.prepare_commitmsg_hook_action)
796 # Diff Menu
797 self.diff_menu = add_menu(N_('Diff'), self.menubar)
798 self.diff_menu.addAction(self.diff_expression_action)
799 self.diff_menu.addAction(self.branch_compare_action)
800 self.diff_menu.addAction(self.show_diffstat_action)
801 self.diff_menu.addSeparator()
802 self.diff_menu.addAction(self.diff_against_commit_action)
803 self.diff_menu.addAction(self.exit_diff_mode_action)
805 # Branch Menu
806 self.branch_menu = add_menu(N_('Branch'), self.menubar)
807 self.branch_menu.addAction(self.branch_review_action)
808 self.branch_menu.addSeparator()
809 self.branch_menu.addAction(self.create_branch_action)
810 self.branch_menu.addAction(self.checkout_branch_action)
811 self.branch_menu.addAction(self.delete_branch_action)
812 self.branch_menu.addAction(self.delete_remote_branch_action)
813 self.branch_menu.addAction(self.rename_branch_action)
814 self.branch_menu.addSeparator()
815 self.branch_menu.addAction(self.browse_branch_action)
816 self.branch_menu.addAction(self.browse_other_branch_action)
817 self.branch_menu.addSeparator()
818 self.branch_menu.addAction(self.visualize_current_action)
819 self.branch_menu.addAction(self.visualize_all_action)
821 # Rebase menu
822 self.rebase_menu = add_menu(N_('Rebase'), self.menubar)
823 self.rebase_menu.addAction(self.rebase_start_action)
824 self.rebase_menu.addAction(self.rebase_edit_todo_action)
825 self.rebase_menu.addSeparator()
826 self.rebase_menu.addAction(self.rebase_continue_action)
827 self.rebase_menu.addAction(self.rebase_skip_action)
828 self.rebase_menu.addSeparator()
829 self.rebase_menu.addAction(self.rebase_abort_action)
831 # Reset menu
832 self.reset_menu = add_menu(N_('Reset'), self.menubar)
833 self.reset_menu.addAction(self.unstage_all_action)
834 self.reset_menu.addAction(self.undo_commit_action)
835 self.reset_menu.addSeparator()
836 self.reset_menu.addAction(self.reset_soft_action)
837 self.reset_menu.addAction(self.reset_mixed_action)
838 self.reset_menu.addAction(self.restore_worktree_action)
839 self.reset_menu.addSeparator()
840 self.reset_menu.addAction(self.reset_keep_action)
841 self.reset_menu.addAction(self.reset_merge_action)
842 self.reset_menu.addAction(self.reset_hard_action)
844 # View Menu
845 self.view_menu = add_menu(N_('View'), self.menubar)
846 # pylint: disable=no-member
847 self.view_menu.aboutToShow.connect(lambda: self.build_view_menu(self.view_menu))
848 self.setup_dockwidget_view_menu()
849 if utils.is_darwin():
850 # The native macOS menu doesn't show empty entries.
851 self.build_view_menu(self.view_menu)
853 # Help Menu
854 self.help_menu = add_menu(N_('Help'), self.menubar)
855 self.help_menu.addAction(self.help_docs_action)
856 self.help_menu.addAction(self.help_shortcuts_action)
857 self.help_menu.addAction(self.help_about_action)
859 # Arrange dock widgets
860 bottom = Qt.BottomDockWidgetArea
861 top = Qt.TopDockWidgetArea
863 self.addDockWidget(top, self.statusdock)
864 self.addDockWidget(top, self.commitdock)
865 if self.browser_dockable:
866 self.addDockWidget(top, self.browserdock)
867 self.tabifyDockWidget(self.browserdock, self.commitdock)
869 self.addDockWidget(top, self.branchdock)
870 self.addDockWidget(top, self.submodulesdock)
871 self.addDockWidget(top, self.bookmarksdock)
872 self.addDockWidget(top, self.recentdock)
874 self.tabifyDockWidget(self.branchdock, self.submodulesdock)
875 self.tabifyDockWidget(self.submodulesdock, self.bookmarksdock)
876 self.tabifyDockWidget(self.bookmarksdock, self.recentdock)
877 self.branchdock.raise_()
879 self.addDockWidget(bottom, self.diffdock)
880 self.addDockWidget(bottom, self.actionsdock)
881 self.addDockWidget(bottom, self.logdock)
882 self.tabifyDockWidget(self.actionsdock, self.logdock)
884 # Listen for model notifications
885 self.model.updated.connect(self.refresh, type=Qt.QueuedConnection)
886 self.model.mode_changed.connect(
887 lambda mode: self.refresh(), type=Qt.QueuedConnection
890 prefs_model.config_updated.connect(self._config_updated)
892 # Set a default value
893 self.show_cursor_position(1, 0)
895 self.commit_menu.aboutToShow.connect(self.update_menu_actions)
896 self.open_recent_menu.aboutToShow.connect(self.build_recent_menu)
897 self.commiteditor.cursor_changed.connect(self.show_cursor_position)
899 self.diffeditor.options_changed.connect(self.statuswidget.refresh)
900 self.diffeditor.up.connect(self.statuswidget.move_up)
901 self.diffeditor.down.connect(self.statuswidget.move_down)
903 self.commiteditor.up.connect(self.statuswidget.move_up)
904 self.commiteditor.down.connect(self.statuswidget.move_down)
906 self.config_actions_changed.connect(
907 lambda names_and_shortcuts: _install_config_actions(
908 context,
909 self.actions_menu,
910 names_and_shortcuts,
912 type=Qt.QueuedConnection,
914 self.init_state(context.settings, self.set_initial_size)
916 # Route command output here
917 Interaction.log_status = self.logwidget.log_status
918 Interaction.log = self.logwidget.log
919 # Focus the status widget; this must be deferred
920 QtCore.QTimer.singleShot(0, self.initialize)
922 def initialize(self):
923 context = self.context
924 git_version = version.git_version_str(context)
925 if git_version:
926 ok = True
927 Interaction.log(
928 git_version + '\n' + N_('git cola version %s') % version.version()
930 else:
931 ok = False
932 error_msg = N_('error: unable to execute git')
933 Interaction.log(error_msg)
935 if ok:
936 self.statuswidget.setFocus()
937 else:
938 title = N_('error: unable to execute git')
939 msg = title
940 details = ''
941 if WIN32:
942 details = git.win32_git_error_hint()
943 Interaction.critical(title, message=msg, details=details)
944 self.context.app.exit(2)
946 def set_initial_size(self):
947 # Default size; this is thrown out when save/restore is used
948 width, height = qtutils.desktop_size()
949 self.resize((width * 3) // 4, height)
950 self.statuswidget.set_initial_size()
951 self.commiteditor.set_initial_size()
953 def set_filter(self, txt):
954 self.statuswidget.set_filter(txt)
956 # Qt overrides
957 def closeEvent(self, event):
958 """Save state in the settings"""
959 commit_msg = self.commiteditor.commit_message(raw=True)
960 self.model.save_commitmsg(msg=commit_msg)
961 for browser in list(self.context.browser_windows):
962 browser.close()
963 standard.MainWindow.closeEvent(self, event)
965 def create_view_menu(self):
966 menu = qtutils.create_menu(N_('View'), self)
967 self.build_view_menu(menu)
968 return menu
970 def build_view_menu(self, menu):
971 menu.clear()
972 menu.addAction(self.browse_action)
973 menu.addAction(self.dag_action)
974 menu.addSeparator()
976 popup_menu = self.createPopupMenu()
977 for menu_action in popup_menu.actions():
978 menu_action.setParent(menu)
979 menu.addAction(menu_action)
981 context = self.context
982 menu_action = menu.addAction(
983 N_('New Toolbar'), partial(toolbar.add_toolbar, context, self)
985 menu_action.setIcon(icons.add())
986 menu.addSeparator()
988 dockwidgets = [
989 self.logdock,
990 self.commitdock,
991 self.statusdock,
992 self.diffdock,
993 self.actionsdock,
994 self.bookmarksdock,
995 self.recentdock,
996 self.branchdock,
997 self.submodulesdock,
999 if self.browser_dockable:
1000 dockwidgets.append(self.browserdock)
1002 for dockwidget in dockwidgets:
1003 # Associate the action with the shortcut
1004 toggleview = dockwidget.toggleViewAction()
1005 menu.addAction(toggleview)
1007 menu.addSeparator()
1008 menu.addAction(self.lock_layout_action)
1009 menu.addAction(self.reset_layout_action)
1011 return menu
1013 def contextMenuEvent(self, event):
1014 menu = self.create_view_menu()
1015 menu.exec_(event.globalPos())
1017 def build_recent_menu(self):
1018 cmd = cmds.OpenRepo
1019 context = self.context
1020 settings = context.settings
1021 settings.load()
1022 menu = self.open_recent_menu
1023 menu.clear()
1024 worktree = context.git.worktree()
1026 for entry in settings.recent:
1027 directory = entry['path']
1028 if directory == worktree:
1029 # Omit the current worktree from the "Open Recent" menu.
1030 continue
1031 name = entry['name']
1032 text = '%s %s %s' % (name, uchr(0x2192), directory)
1033 menu.addAction(text, cmds.run(cmd, context, directory))
1035 # Accessors
1036 mode = property(lambda self: self.model.mode)
1038 def _config_updated(self, _source, config, value):
1039 if config == prefs.FONTDIFF:
1040 # The diff font
1041 font = QtGui.QFont()
1042 if not font.fromString(value):
1043 return
1044 self.logwidget.setFont(font)
1045 self.diffeditor.setFont(font)
1046 self.commiteditor.setFont(font)
1048 elif config == prefs.TABWIDTH:
1049 # This can be set locally or globally, so we have to use the
1050 # effective value otherwise we'll update when we shouldn't.
1051 # For example, if this value is overridden locally, and the
1052 # global value is tweaked, we should not update.
1053 value = prefs.tabwidth(self.context)
1054 self.diffeditor.set_tabwidth(value)
1055 self.commiteditor.set_tabwidth(value)
1057 elif config == prefs.EXPANDTAB:
1058 self.commiteditor.set_expandtab(value)
1060 elif config == prefs.LINEBREAK:
1061 # enables automatic line breaks
1062 self.commiteditor.set_linebreak(value)
1064 elif config == prefs.SORT_BOOKMARKS:
1065 self.bookmarksdock.widget().reload_bookmarks()
1067 elif config == prefs.TEXTWIDTH:
1068 # Use the effective value for the same reason as tabwidth.
1069 value = prefs.textwidth(self.context)
1070 self.commiteditor.set_textwidth(value)
1072 elif config == prefs.SHOW_PATH:
1073 # the path in the window title was toggled
1074 self.refresh_window_title()
1076 def start(self, context):
1077 """Do the expensive "get_config_actions()" call in the background"""
1078 # Install .git-config-defined actions
1079 task = qtutils.SimpleTask(self.get_config_actions)
1080 context.runtask.start(task)
1082 def get_config_actions(self):
1083 actions = cfgactions.get_config_actions(self.context)
1084 self.config_actions_changed.emit(actions)
1086 def refresh(self):
1087 """Update the title with the current branch and directory name."""
1088 curbranch = self.model.currentbranch
1089 is_merging = self.model.is_merging
1090 is_rebasing = self.model.is_rebasing
1091 is_applying_patch = self.model.is_applying_patch
1092 is_cherry_picking = self.model.is_rebasing
1094 curdir = core.getcwd()
1095 msg = N_('Repository: %s') % curdir
1096 msg += '\n'
1097 msg += N_('Branch: %s') % curbranch
1099 if is_rebasing:
1100 msg += '\n\n'
1101 msg += N_(
1102 'This repository is currently being rebased.\n'
1103 'Resolve conflicts, commit changes, and run:\n'
1104 ' Rebase > Continue'
1106 elif is_applying_patch:
1107 msg += '\n\n'
1108 msg += N_(
1109 'This repository has unresolved conflicts after applying a patch.\n'
1110 'Resolve conflicts and commit changes.'
1112 elif is_cherry_picking:
1113 msg += '\n\n'
1114 msg += N_(
1115 'This repository is in the middle of a cherry-pick.\n'
1116 'Resolve conflicts and commit changes.'
1118 elif is_merging:
1119 msg += '\n\n'
1120 msg += N_(
1121 'This repository is in the middle of a merge.\n'
1122 'Resolve conflicts and commit changes.'
1125 self.refresh_window_title()
1127 if self.mode == self.model.mode_amend:
1128 self.commit_amend_action.setChecked(True)
1129 else:
1130 self.commit_amend_action.setChecked(False)
1132 self.commitdock.setToolTip(msg)
1134 self.actionswidget.set_mode(self.mode)
1135 self.commiteditor.set_mode(self.mode)
1136 self.statuswidget.set_mode(self.mode)
1138 self.update_actions()
1140 def refresh_window_title(self):
1141 """Refresh the window title when state changes"""
1142 alerts = []
1144 project = self.model.project
1145 curbranch = self.model.currentbranch
1146 is_cherry_picking = self.model.is_cherry_picking
1147 is_merging = self.model.is_merging
1148 is_rebasing = self.model.is_rebasing
1149 is_applying_patch = self.model.is_applying_patch
1150 is_diff_mode = self.model.is_diff_mode()
1151 is_amend_mode = self.mode == self.model.mode_amend
1153 prefix = uchr(0xAB)
1154 suffix = uchr(0xBB)
1156 if is_amend_mode:
1157 alerts.append(N_('Amending'))
1158 elif is_diff_mode:
1159 alerts.append(N_('Diff Mode'))
1160 elif is_cherry_picking:
1161 alerts.append(N_('Cherry-picking'))
1162 elif is_merging:
1163 alerts.append(N_('Merging'))
1164 elif is_rebasing:
1165 alerts.append(N_('Rebasing'))
1166 elif is_applying_patch:
1167 alerts.append(N_('Applying Patch'))
1169 if alerts:
1170 alert_text = (prefix + ' %s ' + suffix + ' ') % ', '.join(alerts)
1171 else:
1172 alert_text = ''
1174 if self.model.cfg.get(prefs.SHOW_PATH, True):
1175 path_text = self.git.worktree()
1176 else:
1177 path_text = ''
1179 title = '%s: %s %s%s' % (project, curbranch, alert_text, path_text)
1180 self.setWindowTitle(title)
1182 def update_actions(self):
1183 is_rebasing = self.model.is_rebasing
1184 self.rebase_group.setEnabled(is_rebasing)
1186 enabled = not self.model.is_empty_repository()
1187 self.rename_branch_action.setEnabled(enabled)
1188 self.delete_branch_action.setEnabled(enabled)
1190 self.annex_init_action.setEnabled(not self.model.annex)
1191 self.lfs_init_action.setEnabled(not self.model.lfs)
1192 self.merge_abort_action.setEnabled(self.model.is_merging)
1193 self.cherry_pick_abort_action.setEnabled(self.model.is_cherry_picking)
1194 self.apply_patches_continue_action.setEnabled(self.model.is_applying_patch)
1195 self.apply_patches_skip_action.setEnabled(self.model.is_applying_patch)
1196 self.apply_patches_abort_action.setEnabled(self.model.is_applying_patch)
1198 diff_mode = self.model.mode == self.model.mode_diff
1199 self.exit_diff_mode_action.setEnabled(diff_mode)
1201 def update_menu_actions(self):
1202 # Enable the Prepare Commit Message action if the hook exists
1203 hook = gitcmds.prepare_commit_message_hook(self.context)
1204 enabled = os.path.exists(hook)
1205 self.prepare_commitmsg_hook_action.setEnabled(enabled)
1207 def export_state(self):
1208 state = standard.MainWindow.export_state(self)
1209 show_status_filter = self.statuswidget.filter_widget.isVisible()
1210 state['show_status_filter'] = show_status_filter
1211 state['toolbars'] = self.toolbar_state.export_state()
1212 state['ref_sort'] = self.model.ref_sort
1213 self.diffviewer.export_state(state)
1215 return state
1217 def apply_state(self, state):
1218 """Imports data for save/restore"""
1219 base_ok = standard.MainWindow.apply_state(self, state)
1220 lock_layout = state.get('lock_layout', False)
1221 self.lock_layout_action.setChecked(lock_layout)
1223 show_status_filter = state.get('show_status_filter', False)
1224 self.statuswidget.filter_widget.setVisible(show_status_filter)
1226 toolbars = state.get('toolbars', [])
1227 self.toolbar_state.apply_state(toolbars)
1229 sort_key = state.get('ref_sort', 0)
1230 self.model.set_ref_sort(sort_key)
1232 diff_ok = self.diffviewer.apply_state(state)
1233 return base_ok and diff_ok
1235 def setup_dockwidget_view_menu(self):
1236 # Hotkeys for toggling the dock widgets
1237 if utils.is_darwin():
1238 optkey = 'Meta'
1239 else:
1240 optkey = 'Ctrl'
1241 dockwidgets = (
1242 (optkey + '+0', self.logdock),
1243 (optkey + '+1', self.commitdock),
1244 (optkey + '+2', self.statusdock),
1245 (optkey + '+3', self.diffdock),
1246 (optkey + '+4', self.actionsdock),
1247 (optkey + '+5', self.bookmarksdock),
1248 (optkey + '+6', self.recentdock),
1249 (optkey + '+7', self.branchdock),
1250 (optkey + '+8', self.submodulesdock),
1252 for shortcut, dockwidget in dockwidgets:
1253 # Associate the action with the shortcut
1254 toggleview = dockwidget.toggleViewAction()
1255 toggleview.setShortcut('Shift+' + shortcut)
1257 def showdock(show, dockwidget=dockwidget):
1258 if show:
1259 dockwidget.raise_()
1260 dockwidget.widget().setFocus()
1261 else:
1262 self.setFocus()
1264 self.addAction(toggleview)
1265 qtutils.connect_action_bool(toggleview, showdock)
1267 # Create a new shortcut Shift+<shortcut> that gives focus
1268 toggleview = QtWidgets.QAction(self)
1269 toggleview.setShortcut(shortcut)
1271 def focusdock(dockwidget=dockwidget):
1272 focus_dock(dockwidget)
1274 self.addAction(toggleview)
1275 qtutils.connect_action(toggleview, focusdock)
1277 # These widgets warrant home-row hotkey status
1278 qtutils.add_action(
1279 self,
1280 'Focus Commit Message',
1281 lambda: focus_dock(self.commitdock),
1282 hotkeys.FOCUS,
1285 qtutils.add_action(
1286 self,
1287 'Focus Status Window',
1288 lambda: focus_dock(self.statusdock),
1289 hotkeys.FOCUS_STATUS,
1292 qtutils.add_action(
1293 self,
1294 'Focus Diff Editor',
1295 lambda: focus_dock(self.diffdock),
1296 hotkeys.FOCUS_DIFF,
1299 def git_dag(self):
1300 self.dag = dag.git_dag(self.context, existing_view=self.dag)
1302 def show_cursor_position(self, rows, cols):
1303 display_content = '%02d:%02d' % (rows, cols)
1304 css = """
1305 <style>
1306 .good {
1308 .first-warning {
1309 color: black;
1310 background-color: yellow;
1312 .second-warning {
1313 color: black;
1314 background-color: #f83;
1316 .error {
1317 color: white;
1318 background-color: red;
1320 </style>
1323 if cols > 78:
1324 cls = 'error'
1325 elif cols > 72:
1326 cls = 'second-warning'
1327 elif cols > 64:
1328 cls = 'first-warning'
1329 else:
1330 cls = 'good'
1331 div = '<div class="%s">%s</div>' % (cls, display_content)
1332 self.position_label.setText(css + div)
1335 class FocusProxy(object):
1336 """Proxy over child widgets and operate on the focused widget"""
1338 def __init__(self, *widgets):
1339 self.widgets = widgets
1340 self.overrides = {}
1342 def override(self, name, widgets):
1343 self.overrides[name] = widgets
1345 def focus(self, name):
1346 """Return the currently focused widget"""
1347 widgets = self.overrides.get(name, self.widgets)
1348 # The parent must be the parent of all the proxied widgets
1349 parent = widgets[0]
1350 # The first widget is used as a fallback
1351 fallback = widgets[1]
1352 # We ignore the parent when delegating to child widgets
1353 widgets = widgets[1:]
1355 focus = parent.focusWidget()
1356 if focus not in widgets:
1357 focus = fallback
1358 return focus
1360 def __getattr__(self, name):
1361 """Return a callback that calls a common child method"""
1363 def callback():
1364 focus = self.focus(name)
1365 func = getattr(focus, name, None)
1366 if func:
1367 func()
1369 return callback
1371 def delete(self):
1372 """Specialized delete() to deal with QLineEdit vs QTextEdit"""
1373 focus = self.focus('delete')
1374 if hasattr(focus, 'del_'):
1375 focus.del_()
1376 elif hasattr(focus, 'textCursor'):
1377 focus.textCursor().deleteChar()
1380 def show_dock(dockwidget):
1381 dockwidget.raise_()
1382 dockwidget.widget().setFocus()
1385 def focus_dock(dockwidget):
1386 if get(dockwidget.toggleViewAction()):
1387 show_dock(dockwidget)
1388 else:
1389 dockwidget.toggleViewAction().trigger()
1392 def _install_config_actions(context, menu, names_and_shortcuts):
1393 """Install .gitconfig-defined actions"""
1394 if not names_and_shortcuts:
1395 return
1396 menu.addSeparator()
1397 cache = {}
1398 for name, shortcut in names_and_shortcuts:
1399 sub_menu, action_name = build_menus(name, menu, cache)
1400 callback = cmds.run(cmds.RunConfigAction, context, name)
1401 menu_action = sub_menu.addAction(action_name, callback)
1402 if shortcut:
1403 menu_action.setShortcut(shortcut)
1406 def build_menus(name, menu, cache):
1407 """Create a chain of QMenu entries parented under a root QMenu
1409 A name of "a/b/c" create a menu chain of menu -> QMenu("a") -> QMenu("b")
1410 and returns a tuple of (QMenu("b"), "c").
1412 :param name: The full entry path, ex: "a/b/c" where "a/b" is the menu chain.
1413 :param menu: The root menu under which to create the menu chain.
1414 :param cache: A dict cache of previously created menus to avoid duplicates.
1417 # NOTE: utils.split() and friends are used instead of os.path.split() because
1418 # slash '/' is the only supported "<menu>/name" separator. Use of os.path.split()
1419 # would introduce differences in behavior across platforms.
1421 # If the menu_path is empty then no parent menus need to be created.
1422 # The action will be added to the root menu.
1423 menu_path, text = utils.split(utils.normalize_slash(name))
1425 if not menu_path:
1426 return (menu, text)
1427 # When menu_path contains ex: "a/b" we will create two menus: "a" and "b".
1428 # The root menu is the parent of "a" and "a" is the parent of "b".
1429 # The menu returned to the caller is "b".
1431 # Loop over the individual menu basenames alongside the full subpath returned by
1432 # pathset(). The subpath is a cache key for finding previously created menus.
1433 menu_names = utils.splitpath(menu_path) # ['a', 'b']
1434 menu_pathset = utils.pathset(menu_path) # ['a', 'a/b']
1435 for menu_name, menu_id in zip(menu_names, menu_pathset):
1436 try:
1437 menu = cache[menu_id]
1438 except KeyError:
1439 menu = cache[menu_id] = menu.addMenu(menu_name)
1441 return (menu, text)