tree-wide: trivial code style tweaks
[git-cola.git] / cola / widgets / main.py
blobbd0c94a869ac6320270d6646ad4fc5d8f2cb5a5c
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 fn=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 fn=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 fn=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', N_('Branches'), self, fn=partial(branch.BranchesWidget, context)
132 self.branchwidget = self.branchdock.widget()
133 titlebar = self.branchdock.titleBarWidget()
134 titlebar.add_corner_widget(self.branchwidget.filter_button)
135 titlebar.add_corner_widget(self.branchwidget.sort_order_button)
137 # "Submodule" widgets
138 self.submodulesdock = create_dock(
139 'Submodules',
140 N_('Submodules'),
141 self,
142 fn=partial(submodules.SubmodulesWidget, context),
144 self.submoduleswidget = self.submodulesdock.widget()
146 # "Commit Message Editor" widget
147 self.position_label = QtWidgets.QLabel()
148 self.position_label.setAlignment(Qt.AlignCenter)
149 font = qtutils.default_monospace_font()
150 font.setPointSize(int(font.pointSize() * 0.8))
151 self.position_label.setFont(font)
153 # make the position label fixed size to avoid layout issues
154 fm = self.position_label.fontMetrics()
155 width = fm.width('99:999') + defs.spacing
156 self.position_label.setMinimumWidth(width)
158 editor = commitmsg.CommitMessageEditor(context, self)
159 self.commiteditor = editor
160 self.commitdock = create_dock('Commit', N_('Commit'), self, widget=editor)
161 titlebar = self.commitdock.titleBarWidget()
162 titlebar.add_corner_widget(self.position_label)
164 # "Console" widget
165 self.logwidget = log.LogWidget(context)
166 self.logdock = create_dock(
167 'Console', N_('Console'), self, widget=self.logwidget
169 qtutils.hide_dock(self.logdock)
171 # "Diff Viewer" widget
172 self.diffdock = create_dock(
173 'Diff', N_('Diff'), self, fn=lambda dock: diff.Viewer(context, parent=dock)
175 self.diffviewer = self.diffdock.widget()
176 self.diffviewer.set_diff_type(self.model.diff_type)
178 self.diffeditor = self.diffviewer.text
179 titlebar = self.diffdock.titleBarWidget()
180 titlebar.add_corner_widget(self.diffviewer.options)
182 # All Actions
183 add_action = qtutils.add_action
184 add_action_bool = qtutils.add_action_bool
186 self.commit_amend_action = add_action_bool(
187 self,
188 N_('Amend Last Commit'),
189 partial(cmds.do, cmds.AmendMode, context),
190 False,
192 self.commit_amend_action.setIcon(icons.edit())
193 self.commit_amend_action.setShortcut(hotkeys.AMEND)
194 self.commit_amend_action.setShortcutContext(Qt.WidgetShortcut)
196 self.unstage_all_action = add_action(
197 self, N_('Unstage All'), cmds.run(cmds.UnstageAll, context)
199 self.unstage_all_action.setIcon(icons.remove())
201 self.undo_commit_action = add_action(
202 self, N_('Undo Last Commit'), cmds.run(cmds.UndoLastCommit, context)
204 self.undo_commit_action.setIcon(icons.style_dialog_discard())
206 self.unstage_selected_action = add_action(
207 self, N_('Unstage From Commit'), cmds.run(cmds.UnstageSelected, context)
209 self.unstage_selected_action.setIcon(icons.remove())
211 self.show_diffstat_action = add_action(
212 self, N_('Diffstat'), self.statuswidget.select_header, hotkeys.DIFFSTAT
214 self.show_diffstat_action.setIcon(icons.diff())
216 self.stage_modified_action = add_action(
217 self,
218 N_('Stage Changed Files To Commit'),
219 cmds.run(cmds.StageModified, context),
220 hotkeys.STAGE_MODIFIED,
222 self.stage_modified_action.setIcon(icons.add())
224 self.stage_untracked_action = add_action(
225 self,
226 N_('Stage All Untracked'),
227 cmds.run(cmds.StageUntracked, context),
228 hotkeys.STAGE_UNTRACKED,
230 self.stage_untracked_action.setIcon(icons.add())
232 self.apply_patches_action = add_action(
233 self, N_('Apply Patches...'), partial(patch.apply_patches, context)
235 self.apply_patches_action.setIcon(icons.diff())
237 self.apply_patches_abort_action = add_action(
238 self,
239 N_('Abort Applying Patches...'),
240 cmds.run(cmds.AbortApplyPatch, context),
242 self.apply_patches_abort_action.setIcon(icons.style_dialog_discard())
243 self.apply_patches_abort_action.setToolTip(
244 N_('Abort the current "git am" patch session')
247 self.apply_patches_continue_action = add_action(
248 self,
249 N_('Continue Applying Patches'),
250 cmds.run(cmds.ApplyPatchesContinue, context),
252 self.apply_patches_continue_action.setToolTip(
253 N_('Commit the current state and continue applying patches')
255 self.apply_patches_continue_action.setIcon(icons.commit())
257 self.apply_patches_skip_action = add_action(
258 self, N_('Skip Current Patch'), cmds.run(cmds.ApplyPatchesContinue, context)
260 self.apply_patches_skip_action.setToolTip(
261 N_('Skip applying the current patch and continue applying patches')
263 self.apply_patches_skip_action.setIcon(icons.discard())
265 self.export_patches_action = add_action(
266 self,
267 N_('Export Patches...'),
268 partial(guicmds.export_patches, context),
269 hotkeys.EXPORT,
271 self.export_patches_action.setIcon(icons.save())
273 self.new_repository_action = add_action(
274 self, N_('New Repository...'), partial(guicmds.open_new_repo, context)
276 self.new_repository_action.setIcon(icons.new())
278 self.new_bare_repository_action = add_action(
279 self, N_('New Bare Repository...'), partial(guicmds.new_bare_repo, context)
281 self.new_bare_repository_action.setIcon(icons.new())
283 prefs_fn = partial(
284 prefs_widget.preferences, context, parent=self, model=prefs_model
286 self.preferences_action = add_action(
287 self, N_('Preferences'), prefs_fn, QtGui.QKeySequence.Preferences
289 self.preferences_action.setIcon(icons.configure())
291 self.edit_remotes_action = add_action(
292 self, N_('Edit Remotes...'), partial(editremotes.editor, context)
294 self.edit_remotes_action.setIcon(icons.edit())
296 self.rescan_action = add_action(
297 self,
298 cmds.Refresh.name(),
299 cmds.run(cmds.Refresh, context),
300 *hotkeys.REFRESH_HOTKEYS
302 self.rescan_action.setIcon(icons.sync())
304 self.find_files_action = add_action(
305 self,
306 N_('Find Files'),
307 partial(finder.finder, context),
308 hotkeys.FINDER,
310 self.find_files_action.setIcon(icons.search())
312 self.browse_recently_modified_action = add_action(
313 self,
314 N_('Recently Modified Files...'),
315 partial(recent.browse_recent_files, context),
316 hotkeys.EDIT_SECONDARY,
318 self.browse_recently_modified_action.setIcon(icons.directory())
320 self.cherry_pick_action = add_action(
321 self,
322 N_('Cherry-Pick...'),
323 partial(guicmds.cherry_pick, context),
324 hotkeys.CHERRY_PICK,
326 self.cherry_pick_action.setIcon(icons.cherry_pick())
327 self.cherry_pick_abort_action = add_action(
328 self, N_('Abort Cherry-Pick...'), cmds.run(cmds.AbortCherryPick, context)
330 self.cherry_pick_abort_action.setIcon(icons.style_dialog_discard())
332 self.load_commitmsg_action = add_action(
333 self, N_('Load Commit Message...'), partial(guicmds.load_commitmsg, context)
335 self.load_commitmsg_action.setIcon(icons.file_text())
337 self.prepare_commitmsg_hook_action = add_action(
338 self,
339 N_('Prepare Commit Message'),
340 cmds.run(cmds.PrepareCommitMessageHook, context),
341 hotkeys.PREPARE_COMMIT_MESSAGE,
344 self.save_tarball_action = add_action(
345 self, N_('Save As Tarball/Zip...'), partial(archive.save_archive, context)
347 self.save_tarball_action.setIcon(icons.file_zip())
349 self.quit_action = add_action(self, N_('Quit'), self.close, hotkeys.QUIT)
351 self.grep_action = add_action(
352 self, N_('Grep'), partial(grep.grep, context), hotkeys.GREP
354 self.grep_action.setIcon(icons.search())
356 self.merge_local_action = add_action(
357 self, N_('Merge...'), partial(merge.local_merge, context), hotkeys.MERGE
359 self.merge_local_action.setIcon(icons.merge())
361 self.merge_abort_action = add_action(
362 self, N_('Abort Merge...'), cmds.run(cmds.AbortMerge, context)
364 self.merge_abort_action.setIcon(icons.style_dialog_discard())
366 self.update_submodules_action = add_action(
367 self,
368 N_('Update All Submodules...'),
369 cmds.run(cmds.SubmodulesUpdate, context),
371 self.update_submodules_action.setIcon(icons.sync())
373 self.add_submodule_action = add_action(
374 self,
375 N_('Add Submodule...'),
376 partial(submodules.add_submodule, context, parent=self),
378 self.add_submodule_action.setIcon(icons.add())
380 self.fetch_action = add_action(
381 self, N_('Fetch...'), partial(remote.fetch, context), hotkeys.FETCH
383 self.fetch_action.setIcon(icons.download())
385 self.push_action = add_action(
386 self, N_('Push...'), partial(remote.push, context), hotkeys.PUSH
388 self.push_action.setIcon(icons.push())
390 self.pull_action = add_action(
391 self, N_('Pull...'), partial(remote.pull, context), hotkeys.PULL
393 self.pull_action.setIcon(icons.pull())
395 self.open_repo_action = add_action(
396 self, N_('Open...'), partial(guicmds.open_repo, context), hotkeys.OPEN
398 self.open_repo_action.setIcon(icons.folder())
400 self.open_repo_new_action = add_action(
401 self,
402 N_('Open in New Window...'),
403 partial(guicmds.open_repo_in_new_window, context),
405 self.open_repo_new_action.setIcon(icons.folder())
407 self.stash_action = add_action(
408 self, N_('Stash...'), partial(stash.view, context), hotkeys.STASH
410 self.stash_action.setIcon(icons.commit())
412 self.reset_soft_action = add_action(
413 self, N_('Reset Branch (Soft)'), partial(guicmds.reset_soft, context)
415 self.reset_soft_action.setIcon(icons.style_dialog_reset())
416 self.reset_soft_action.setToolTip(cmds.ResetSoft.tooltip('<commit>'))
418 self.reset_mixed_action = add_action(
419 self,
420 N_('Reset Branch and Stage (Mixed)'),
421 partial(guicmds.reset_mixed, context),
423 self.reset_mixed_action.setIcon(icons.style_dialog_reset())
424 self.reset_mixed_action.setToolTip(cmds.ResetMixed.tooltip('<commit>'))
426 self.reset_keep_action = add_action(
427 self,
428 N_('Restore Worktree and Reset All (Keep Unstaged Changes)'),
429 partial(guicmds.reset_keep, context),
431 self.reset_keep_action.setIcon(icons.style_dialog_reset())
432 self.reset_keep_action.setToolTip(cmds.ResetKeep.tooltip('<commit>'))
434 self.reset_merge_action = add_action(
435 self,
436 N_('Restore Worktree and Reset All (Merge)'),
437 partial(guicmds.reset_merge, context),
439 self.reset_merge_action.setIcon(icons.style_dialog_reset())
440 self.reset_merge_action.setToolTip(cmds.ResetMerge.tooltip('<commit>'))
442 self.reset_hard_action = add_action(
443 self,
444 N_('Restore Worktree and Reset All (Hard)'),
445 partial(guicmds.reset_hard, context),
447 self.reset_hard_action.setIcon(icons.style_dialog_reset())
448 self.reset_hard_action.setToolTip(cmds.ResetHard.tooltip('<commit>'))
450 self.restore_worktree_action = add_action(
451 self, N_('Restore Worktree'), partial(guicmds.restore_worktree, context)
453 self.restore_worktree_action.setIcon(icons.edit())
454 self.restore_worktree_action.setToolTip(
455 cmds.RestoreWorktree.tooltip('<commit>')
458 self.clone_repo_action = add_action(
459 self, N_('Clone...'), partial(clone.clone, context)
461 self.clone_repo_action.setIcon(icons.repo())
463 self.help_docs_action = add_action(
464 self,
465 N_('Documentation'),
466 resources.show_html_docs,
467 QtGui.QKeySequence.HelpContents,
470 self.help_shortcuts_action = add_action(
471 self, N_('Keyboard Shortcuts'), about.show_shortcuts, hotkeys.QUESTION
474 self.visualize_current_action = add_action(
475 self,
476 N_('Visualize Current Branch...'),
477 cmds.run(cmds.VisualizeCurrent, context),
479 self.visualize_current_action.setIcon(icons.visualize())
481 self.visualize_all_action = add_action(
482 self, N_('Visualize All Branches...'), cmds.run(cmds.VisualizeAll, context)
484 self.visualize_all_action.setIcon(icons.visualize())
486 self.search_commits_action = add_action(
487 self, N_('Search...'), partial(search.search, context)
489 self.search_commits_action.setIcon(icons.search())
491 self.browse_branch_action = add_action(
492 self,
493 N_('Browse Current Branch...'),
494 partial(guicmds.browse_current, context),
496 self.browse_branch_action.setIcon(icons.directory())
498 self.browse_other_branch_action = add_action(
499 self, N_('Browse Other Branch...'), partial(guicmds.browse_other, context)
501 self.browse_other_branch_action.setIcon(icons.directory())
503 self.load_commitmsg_template_action = add_action(
504 self,
505 N_('Get Commit Message Template'),
506 cmds.run(cmds.LoadCommitMessageFromTemplate, context),
508 self.load_commitmsg_template_action.setIcon(icons.style_dialog_apply())
510 self.help_about_action = add_action(
511 self, N_('About'), partial(about.about_dialog, context)
514 self.diff_against_commit_action = add_action(
515 self,
516 N_('Against Commit... (Diff Mode)'),
517 partial(guicmds.diff_against_commit, context),
519 self.diff_against_commit_action.setIcon(icons.compare())
521 self.exit_diff_mode_action = add_action(
522 self, N_('Exit Diff Mode'), cmds.run(cmds.ResetMode, context)
524 self.exit_diff_mode_action.setIcon(icons.compare())
526 self.diff_expression_action = add_action(
527 self, N_('Expression...'), partial(guicmds.diff_expression, context)
529 self.diff_expression_action.setIcon(icons.compare())
531 self.branch_compare_action = add_action(
532 self, N_('Branches...'), partial(compare.compare_branches, context)
534 self.branch_compare_action.setIcon(icons.compare())
536 self.create_tag_action = add_action(
537 self,
538 N_('Create Tag...'),
539 partial(createtag.create_tag, context),
541 self.create_tag_action.setIcon(icons.tag())
543 self.create_branch_action = add_action(
544 self,
545 N_('Create...'),
546 partial(createbranch.create_new_branch, context),
547 hotkeys.BRANCH,
549 self.create_branch_action.setIcon(icons.branch())
551 self.delete_branch_action = add_action(
552 self, N_('Delete...'), partial(guicmds.delete_branch, context)
554 self.delete_branch_action.setIcon(icons.discard())
556 self.delete_remote_branch_action = add_action(
557 self,
558 N_('Delete Remote Branch...'),
559 partial(guicmds.delete_remote_branch, context),
561 self.delete_remote_branch_action.setIcon(icons.discard())
563 self.rename_branch_action = add_action(
564 self, N_('Rename Branch...'), partial(guicmds.rename_branch, context)
566 self.rename_branch_action.setIcon(icons.edit())
568 self.checkout_branch_action = add_action(
569 self,
570 N_('Checkout...'),
571 partial(guicmds.checkout_branch, context),
572 hotkeys.CHECKOUT,
574 self.checkout_branch_action.setIcon(icons.branch())
576 self.branch_review_action = add_action(
577 self, N_('Review...'), partial(guicmds.review_branch, context)
579 self.branch_review_action.setIcon(icons.compare())
581 self.browse_action = add_action(
582 self, N_('File Browser...'), partial(browse.worktree_browser, context)
584 self.browse_action.setIcon(icons.cola())
586 self.dag_action = add_action(self, N_('DAG...'), self.git_dag)
587 self.dag_action.setIcon(icons.cola())
589 self.rebase_start_action = add_action(
590 self,
591 N_('Start Interactive Rebase...'),
592 cmds.run(cmds.Rebase, context),
593 hotkeys.REBASE_START_AND_CONTINUE,
596 self.rebase_edit_todo_action = add_action(
597 self, N_('Edit...'), cmds.run(cmds.RebaseEditTodo, context)
600 self.rebase_continue_action = add_action(
601 self,
602 N_('Continue'),
603 cmds.run(cmds.RebaseContinue, context),
604 hotkeys.REBASE_START_AND_CONTINUE,
607 self.rebase_skip_action = add_action(
608 self, N_('Skip Current Patch'), cmds.run(cmds.RebaseSkip, context)
611 self.rebase_abort_action = add_action(
612 self, N_('Abort'), cmds.run(cmds.RebaseAbort, context)
615 # For "Start Rebase" only, reverse the first argument to setEnabled()
616 # so that we can operate on it as a group.
617 # We can do this because can_rebase == not is_rebasing
618 self.rebase_start_action_proxy = utils.Proxy(
619 self.rebase_start_action,
620 setEnabled=lambda x: self.rebase_start_action.setEnabled(not x),
623 self.rebase_group = utils.Group(
624 self.rebase_start_action_proxy,
625 self.rebase_edit_todo_action,
626 self.rebase_continue_action,
627 self.rebase_skip_action,
628 self.rebase_abort_action,
631 self.annex_init_action = qtutils.add_action(
632 self, N_('Initialize Git Annex'), cmds.run(cmds.AnnexInit, context)
635 self.lfs_init_action = qtutils.add_action(
636 self, N_('Initialize Git LFS'), cmds.run(cmds.LFSInstall, context)
639 self.lock_layout_action = add_action_bool(
640 self, N_('Lock Layout'), self.set_lock_layout, False
643 self.reset_layout_action = add_action(
644 self, N_('Reset Layout'), self.reset_layout
647 self.quick_repository_search = add_action(
648 self,
649 N_('Quick Open...'),
650 lambda: guicmds.open_quick_repo_search(self.context, parent=self),
651 hotkeys.OPEN_REPO_SEARCH,
653 self.quick_repository_search.setIcon(icons.search())
655 self.terminal_action = common.terminal_action(
656 context, self, hotkey=hotkeys.TERMINAL
659 # Create the application menu
660 self.menubar = QtWidgets.QMenuBar(self)
661 self.setMenuBar(self.menubar)
663 # File Menu
664 add_menu = qtutils.add_menu
665 self.file_menu = add_menu(N_('&File'), self.menubar)
666 self.file_menu.addAction(self.quick_repository_search)
667 # File->Open Recent menu
668 self.open_recent_menu = self.file_menu.addMenu(N_('Open Recent'))
669 self.open_recent_menu.setIcon(icons.folder())
670 self.file_menu.addAction(self.open_repo_action)
671 self.file_menu.addAction(self.open_repo_new_action)
672 self.file_menu.addSeparator()
673 self.file_menu.addAction(self.new_repository_action)
674 self.file_menu.addAction(self.new_bare_repository_action)
675 self.file_menu.addAction(self.clone_repo_action)
676 self.file_menu.addSeparator()
677 self.file_menu.addAction(self.rescan_action)
678 self.file_menu.addAction(self.find_files_action)
679 self.file_menu.addAction(self.edit_remotes_action)
680 self.file_menu.addAction(self.browse_recently_modified_action)
681 self.file_menu.addSeparator()
682 self.file_menu.addAction(self.save_tarball_action)
684 self.patches_menu = self.file_menu.addMenu(N_('Patches'))
685 self.patches_menu.setIcon(icons.diff())
686 self.patches_menu.addAction(self.export_patches_action)
687 self.patches_menu.addAction(self.apply_patches_action)
688 self.patches_menu.addAction(self.apply_patches_continue_action)
689 self.patches_menu.addAction(self.apply_patches_skip_action)
690 self.patches_menu.addAction(self.apply_patches_abort_action)
692 # Git Annex / Git LFS
693 annex = core.find_executable('git-annex')
694 lfs = core.find_executable('git-lfs')
695 if annex or lfs:
696 self.file_menu.addSeparator()
697 if annex:
698 self.file_menu.addAction(self.annex_init_action)
699 if lfs:
700 self.file_menu.addAction(self.lfs_init_action)
702 self.file_menu.addSeparator()
703 self.file_menu.addAction(self.preferences_action)
704 self.file_menu.addAction(self.quit_action)
706 # Edit Menu
707 self.edit_proxy = edit_proxy = FocusProxy(
708 editor, editor.summary, editor.description
711 copy_widgets = (
712 self,
713 editor.summary,
714 editor.description,
715 self.diffeditor,
716 bookmarkswidget.tree,
717 recentwidget.tree,
719 select_widgets = copy_widgets + (self.statuswidget.tree,)
720 edit_proxy.override('copy', copy_widgets)
721 edit_proxy.override('selectAll', select_widgets)
723 edit_menu = self.edit_menu = add_menu(N_('&Edit'), self.menubar)
724 undo = add_action(edit_menu, N_('Undo'), edit_proxy.undo, hotkeys.UNDO)
725 undo.setIcon(icons.undo())
726 redo = add_action(edit_menu, N_('Redo'), edit_proxy.redo, hotkeys.REDO)
727 redo.setIcon(icons.redo())
728 edit_menu.addSeparator()
729 cut = add_action(edit_menu, N_('Cut'), edit_proxy.cut, hotkeys.CUT)
730 cut.setIcon(icons.cut())
731 copy = add_action(edit_menu, N_('Copy'), edit_proxy.copy, hotkeys.COPY)
732 copy.setIcon(icons.copy())
733 paste = add_action(edit_menu, N_('Paste'), edit_proxy.paste, hotkeys.PASTE)
734 paste.setIcon(icons.paste())
735 delete = add_action(edit_menu, N_('Delete'), edit_proxy.delete, hotkeys.DELETE)
736 delete.setIcon(icons.delete())
737 edit_menu.addSeparator()
738 select_all = add_action(
739 edit_menu, N_('Select All'), edit_proxy.selectAll, hotkeys.SELECT_ALL
741 select_all.setIcon(icons.select_all())
742 edit_menu.addSeparator()
743 commitmsg.add_menu_actions(edit_menu, self.commiteditor.menu_actions)
745 # Actions menu
746 self.actions_menu = add_menu(N_('Actions'), self.menubar)
747 if self.terminal_action is not None:
748 self.actions_menu.addAction(self.terminal_action)
749 self.actions_menu.addAction(self.fetch_action)
750 self.actions_menu.addAction(self.push_action)
751 self.actions_menu.addAction(self.pull_action)
752 self.actions_menu.addAction(self.stash_action)
753 self.actions_menu.addSeparator()
754 self.actions_menu.addAction(self.create_tag_action)
755 self.actions_menu.addAction(self.cherry_pick_action)
756 self.actions_menu.addAction(self.cherry_pick_abort_action)
757 self.actions_menu.addAction(self.merge_local_action)
758 self.actions_menu.addAction(self.merge_abort_action)
759 self.actions_menu.addSeparator()
760 self.actions_menu.addAction(self.update_submodules_action)
761 self.actions_menu.addAction(self.add_submodule_action)
762 self.actions_menu.addSeparator()
763 self.actions_menu.addAction(self.grep_action)
764 self.actions_menu.addAction(self.search_commits_action)
766 # Commit Menu
767 self.commit_menu = add_menu(N_('Commit@@verb'), self.menubar)
768 self.commit_menu.setTitle(N_('Commit@@verb'))
769 self.commit_menu.addAction(self.commiteditor.commit_action)
770 self.commit_menu.addAction(self.commit_amend_action)
771 self.commit_menu.addAction(self.undo_commit_action)
772 self.commit_menu.addSeparator()
773 self.commit_menu.addAction(self.statuswidget.tree.process_selection_action)
774 self.commit_menu.addAction(self.statuswidget.tree.stage_or_unstage_all_action)
775 self.commit_menu.addAction(self.stage_modified_action)
776 self.commit_menu.addAction(self.stage_untracked_action)
777 self.commit_menu.addSeparator()
778 self.commit_menu.addAction(self.unstage_all_action)
779 self.commit_menu.addAction(self.unstage_selected_action)
780 self.commit_menu.addSeparator()
781 self.commit_menu.addAction(self.load_commitmsg_action)
782 self.commit_menu.addAction(self.load_commitmsg_template_action)
783 self.commit_menu.addAction(self.prepare_commitmsg_hook_action)
785 # Diff Menu
786 self.diff_menu = add_menu(N_('Diff'), self.menubar)
787 self.diff_menu.addAction(self.diff_expression_action)
788 self.diff_menu.addAction(self.branch_compare_action)
789 self.diff_menu.addAction(self.show_diffstat_action)
790 self.diff_menu.addSeparator()
791 self.diff_menu.addAction(self.diff_against_commit_action)
792 self.diff_menu.addAction(self.exit_diff_mode_action)
794 # Branch Menu
795 self.branch_menu = add_menu(N_('Branch'), self.menubar)
796 self.branch_menu.addAction(self.branch_review_action)
797 self.branch_menu.addSeparator()
798 self.branch_menu.addAction(self.create_branch_action)
799 self.branch_menu.addAction(self.checkout_branch_action)
800 self.branch_menu.addAction(self.delete_branch_action)
801 self.branch_menu.addAction(self.delete_remote_branch_action)
802 self.branch_menu.addAction(self.rename_branch_action)
803 self.branch_menu.addSeparator()
804 self.branch_menu.addAction(self.browse_branch_action)
805 self.branch_menu.addAction(self.browse_other_branch_action)
806 self.branch_menu.addSeparator()
807 self.branch_menu.addAction(self.visualize_current_action)
808 self.branch_menu.addAction(self.visualize_all_action)
810 # Rebase menu
811 self.rebase_menu = add_menu(N_('Rebase'), self.menubar)
812 self.rebase_menu.addAction(self.rebase_start_action)
813 self.rebase_menu.addAction(self.rebase_edit_todo_action)
814 self.rebase_menu.addSeparator()
815 self.rebase_menu.addAction(self.rebase_continue_action)
816 self.rebase_menu.addAction(self.rebase_skip_action)
817 self.rebase_menu.addSeparator()
818 self.rebase_menu.addAction(self.rebase_abort_action)
820 # Reset menu
821 self.reset_menu = add_menu(N_('Reset'), self.menubar)
822 self.reset_menu.addAction(self.unstage_all_action)
823 self.reset_menu.addAction(self.undo_commit_action)
824 self.reset_menu.addSeparator()
825 self.reset_menu.addAction(self.reset_soft_action)
826 self.reset_menu.addAction(self.reset_mixed_action)
827 self.reset_menu.addAction(self.restore_worktree_action)
828 self.reset_menu.addSeparator()
829 self.reset_menu.addAction(self.reset_keep_action)
830 self.reset_menu.addAction(self.reset_merge_action)
831 self.reset_menu.addAction(self.reset_hard_action)
833 # View Menu
834 self.view_menu = add_menu(N_('View'), self.menubar)
835 # pylint: disable=no-member
836 self.view_menu.aboutToShow.connect(lambda: self.build_view_menu(self.view_menu))
837 self.setup_dockwidget_view_menu()
838 if utils.is_darwin():
839 # TODO or self.menubar.setNativeMenuBar(False)
840 # Since native OSX menu doesn't show empty entries
841 self.build_view_menu(self.view_menu)
843 # Help Menu
844 self.help_menu = add_menu(N_('Help'), self.menubar)
845 self.help_menu.addAction(self.help_docs_action)
846 self.help_menu.addAction(self.help_shortcuts_action)
847 self.help_menu.addAction(self.help_about_action)
849 # Arrange dock widgets
850 bottom = Qt.BottomDockWidgetArea
851 top = Qt.TopDockWidgetArea
853 self.addDockWidget(top, self.statusdock)
854 self.addDockWidget(top, self.commitdock)
855 if self.browser_dockable:
856 self.addDockWidget(top, self.browserdock)
857 self.tabifyDockWidget(self.browserdock, self.commitdock)
859 self.addDockWidget(top, self.branchdock)
860 self.addDockWidget(top, self.submodulesdock)
861 self.addDockWidget(top, self.bookmarksdock)
862 self.addDockWidget(top, self.recentdock)
864 self.tabifyDockWidget(self.branchdock, self.submodulesdock)
865 self.tabifyDockWidget(self.submodulesdock, self.bookmarksdock)
866 self.tabifyDockWidget(self.bookmarksdock, self.recentdock)
867 self.branchdock.raise_()
869 self.addDockWidget(bottom, self.diffdock)
870 self.addDockWidget(bottom, self.actionsdock)
871 self.addDockWidget(bottom, self.logdock)
872 self.tabifyDockWidget(self.actionsdock, self.logdock)
874 # Listen for model notifications
875 self.model.updated.connect(self.refresh, type=Qt.QueuedConnection)
876 self.model.mode_changed.connect(
877 lambda mode: self.refresh(), type=Qt.QueuedConnection
880 prefs_model.config_updated.connect(self._config_updated)
882 # Set a default value
883 self.show_cursor_position(1, 0)
885 self.commit_menu.aboutToShow.connect(self.update_menu_actions)
886 self.open_recent_menu.aboutToShow.connect(self.build_recent_menu)
887 self.commiteditor.cursor_changed.connect(self.show_cursor_position)
889 self.diffeditor.options_changed.connect(self.statuswidget.refresh)
890 self.diffeditor.up.connect(self.statuswidget.move_up)
891 self.diffeditor.down.connect(self.statuswidget.move_down)
893 self.commiteditor.up.connect(self.statuswidget.move_up)
894 self.commiteditor.down.connect(self.statuswidget.move_down)
896 self.config_actions_changed.connect(
897 lambda names_and_shortcuts: _install_config_actions(
898 context,
899 self.actions_menu,
900 names_and_shortcuts,
902 type=Qt.QueuedConnection,
904 self.init_state(context.settings, self.set_initial_size)
906 # Route command output here
907 Interaction.log_status = self.logwidget.log_status
908 Interaction.log = self.logwidget.log
909 # Focus the status widget; this must be deferred
910 QtCore.QTimer.singleShot(0, self.initialize)
912 def initialize(self):
913 context = self.context
914 git_version = version.git_version_str(context)
915 if git_version:
916 ok = True
917 Interaction.log(
918 git_version + '\n' + N_('git cola version %s') % version.version()
920 else:
921 ok = False
922 error_msg = N_('error: unable to execute git')
923 Interaction.log(error_msg)
925 if ok:
926 self.statuswidget.setFocus()
927 else:
928 title = N_('error: unable to execute git')
929 msg = title
930 details = ''
931 if WIN32:
932 details = git.win32_git_error_hint()
933 Interaction.critical(title, message=msg, details=details)
934 self.context.app.exit(2)
936 def set_initial_size(self):
937 # Default size; this is thrown out when save/restore is used
938 width, height = qtutils.desktop_size()
939 self.resize((width * 3) // 4, height)
940 self.statuswidget.set_initial_size()
941 self.commiteditor.set_initial_size()
943 def set_filter(self, txt):
944 self.statuswidget.set_filter(txt)
946 # Qt overrides
947 def closeEvent(self, event):
948 """Save state in the settings"""
949 commit_msg = self.commiteditor.commit_message(raw=True)
950 self.model.save_commitmsg(msg=commit_msg)
951 for browser in list(self.context.browser_windows):
952 browser.close()
953 standard.MainWindow.closeEvent(self, event)
955 def create_view_menu(self):
956 menu = qtutils.create_menu(N_('View'), self)
957 self.build_view_menu(menu)
958 return menu
960 def build_view_menu(self, menu):
961 menu.clear()
962 menu.addAction(self.browse_action)
963 menu.addAction(self.dag_action)
964 menu.addSeparator()
966 popup_menu = self.createPopupMenu()
967 for menu_action in popup_menu.actions():
968 menu_action.setParent(menu)
969 menu.addAction(menu_action)
971 context = self.context
972 menu_action = menu.addAction(
973 N_('New Toolbar'), partial(toolbar.add_toolbar, context, self)
975 menu_action.setIcon(icons.add())
976 menu.addSeparator()
978 dockwidgets = [
979 self.logdock,
980 self.commitdock,
981 self.statusdock,
982 self.diffdock,
983 self.actionsdock,
984 self.bookmarksdock,
985 self.recentdock,
986 self.branchdock,
987 self.submodulesdock,
989 if self.browser_dockable:
990 dockwidgets.append(self.browserdock)
992 for dockwidget in dockwidgets:
993 # Associate the action with the shortcut
994 toggleview = dockwidget.toggleViewAction()
995 menu.addAction(toggleview)
997 menu.addSeparator()
998 menu.addAction(self.lock_layout_action)
999 menu.addAction(self.reset_layout_action)
1001 return menu
1003 def contextMenuEvent(self, event):
1004 menu = self.create_view_menu()
1005 menu.exec_(event.globalPos())
1007 def build_recent_menu(self):
1008 cmd = cmds.OpenRepo
1009 context = self.context
1010 settings = context.settings
1011 settings.load()
1012 menu = self.open_recent_menu
1013 menu.clear()
1014 worktree = context.git.worktree()
1016 for entry in settings.recent:
1017 directory = entry['path']
1018 if directory == worktree:
1019 # Omit the current worktree from the "Open Recent" menu.
1020 continue
1021 name = entry['name']
1022 text = '%s %s %s' % (name, uchr(0x2192), directory)
1023 menu.addAction(text, cmds.run(cmd, context, directory))
1025 # Accessors
1026 mode = property(lambda self: self.model.mode)
1028 def _config_updated(self, _source, config, value):
1029 if config == prefs.FONTDIFF:
1030 # The diff font
1031 font = QtGui.QFont()
1032 if not font.fromString(value):
1033 return
1034 self.logwidget.setFont(font)
1035 self.diffeditor.setFont(font)
1036 self.commiteditor.setFont(font)
1038 elif config == prefs.TABWIDTH:
1039 # This can be set locally or globally, so we have to use the
1040 # effective value otherwise we'll update when we shouldn't.
1041 # For example, if this value is overridden locally, and the
1042 # global value is tweaked, we should not update.
1043 value = prefs.tabwidth(self.context)
1044 self.diffeditor.set_tabwidth(value)
1045 self.commiteditor.set_tabwidth(value)
1047 elif config == prefs.EXPANDTAB:
1048 self.commiteditor.set_expandtab(value)
1050 elif config == prefs.LINEBREAK:
1051 # enables automatic line breaks
1052 self.commiteditor.set_linebreak(value)
1054 elif config == prefs.SORT_BOOKMARKS:
1055 self.bookmarksdock.widget().reload_bookmarks()
1057 elif config == prefs.TEXTWIDTH:
1058 # Use the effective value for the same reason as tabwidth.
1059 value = prefs.textwidth(self.context)
1060 self.commiteditor.set_textwidth(value)
1062 elif config == prefs.SHOW_PATH:
1063 # the path in the window title was toggled
1064 self.refresh_window_title()
1066 def start(self, context):
1067 """Do the expensive "get_config_actions()" call in the background"""
1068 # Install .git-config-defined actions
1069 task = qtutils.SimpleTask(self.get_config_actions)
1070 context.runtask.start(task)
1072 def get_config_actions(self):
1073 actions = cfgactions.get_config_actions(self.context)
1074 self.config_actions_changed.emit(actions)
1076 def refresh(self):
1077 """Update the title with the current branch and directory name."""
1078 curbranch = self.model.currentbranch
1079 is_merging = self.model.is_merging
1080 is_rebasing = self.model.is_rebasing
1081 is_applying_patch = self.model.is_applying_patch
1082 is_cherry_picking = self.model.is_rebasing
1084 curdir = core.getcwd()
1085 msg = N_('Repository: %s') % curdir
1086 msg += '\n'
1087 msg += N_('Branch: %s') % curbranch
1089 if is_rebasing:
1090 msg += '\n\n'
1091 msg += N_(
1092 'This repository is currently being rebased.\n'
1093 'Resolve conflicts, commit changes, and run:\n'
1094 ' Rebase > Continue'
1096 elif is_applying_patch:
1097 msg += '\n\n'
1098 msg += N_(
1099 'This repository has unresolved conflicts after applying a patch.\n'
1100 'Resolve conflicts and commit changes.'
1102 elif is_cherry_picking:
1103 msg += '\n\n'
1104 msg += N_(
1105 'This repository is in the middle of a cherry-pick.\n'
1106 'Resolve conflicts and commit changes.'
1108 elif is_merging:
1109 msg += '\n\n'
1110 msg += N_(
1111 'This repository is in the middle of a merge.\n'
1112 'Resolve conflicts and commit changes.'
1115 self.refresh_window_title()
1117 if self.mode == self.model.mode_amend:
1118 self.commit_amend_action.setChecked(True)
1119 else:
1120 self.commit_amend_action.setChecked(False)
1122 self.commitdock.setToolTip(msg)
1124 self.actionswidget.set_mode(self.mode)
1125 self.commiteditor.set_mode(self.mode)
1126 self.statuswidget.set_mode(self.mode)
1128 self.update_actions()
1130 def refresh_window_title(self):
1131 """Refresh the window title when state changes"""
1132 alerts = []
1134 project = self.model.project
1135 curbranch = self.model.currentbranch
1136 is_cherry_picking = self.model.is_cherry_picking
1137 is_merging = self.model.is_merging
1138 is_rebasing = self.model.is_rebasing
1139 is_applying_patch = self.model.is_applying_patch
1140 is_diff_mode = self.mode == self.model.mode_diff
1141 is_amend_mode = self.mode == self.model.mode_amend
1143 prefix = uchr(0xAB)
1144 suffix = uchr(0xBB)
1146 if is_amend_mode:
1147 alerts.append(N_('Amending'))
1148 elif is_diff_mode:
1149 alerts.append(N_('Diff Mode'))
1150 elif is_cherry_picking:
1151 alerts.append(N_('Cherry-picking'))
1152 elif is_merging:
1153 alerts.append(N_('Merging'))
1154 elif is_rebasing:
1155 alerts.append(N_('Rebasing'))
1156 elif is_applying_patch:
1157 alerts.append(N_('Applying Patch'))
1159 if alerts:
1160 alert_text = (prefix + ' %s ' + suffix + ' ') % ', '.join(alerts)
1161 else:
1162 alert_text = ''
1164 if self.model.cfg.get(prefs.SHOW_PATH, True):
1165 path_text = self.git.worktree()
1166 else:
1167 path_text = ''
1169 title = '%s: %s %s%s' % (project, curbranch, alert_text, path_text)
1170 self.setWindowTitle(title)
1172 def update_actions(self):
1173 is_rebasing = self.model.is_rebasing
1174 self.rebase_group.setEnabled(is_rebasing)
1176 enabled = not self.model.is_empty_repository()
1177 self.rename_branch_action.setEnabled(enabled)
1178 self.delete_branch_action.setEnabled(enabled)
1180 self.annex_init_action.setEnabled(not self.model.annex)
1181 self.lfs_init_action.setEnabled(not self.model.lfs)
1182 self.merge_abort_action.setEnabled(self.model.is_merging)
1183 self.cherry_pick_abort_action.setEnabled(self.model.is_cherry_picking)
1184 self.apply_patches_continue_action.setEnabled(self.model.is_applying_patch)
1185 self.apply_patches_skip_action.setEnabled(self.model.is_applying_patch)
1186 self.apply_patches_abort_action.setEnabled(self.model.is_applying_patch)
1188 diff_mode = self.model.mode == self.model.mode_diff
1189 self.exit_diff_mode_action.setEnabled(diff_mode)
1191 def update_menu_actions(self):
1192 # Enable the Prepare Commit Message action if the hook exists
1193 hook = gitcmds.prepare_commit_message_hook(self.context)
1194 enabled = os.path.exists(hook)
1195 self.prepare_commitmsg_hook_action.setEnabled(enabled)
1197 def export_state(self):
1198 state = standard.MainWindow.export_state(self)
1199 show_status_filter = self.statuswidget.filter_widget.isVisible()
1200 state['show_status_filter'] = show_status_filter
1201 state['toolbars'] = self.toolbar_state.export_state()
1202 state['ref_sort'] = self.model.ref_sort
1203 self.diffviewer.export_state(state)
1205 return state
1207 def apply_state(self, state):
1208 """Imports data for save/restore"""
1209 base_ok = standard.MainWindow.apply_state(self, state)
1210 lock_layout = state.get('lock_layout', False)
1211 self.lock_layout_action.setChecked(lock_layout)
1213 show_status_filter = state.get('show_status_filter', False)
1214 self.statuswidget.filter_widget.setVisible(show_status_filter)
1216 toolbars = state.get('toolbars', [])
1217 self.toolbar_state.apply_state(toolbars)
1219 sort_key = state.get('ref_sort', 0)
1220 self.model.set_ref_sort(sort_key)
1222 diff_ok = self.diffviewer.apply_state(state)
1223 return base_ok and diff_ok
1225 def setup_dockwidget_view_menu(self):
1226 # Hotkeys for toggling the dock widgets
1227 if utils.is_darwin():
1228 optkey = 'Meta'
1229 else:
1230 optkey = 'Ctrl'
1231 dockwidgets = (
1232 (optkey + '+0', self.logdock),
1233 (optkey + '+1', self.commitdock),
1234 (optkey + '+2', self.statusdock),
1235 (optkey + '+3', self.diffdock),
1236 (optkey + '+4', self.actionsdock),
1237 (optkey + '+5', self.bookmarksdock),
1238 (optkey + '+6', self.recentdock),
1239 (optkey + '+7', self.branchdock),
1240 (optkey + '+8', self.submodulesdock),
1242 for shortcut, dockwidget in dockwidgets:
1243 # Associate the action with the shortcut
1244 toggleview = dockwidget.toggleViewAction()
1245 toggleview.setShortcut('Shift+' + shortcut)
1247 def showdock(show, dockwidget=dockwidget):
1248 if show:
1249 dockwidget.raise_()
1250 dockwidget.widget().setFocus()
1251 else:
1252 self.setFocus()
1254 self.addAction(toggleview)
1255 qtutils.connect_action_bool(toggleview, showdock)
1257 # Create a new shortcut Shift+<shortcut> that gives focus
1258 toggleview = QtWidgets.QAction(self)
1259 toggleview.setShortcut(shortcut)
1261 def focusdock(dockwidget=dockwidget):
1262 focus_dock(dockwidget)
1264 self.addAction(toggleview)
1265 qtutils.connect_action(toggleview, focusdock)
1267 # These widgets warrant home-row hotkey status
1268 qtutils.add_action(
1269 self,
1270 'Focus Commit Message',
1271 lambda: focus_dock(self.commitdock),
1272 hotkeys.FOCUS,
1275 qtutils.add_action(
1276 self,
1277 'Focus Status Window',
1278 lambda: focus_dock(self.statusdock),
1279 hotkeys.FOCUS_STATUS,
1282 qtutils.add_action(
1283 self,
1284 'Focus Diff Editor',
1285 lambda: focus_dock(self.diffdock),
1286 hotkeys.FOCUS_DIFF,
1289 def git_dag(self):
1290 self.dag = dag.git_dag(self.context, existing_view=self.dag)
1292 def show_cursor_position(self, rows, cols):
1293 display_content = '%02d:%02d' % (rows, cols)
1294 css = """
1295 <style>
1296 .good {
1298 .first-warning {
1299 color: black;
1300 background-color: yellow;
1302 .second-warning {
1303 color: black;
1304 background-color: #f83;
1306 .error {
1307 color: white;
1308 background-color: red;
1310 </style>
1313 if cols > 78:
1314 cls = 'error'
1315 elif cols > 72:
1316 cls = 'second-warning'
1317 elif cols > 64:
1318 cls = 'first-warning'
1319 else:
1320 cls = 'good'
1321 div = '<div class="%s">%s</div>' % (cls, display_content)
1322 self.position_label.setText(css + div)
1325 class FocusProxy(object):
1326 """Proxy over child widgets and operate on the focused widget"""
1328 def __init__(self, *widgets):
1329 self.widgets = widgets
1330 self.overrides = {}
1332 def override(self, name, widgets):
1333 self.overrides[name] = widgets
1335 def focus(self, name):
1336 """Return the currently focused widget"""
1337 widgets = self.overrides.get(name, self.widgets)
1338 # The parent must be the parent of all the proxied widgets
1339 parent = widgets[0]
1340 # The first widget is used as a fallback
1341 fallback = widgets[1]
1342 # We ignore the parent when delegating to child widgets
1343 widgets = widgets[1:]
1345 focus = parent.focusWidget()
1346 if focus not in widgets:
1347 focus = fallback
1348 return focus
1350 def __getattr__(self, name):
1351 """Return a callback that calls a common child method"""
1353 def callback():
1354 focus = self.focus(name)
1355 fn = getattr(focus, name, None)
1356 if fn:
1357 fn()
1359 return callback
1361 def delete(self):
1362 """Specialized delete() to deal with QLineEdit vs QTextEdit"""
1363 focus = self.focus('delete')
1364 if hasattr(focus, 'del_'):
1365 focus.del_()
1366 elif hasattr(focus, 'textCursor'):
1367 focus.textCursor().deleteChar()
1370 def show_dock(dockwidget):
1371 dockwidget.raise_()
1372 dockwidget.widget().setFocus()
1375 def focus_dock(dockwidget):
1376 if get(dockwidget.toggleViewAction()):
1377 show_dock(dockwidget)
1378 else:
1379 dockwidget.toggleViewAction().trigger()
1382 def _install_config_actions(context, menu, names_and_shortcuts):
1383 """Install .gitconfig-defined actions"""
1384 if not names_and_shortcuts:
1385 return
1386 menu.addSeparator()
1387 cache = {}
1388 for (name, shortcut) in names_and_shortcuts:
1389 sub_menu, action_name = build_menus(name, menu, cache)
1390 callback = cmds.run(cmds.RunConfigAction, context, name)
1391 menu_action = sub_menu.addAction(action_name, callback)
1392 if shortcut:
1393 menu_action.setShortcut(shortcut)
1396 def build_menus(name, menu, cache):
1397 """Create a chain of QMenu entries parented under a root QMenu
1399 A name of "a/b/c" create a menu chain of menu -> QMenu("a") -> QMenu("b")
1400 and returns a tuple of (QMenu("b"), "c").
1402 :param name: The full entry path, ex: "a/b/c" where "a/b" is the menu chain.
1403 :param menu: The root menu under which to create the menu chain.
1404 :param cache: A dict cache of previously created menus to avoid duplicates.
1407 # NOTE: utils.split() and friends are used instead of os.path.split() because
1408 # slash '/' is the only supported "<menu>/name" separator. Use of os.path.split()
1409 # would introduce differences in behavior across platforms.
1411 # If the menu_path is empty then no parent menus need to be created.
1412 # The action will be added to the root menu.
1413 menu_path, text = utils.split(utils.normalize_slash(name))
1415 if not menu_path:
1416 return (menu, text)
1417 # When menu_path contains ex: "a/b" we will create two menus: "a" and "b".
1418 # The root menu is the parent of "a" and "a" is the parent of "b".
1419 # The menu returned to the caller is "b".
1421 # Loop over the individual menu basenames alongside the full subpath returned by
1422 # pathset(). The subpath is a cache key for finding previously created menus.
1423 menu_names = utils.splitpath(menu_path) # ['a', 'b']
1424 menu_pathset = utils.pathset(menu_path) # ['a', 'a/b']
1425 for menu_name, menu_id in zip(menu_names, menu_pathset):
1426 try:
1427 menu = cache[menu_id]
1428 except KeyError:
1429 menu = cache[menu_id] = menu.addMenu(menu_name)
1431 return (menu, text)