diff: display filenames in the left title area
[git-cola.git] / cola / widgets / main.py
blob515f2e34e82773ba5df2b3618ce1aaf7e5aed6d9
1 """Main UI for authoring commits and other Git Cola interactions"""
2 import os
3 from functools import partial
5 from qtpy import QtCore
6 from qtpy import QtGui
7 from qtpy import QtWidgets
8 from qtpy.QtCore import Qt
9 from qtpy.QtCore import Signal
11 from ..compat import uchr
12 from ..compat import WIN32
13 from ..i18n import N_
14 from ..interaction import Interaction
15 from ..models import prefs
16 from ..qtutils import get
17 from .. import cmds
18 from .. import core
19 from .. import guicmds
20 from .. import git
21 from .. import gitcmds
22 from .. import hotkeys
23 from .. import icons
24 from .. import qtutils
25 from .. import resources
26 from .. import utils
27 from .. import version
28 from . import about
29 from . import action
30 from . import archive
31 from . import bookmarks
32 from . import branch
33 from . import submodules
34 from . import browse
35 from . import cfgactions
36 from . import clone
37 from . import commitmsg
38 from . import common
39 from . import compare
40 from . import createbranch
41 from . import createtag
42 from . import dag
43 from . import defs
44 from . import diff
45 from . import finder
46 from . import editremotes
47 from . import grep
48 from . import log
49 from . import merge
50 from . import prefs as prefs_widget
51 from . import recent
52 from . import remote
53 from . import search
54 from . import standard
55 from . import status
56 from . import stash
57 from . import toolbar
60 class MainView(standard.MainWindow):
61 config_actions_changed = Signal(object)
63 def __init__(self, context, parent=None):
64 # pylint: disable=too-many-statements,too-many-locals
65 standard.MainWindow.__init__(self, parent)
66 self.setAttribute(Qt.WA_DeleteOnClose)
68 self.context = context
69 self.git = context.git
70 self.dag = None
71 self.model = context.model
72 self.prefs_model = prefs_model = prefs.PreferencesModel(context)
73 self.toolbar_state = toolbar.ToolBarState(context, self)
75 # The widget version is used by import/export_state().
76 # Change this whenever dockwidgets are removed.
77 self.widget_version = 2
79 create_dock = qtutils.create_dock
80 cfg = context.cfg
81 self.browser_dockable = cfg.get('cola.browserdockable')
82 if self.browser_dockable:
83 browser = browse.worktree_browser(
84 context, parent=self, show=False, update=False
86 self.browserdock = create_dock(
87 'Browser', N_('Browser'), self, widget=browser
90 # "Actions" widget
91 self.actionswidget = action.ActionButtons(context, self)
92 self.actionsdock = create_dock(
93 'Actions', N_('Actions'), self, widget=self.actionswidget
95 qtutils.hide_dock(self.actionsdock)
97 # "Repository Status" widget
98 self.statusdock = create_dock(
99 'Status',
100 N_('Status'),
101 self,
102 func=lambda dock: status.StatusWidget(context, dock.titleBarWidget(), dock),
104 self.statuswidget = self.statusdock.widget()
106 # "Switch Repository" widgets
107 self.bookmarksdock = create_dock(
108 'Favorites',
109 N_('Favorites'),
110 self,
111 func=lambda dock: bookmarks.bookmark(context, dock),
113 bookmarkswidget = self.bookmarksdock.widget()
114 qtutils.hide_dock(self.bookmarksdock)
116 self.recentdock = create_dock(
117 'Recent',
118 N_('Recent'),
119 self,
120 func=lambda dock: bookmarks.recent(context, dock),
122 recentwidget = self.recentdock.widget()
123 qtutils.hide_dock(self.recentdock)
124 bookmarkswidget.connect_to(recentwidget)
126 # "Branch" widgets
127 self.branchdock = create_dock(
128 'Branches',
129 N_('Branches'),
130 self,
131 func=partial(branch.BranchesWidget, context),
133 self.branchwidget = self.branchdock.widget()
134 titlebar = self.branchdock.titleBarWidget()
135 titlebar.add_corner_widget(self.branchwidget.filter_button)
136 titlebar.add_corner_widget(self.branchwidget.sort_order_button)
138 # "Submodule" widgets
139 self.submodulesdock = create_dock(
140 'Submodules',
141 N_('Submodules'),
142 self,
143 func=partial(submodules.SubmodulesWidget, context),
145 self.submoduleswidget = self.submodulesdock.widget()
147 # "Commit Message Editor" widget
148 self.position_label = QtWidgets.QLabel()
149 self.position_label.setAlignment(Qt.AlignCenter)
150 font = qtutils.default_monospace_font()
151 font.setPointSize(int(font.pointSize() * 0.8))
152 self.position_label.setFont(font)
154 # make the position label fixed size to avoid layout issues
155 metrics = self.position_label.fontMetrics()
156 width = metrics.width('99:999') + defs.spacing
157 self.position_label.setMinimumWidth(width)
159 editor = commitmsg.CommitMessageEditor(context, self)
160 self.commiteditor = editor
161 self.commitdock = create_dock('Commit', N_('Commit'), self, widget=editor)
162 titlebar = self.commitdock.titleBarWidget()
163 titlebar.add_corner_widget(self.position_label)
165 # "Console" widget
166 self.logwidget = log.LogWidget(context)
167 self.logdock = create_dock(
168 'Console', N_('Console'), self, widget=self.logwidget
170 qtutils.hide_dock(self.logdock)
172 # "Diff Viewer" widget
173 self.diffdock = create_dock(
174 'Diff',
175 N_('Diff'),
176 self,
177 func=lambda dock: diff.Viewer(context, parent=dock),
179 self.diffviewer = self.diffdock.widget()
180 self.diffviewer.set_diff_type(self.model.diff_type)
181 self.diffviewer.enable_filename_tracking()
182 self.diffeditor = self.diffviewer.text
183 titlebar = self.diffdock.titleBarWidget()
184 titlebar.add_title_widget(self.diffviewer.filename)
185 titlebar.add_corner_widget(self.diffviewer.options)
187 # All Actions
188 add_action = qtutils.add_action
189 add_action_bool = qtutils.add_action_bool
191 self.commit_amend_action = add_action_bool(
192 self,
193 N_('Amend Last Commit'),
194 partial(cmds.do, cmds.AmendMode, context),
195 False,
197 self.commit_amend_action.setIcon(icons.edit())
198 self.commit_amend_action.setShortcut(hotkeys.AMEND)
199 self.commit_amend_action.setShortcutContext(Qt.WidgetShortcut)
201 self.unstage_all_action = add_action(
202 self, N_('Unstage All'), cmds.run(cmds.UnstageAll, context)
204 self.unstage_all_action.setIcon(icons.remove())
206 self.undo_commit_action = add_action(
207 self, N_('Undo Last Commit'), cmds.run(cmds.UndoLastCommit, context)
209 self.undo_commit_action.setIcon(icons.style_dialog_discard())
211 self.unstage_selected_action = add_action(
212 self, N_('Unstage From Commit'), cmds.run(cmds.UnstageSelected, context)
214 self.unstage_selected_action.setIcon(icons.remove())
216 self.show_diffstat_action = add_action(
217 self, N_('Diffstat'), self.statuswidget.select_header, hotkeys.DIFFSTAT
219 self.show_diffstat_action.setIcon(icons.diff())
221 self.stage_modified_action = add_action(
222 self,
223 N_('Stage Changed Files To Commit'),
224 cmds.run(cmds.StageModified, context),
225 hotkeys.STAGE_MODIFIED,
227 self.stage_modified_action.setIcon(icons.add())
229 self.stage_untracked_action = add_action(
230 self,
231 N_('Stage All Untracked'),
232 cmds.run(cmds.StageUntracked, context),
233 hotkeys.STAGE_UNTRACKED,
235 self.stage_untracked_action.setIcon(icons.add())
237 self.apply_patches_action = add_action(
238 self, N_('Apply Patches...'), partial(diff.apply_patches, context)
240 self.apply_patches_action.setIcon(icons.diff())
242 self.apply_patches_abort_action = qtutils.add_action_with_tooltip(
243 self,
244 N_('Abort Applying Patches...'),
245 N_('Abort the current "git am" patch session'),
246 cmds.run(cmds.AbortApplyPatch, context),
248 self.apply_patches_abort_action.setIcon(icons.style_dialog_discard())
250 self.apply_patches_continue_action = qtutils.add_action_with_tooltip(
251 self,
252 N_('Continue Applying Patches'),
253 N_('Commit the current state and continue applying patches'),
254 cmds.run(cmds.ApplyPatchesContinue, context),
256 self.apply_patches_continue_action.setIcon(icons.commit())
258 self.apply_patches_skip_action = qtutils.add_action_with_tooltip(
259 self,
260 N_('Skip Current Patch'),
261 N_('Skip applying the current patch and continue applying patches'),
262 cmds.run(cmds.ApplyPatchesContinue, context),
264 self.apply_patches_skip_action.setIcon(icons.discard())
266 self.export_patches_action = add_action(
267 self,
268 N_('Export Patches...'),
269 partial(guicmds.export_patches, context),
270 hotkeys.EXPORT,
272 self.export_patches_action.setIcon(icons.save())
274 self.new_repository_action = add_action(
275 self, N_('New Repository...'), partial(guicmds.open_new_repo, context)
277 self.new_repository_action.setIcon(icons.new())
279 self.new_bare_repository_action = add_action(
280 self, N_('New Bare Repository...'), partial(guicmds.new_bare_repo, context)
282 self.new_bare_repository_action.setIcon(icons.new())
284 prefs_func = partial(
285 prefs_widget.preferences, context, parent=self, model=prefs_model
287 self.preferences_action = add_action(
288 self, N_('Preferences'), prefs_func, QtGui.QKeySequence.Preferences
290 self.preferences_action.setIcon(icons.configure())
292 self.edit_remotes_action = add_action(
293 self, N_('Edit Remotes...'), partial(editremotes.editor, context)
295 self.edit_remotes_action.setIcon(icons.edit())
297 self.rescan_action = add_action(
298 self,
299 cmds.Refresh.name(),
300 cmds.run(cmds.Refresh, context),
301 *hotkeys.REFRESH_HOTKEYS,
303 self.rescan_action.setIcon(icons.sync())
305 self.find_files_action = add_action(
306 self,
307 N_('Find Files'),
308 partial(finder.finder, context),
309 hotkeys.FINDER,
311 self.find_files_action.setIcon(icons.search())
313 self.browse_recently_modified_action = add_action(
314 self,
315 N_('Recently Modified Files...'),
316 partial(recent.browse_recent_files, context),
317 hotkeys.EDIT_SECONDARY,
319 self.browse_recently_modified_action.setIcon(icons.directory())
321 self.cherry_pick_action = add_action(
322 self,
323 N_('Cherry-Pick...'),
324 partial(guicmds.cherry_pick, context),
325 hotkeys.CHERRY_PICK,
327 self.cherry_pick_action.setIcon(icons.cherry_pick())
328 self.cherry_pick_abort_action = add_action(
329 self, N_('Abort Cherry-Pick...'), cmds.run(cmds.AbortCherryPick, context)
331 self.cherry_pick_abort_action.setIcon(icons.style_dialog_discard())
333 self.load_commitmsg_action = add_action(
334 self, N_('Load Commit Message...'), partial(guicmds.load_commitmsg, context)
336 self.load_commitmsg_action.setIcon(icons.file_text())
338 self.prepare_commitmsg_hook_action = add_action(
339 self,
340 N_('Prepare Commit Message'),
341 cmds.run(cmds.PrepareCommitMessageHook, context),
342 hotkeys.PREPARE_COMMIT_MESSAGE,
345 self.save_tarball_action = add_action(
346 self, N_('Save As Tarball/Zip...'), partial(archive.save_archive, context)
348 self.save_tarball_action.setIcon(icons.file_zip())
350 self.quit_action = add_action(self, N_('Quit'), self.close, hotkeys.QUIT)
352 self.grep_action = add_action(
353 self, N_('Grep'), partial(grep.grep, context), hotkeys.GREP
355 self.grep_action.setIcon(icons.search())
357 self.merge_local_action = add_action(
358 self, N_('Merge...'), partial(merge.local_merge, context), hotkeys.MERGE
360 self.merge_local_action.setIcon(icons.merge())
362 self.merge_abort_action = add_action(
363 self, N_('Abort Merge...'), cmds.run(cmds.AbortMerge, context)
365 self.merge_abort_action.setIcon(icons.style_dialog_discard())
367 self.update_submodules_action = add_action(
368 self,
369 N_('Update All Submodules...'),
370 cmds.run(cmds.SubmodulesUpdate, context),
372 self.update_submodules_action.setIcon(icons.sync())
374 self.add_submodule_action = add_action(
375 self,
376 N_('Add Submodule...'),
377 partial(submodules.add_submodule, context, parent=self),
379 self.add_submodule_action.setIcon(icons.add())
381 self.fetch_action = qtutils.add_action_with_tooltip(
382 self,
383 N_('Fetch...'),
384 N_('Fetch from one or more remotes using "git fetch"'),
385 partial(remote.fetch, context),
386 hotkeys.FETCH,
388 self.fetch_action.setIcon(icons.download())
390 self.push_action = qtutils.add_action_with_tooltip(
391 self,
392 N_('Push...'),
393 N_('Push to one or more remotes using "git push"'),
394 partial(remote.push, context),
395 hotkeys.PUSH,
397 self.push_action.setIcon(icons.push())
399 self.pull_action = qtutils.add_action_with_tooltip(
400 self,
401 N_('Pull...'),
402 N_('Integrate changes using "git pull"'),
403 partial(remote.pull, context),
404 hotkeys.PULL,
406 self.pull_action.setIcon(icons.pull())
408 self.open_repo_action = add_action(
409 self, N_('Open...'), partial(guicmds.open_repo, context), hotkeys.OPEN
411 self.open_repo_action.setIcon(icons.folder())
413 self.open_repo_new_action = add_action(
414 self,
415 N_('Open in New Window...'),
416 partial(guicmds.open_repo_in_new_window, context),
418 self.open_repo_new_action.setIcon(icons.folder())
420 self.stash_action = qtutils.add_action_with_tooltip(
421 self,
422 N_('Stash...'),
423 N_('Temporarily stash away uncommitted changes using "git stash"'),
424 partial(stash.view, context),
425 hotkeys.STASH,
427 self.stash_action.setIcon(icons.commit())
429 self.reset_soft_action = qtutils.add_action_with_tooltip(
430 self,
431 N_('Reset Branch (Soft)'),
432 cmds.ResetSoft.tooltip('<commit>'),
433 partial(guicmds.reset_soft, context),
435 self.reset_soft_action.setIcon(icons.style_dialog_reset())
437 self.reset_mixed_action = qtutils.add_action_with_tooltip(
438 self,
439 N_('Reset Branch and Stage (Mixed)'),
440 cmds.ResetMixed.tooltip('<commit>'),
441 partial(guicmds.reset_mixed, context),
443 self.reset_mixed_action.setIcon(icons.style_dialog_reset())
445 self.reset_keep_action = qtutils.add_action_with_tooltip(
446 self,
447 N_('Restore Worktree and Reset All (Keep Unstaged Changes)'),
448 cmds.ResetKeep.tooltip('<commit>'),
449 partial(guicmds.reset_keep, context),
451 self.reset_keep_action.setIcon(icons.style_dialog_reset())
453 self.reset_merge_action = qtutils.add_action_with_tooltip(
454 self,
455 N_('Restore Worktree and Reset All (Merge)'),
456 cmds.ResetMerge.tooltip('<commit>'),
457 partial(guicmds.reset_merge, context),
459 self.reset_merge_action.setIcon(icons.style_dialog_reset())
461 self.reset_hard_action = qtutils.add_action_with_tooltip(
462 self,
463 N_('Restore Worktree and Reset All (Hard)'),
464 cmds.ResetHard.tooltip('<commit>'),
465 partial(guicmds.reset_hard, context),
467 self.reset_hard_action.setIcon(icons.style_dialog_reset())
469 self.restore_worktree_action = qtutils.add_action_with_tooltip(
470 self,
471 N_('Restore Worktree'),
472 cmds.RestoreWorktree.tooltip('<commit>'),
473 partial(guicmds.restore_worktree, context),
475 self.restore_worktree_action.setIcon(icons.edit())
477 self.clone_repo_action = add_action(
478 self, N_('Clone...'), partial(clone.clone, context)
480 self.clone_repo_action.setIcon(icons.repo())
482 self.help_docs_action = add_action(
483 self,
484 N_('Documentation'),
485 resources.show_html_docs,
486 QtGui.QKeySequence.HelpContents,
489 self.help_shortcuts_action = add_action(
490 self, N_('Keyboard Shortcuts'), about.show_shortcuts, hotkeys.QUESTION
493 self.visualize_current_action = add_action(
494 self,
495 N_('Visualize Current Branch...'),
496 cmds.run(cmds.VisualizeCurrent, context),
498 self.visualize_current_action.setIcon(icons.visualize())
500 self.visualize_all_action = add_action(
501 self, N_('Visualize All Branches...'), cmds.run(cmds.VisualizeAll, context)
503 self.visualize_all_action.setIcon(icons.visualize())
505 self.search_commits_action = add_action(
506 self, N_('Search...'), partial(search.search, context)
508 self.search_commits_action.setIcon(icons.search())
510 self.browse_branch_action = add_action(
511 self,
512 N_('Browse Current Branch...'),
513 partial(guicmds.browse_current, context),
515 self.browse_branch_action.setIcon(icons.directory())
517 self.browse_other_branch_action = add_action(
518 self, N_('Browse Other Branch...'), partial(guicmds.browse_other, context)
520 self.browse_other_branch_action.setIcon(icons.directory())
522 self.load_commitmsg_template_action = add_action(
523 self,
524 N_('Get Commit Message Template'),
525 cmds.run(cmds.LoadCommitMessageFromTemplate, context),
527 self.load_commitmsg_template_action.setIcon(icons.style_dialog_apply())
529 self.help_about_action = add_action(
530 self, N_('About'), partial(about.about_dialog, context)
533 self.diff_against_commit_action = add_action(
534 self,
535 N_('Against Commit... (Diff Mode)'),
536 partial(guicmds.diff_against_commit, context),
538 self.diff_against_commit_action.setIcon(icons.compare())
540 self.exit_diff_mode_action = add_action(
541 self, N_('Exit Diff Mode'), cmds.run(cmds.ResetMode, context)
543 self.exit_diff_mode_action.setIcon(icons.compare())
545 self.diff_expression_action = add_action(
546 self, N_('Expression...'), partial(guicmds.diff_expression, context)
548 self.diff_expression_action.setIcon(icons.compare())
550 self.branch_compare_action = add_action(
551 self, N_('Branches...'), partial(compare.compare_branches, context)
553 self.branch_compare_action.setIcon(icons.compare())
555 self.create_tag_action = add_action(
556 self,
557 N_('Create Tag...'),
558 partial(createtag.create_tag, context),
560 self.create_tag_action.setIcon(icons.tag())
562 self.create_branch_action = add_action(
563 self,
564 N_('Create...'),
565 partial(createbranch.create_new_branch, context),
566 hotkeys.BRANCH,
568 self.create_branch_action.setIcon(icons.branch())
570 self.delete_branch_action = add_action(
571 self, N_('Delete...'), partial(guicmds.delete_branch, context)
573 self.delete_branch_action.setIcon(icons.discard())
575 self.delete_remote_branch_action = add_action(
576 self,
577 N_('Delete Remote Branch...'),
578 partial(guicmds.delete_remote_branch, context),
580 self.delete_remote_branch_action.setIcon(icons.discard())
582 self.rename_branch_action = add_action(
583 self, N_('Rename Branch...'), partial(guicmds.rename_branch, context)
585 self.rename_branch_action.setIcon(icons.edit())
587 self.checkout_branch_action = add_action(
588 self,
589 N_('Checkout...'),
590 partial(guicmds.checkout_branch, context),
591 hotkeys.CHECKOUT,
593 self.checkout_branch_action.setIcon(icons.branch())
595 self.branch_review_action = add_action(
596 self, N_('Review...'), partial(guicmds.review_branch, context)
598 self.branch_review_action.setIcon(icons.compare())
600 self.browse_action = add_action(
601 self, N_('File Browser...'), partial(browse.worktree_browser, context)
603 self.browse_action.setIcon(icons.cola())
605 self.dag_action = add_action(self, N_('DAG...'), self.git_dag)
606 self.dag_action.setIcon(icons.cola())
608 self.rebase_start_action = add_action(
609 self,
610 N_('Start Interactive Rebase...'),
611 cmds.run(cmds.Rebase, context),
612 hotkeys.REBASE_START_AND_CONTINUE,
614 self.rebase_start_action.setIcon(icons.play())
616 self.rebase_edit_todo_action = add_action(
617 self, N_('Edit...'), cmds.run(cmds.RebaseEditTodo, context)
619 self.rebase_edit_todo_action.setIcon(icons.edit())
621 self.rebase_continue_action = add_action(
622 self,
623 N_('Continue'),
624 cmds.run(cmds.RebaseContinue, context),
625 hotkeys.REBASE_START_AND_CONTINUE,
627 self.rebase_continue_action.setIcon(icons.play())
629 self.rebase_skip_action = add_action(
630 self, N_('Skip Current Patch'), cmds.run(cmds.RebaseSkip, context)
632 self.rebase_skip_action.setIcon(icons.delete())
634 self.rebase_abort_action = add_action(
635 self, N_('Abort'), cmds.run(cmds.RebaseAbort, context)
637 self.rebase_abort_action.setIcon(icons.close())
639 # For "Start Rebase" only, reverse the first argument to setEnabled()
640 # so that we can operate on it as a group.
641 # We can do this because can_rebase == not is_rebasing
642 self.rebase_start_action_proxy = utils.Proxy(
643 self.rebase_start_action,
644 setEnabled=lambda x: self.rebase_start_action.setEnabled(not x),
647 self.rebase_group = utils.Group(
648 self.rebase_start_action_proxy,
649 self.rebase_edit_todo_action,
650 self.rebase_continue_action,
651 self.rebase_skip_action,
652 self.rebase_abort_action,
655 self.annex_init_action = qtutils.add_action(
656 self, N_('Initialize Git Annex'), cmds.run(cmds.AnnexInit, context)
659 self.lfs_init_action = qtutils.add_action(
660 self, N_('Initialize Git LFS'), cmds.run(cmds.LFSInstall, context)
663 self.lock_layout_action = add_action_bool(
664 self, N_('Lock Layout'), self.set_lock_layout, False
667 self.reset_layout_action = add_action(
668 self, N_('Reset Layout'), self.reset_layout
671 self.quick_repository_search = add_action(
672 self,
673 N_('Quick Open...'),
674 lambda: guicmds.open_quick_repo_search(self.context, parent=self),
675 hotkeys.OPEN_REPO_SEARCH,
677 self.quick_repository_search.setIcon(icons.search())
679 self.terminal_action = common.terminal_action(
680 context, self, hotkey=hotkeys.TERMINAL
683 # Create the application menu
684 self.menubar = QtWidgets.QMenuBar(self)
685 self.setMenuBar(self.menubar)
687 # File Menu
688 add_menu = qtutils.add_menu
689 self.file_menu = add_menu(N_('&File'), self.menubar)
690 self.file_menu.addAction(self.quick_repository_search)
691 # File->Open Recent menu
692 self.open_recent_menu = self.file_menu.addMenu(N_('Open Recent'))
693 self.open_recent_menu.setIcon(icons.folder())
694 self.file_menu.addAction(self.open_repo_action)
695 self.file_menu.addAction(self.open_repo_new_action)
696 self.file_menu.addSeparator()
697 self.file_menu.addAction(self.new_repository_action)
698 self.file_menu.addAction(self.new_bare_repository_action)
699 self.file_menu.addAction(self.clone_repo_action)
700 self.file_menu.addSeparator()
701 self.file_menu.addAction(self.rescan_action)
702 self.file_menu.addAction(self.find_files_action)
703 self.file_menu.addAction(self.edit_remotes_action)
704 self.file_menu.addAction(self.browse_recently_modified_action)
705 self.file_menu.addSeparator()
706 self.file_menu.addAction(self.save_tarball_action)
708 self.patches_menu = self.file_menu.addMenu(N_('Patches'))
709 self.patches_menu.setIcon(icons.diff())
710 self.patches_menu.addAction(self.export_patches_action)
711 self.patches_menu.addAction(self.apply_patches_action)
712 self.patches_menu.addAction(self.apply_patches_continue_action)
713 self.patches_menu.addAction(self.apply_patches_skip_action)
714 self.patches_menu.addAction(self.apply_patches_abort_action)
716 # Git Annex / Git LFS
717 annex = core.find_executable('git-annex')
718 lfs = core.find_executable('git-lfs')
719 if annex or lfs:
720 self.file_menu.addSeparator()
721 if annex:
722 self.file_menu.addAction(self.annex_init_action)
723 if lfs:
724 self.file_menu.addAction(self.lfs_init_action)
726 self.file_menu.addSeparator()
727 self.file_menu.addAction(self.preferences_action)
728 self.file_menu.addAction(self.quit_action)
730 # Edit Menu
731 self.edit_proxy = edit_proxy = FocusProxy(
732 editor, editor.summary, editor.description
735 copy_widgets = (
736 self,
737 editor.summary,
738 editor.description,
739 self.diffeditor,
740 bookmarkswidget.tree,
741 recentwidget.tree,
743 select_widgets = copy_widgets + (self.statuswidget.tree,)
744 edit_proxy.override('copy', copy_widgets)
745 edit_proxy.override('selectAll', select_widgets)
747 edit_menu = self.edit_menu = add_menu(N_('&Edit'), self.menubar)
748 undo = add_action(edit_menu, N_('Undo'), edit_proxy.undo, hotkeys.UNDO)
749 undo.setIcon(icons.undo())
750 redo = add_action(edit_menu, N_('Redo'), edit_proxy.redo, hotkeys.REDO)
751 redo.setIcon(icons.redo())
752 edit_menu.addSeparator()
753 cut = add_action(edit_menu, N_('Cut'), edit_proxy.cut, hotkeys.CUT)
754 cut.setIcon(icons.cut())
755 copy = add_action(edit_menu, N_('Copy'), edit_proxy.copy, hotkeys.COPY)
756 copy.setIcon(icons.copy())
757 paste = add_action(edit_menu, N_('Paste'), edit_proxy.paste, hotkeys.PASTE)
758 paste.setIcon(icons.paste())
759 delete = add_action(edit_menu, N_('Delete'), edit_proxy.delete, hotkeys.DELETE)
760 delete.setIcon(icons.delete())
761 edit_menu.addSeparator()
762 select_all = add_action(
763 edit_menu, N_('Select All'), edit_proxy.selectAll, hotkeys.SELECT_ALL
765 select_all.setIcon(icons.select_all())
766 edit_menu.addSeparator()
767 qtutils.add_menu_actions(edit_menu, self.commiteditor.menu_actions)
769 # Actions menu
770 self.actions_menu = add_menu(N_('Actions'), self.menubar)
771 if self.terminal_action is not None:
772 self.actions_menu.addAction(self.terminal_action)
773 self.actions_menu.addAction(self.fetch_action)
774 self.actions_menu.addAction(self.push_action)
775 self.actions_menu.addAction(self.pull_action)
776 self.actions_menu.addAction(self.stash_action)
777 self.actions_menu.addSeparator()
778 self.actions_menu.addAction(self.create_tag_action)
779 self.actions_menu.addAction(self.cherry_pick_action)
780 self.actions_menu.addAction(self.cherry_pick_abort_action)
781 self.actions_menu.addAction(self.merge_local_action)
782 self.actions_menu.addAction(self.merge_abort_action)
783 self.actions_menu.addSeparator()
784 self.actions_menu.addAction(self.update_submodules_action)
785 self.actions_menu.addAction(self.add_submodule_action)
786 self.actions_menu.addSeparator()
787 self.actions_menu.addAction(self.grep_action)
788 self.actions_menu.addAction(self.search_commits_action)
790 # Commit Menu
791 self.commit_menu = add_menu(N_('Commit@@verb'), self.menubar)
792 self.commit_menu.setTitle(N_('Commit@@verb'))
793 self.commit_menu.addAction(self.commiteditor.commit_action)
794 self.commit_menu.addAction(self.commit_amend_action)
795 self.commit_menu.addAction(self.undo_commit_action)
796 self.commit_menu.addSeparator()
797 self.commit_menu.addAction(self.statuswidget.tree.process_selection_action)
798 self.commit_menu.addAction(self.statuswidget.tree.stage_or_unstage_all_action)
799 self.commit_menu.addAction(self.stage_modified_action)
800 self.commit_menu.addAction(self.stage_untracked_action)
801 self.commit_menu.addSeparator()
802 self.commit_menu.addAction(self.unstage_all_action)
803 self.commit_menu.addAction(self.unstage_selected_action)
804 self.commit_menu.addSeparator()
805 self.commit_menu.addAction(self.load_commitmsg_action)
806 self.commit_menu.addAction(self.load_commitmsg_template_action)
807 self.commit_menu.addAction(self.prepare_commitmsg_hook_action)
809 # Diff Menu
810 self.diff_menu = add_menu(N_('Diff'), self.menubar)
811 self.diff_menu.addAction(self.diff_expression_action)
812 self.diff_menu.addAction(self.branch_compare_action)
813 self.diff_menu.addAction(self.show_diffstat_action)
814 self.diff_menu.addSeparator()
815 self.diff_menu.addAction(self.diff_against_commit_action)
816 self.diff_menu.addAction(self.exit_diff_mode_action)
818 # Branch Menu
819 self.branch_menu = add_menu(N_('Branch'), self.menubar)
820 self.branch_menu.addAction(self.branch_review_action)
821 self.branch_menu.addSeparator()
822 self.branch_menu.addAction(self.create_branch_action)
823 self.branch_menu.addAction(self.checkout_branch_action)
824 self.branch_menu.addAction(self.delete_branch_action)
825 self.branch_menu.addAction(self.delete_remote_branch_action)
826 self.branch_menu.addAction(self.rename_branch_action)
827 self.branch_menu.addSeparator()
828 self.branch_menu.addAction(self.browse_branch_action)
829 self.branch_menu.addAction(self.browse_other_branch_action)
830 self.branch_menu.addSeparator()
831 self.branch_menu.addAction(self.visualize_current_action)
832 self.branch_menu.addAction(self.visualize_all_action)
834 # Rebase menu
835 self.rebase_menu = add_menu(N_('Rebase'), self.menubar)
836 self.rebase_menu.addAction(self.rebase_start_action)
837 self.rebase_menu.addAction(self.rebase_edit_todo_action)
838 self.rebase_menu.addSeparator()
839 self.rebase_menu.addAction(self.rebase_continue_action)
840 self.rebase_menu.addAction(self.rebase_skip_action)
841 self.rebase_menu.addSeparator()
842 self.rebase_menu.addAction(self.rebase_abort_action)
844 # Reset menu
845 self.reset_menu = add_menu(N_('Reset'), self.menubar)
846 self.reset_menu.addAction(self.unstage_all_action)
847 self.reset_menu.addAction(self.undo_commit_action)
848 self.reset_menu.addSeparator()
849 self.reset_menu.addAction(self.reset_soft_action)
850 self.reset_menu.addAction(self.reset_mixed_action)
851 self.reset_menu.addAction(self.restore_worktree_action)
852 self.reset_menu.addSeparator()
853 self.reset_menu.addAction(self.reset_keep_action)
854 self.reset_menu.addAction(self.reset_merge_action)
855 self.reset_menu.addAction(self.reset_hard_action)
857 # View Menu
858 self.view_menu = add_menu(N_('View'), self.menubar)
859 # pylint: disable=no-member
860 self.view_menu.aboutToShow.connect(lambda: self.build_view_menu(self.view_menu))
861 self.setup_dockwidget_view_menu()
862 if utils.is_darwin():
863 # The native macOS menu doesn't show empty entries.
864 self.build_view_menu(self.view_menu)
866 # Help Menu
867 self.help_menu = add_menu(N_('Help'), self.menubar)
868 self.help_menu.addAction(self.help_docs_action)
869 self.help_menu.addAction(self.help_shortcuts_action)
870 self.help_menu.addAction(self.help_about_action)
872 # Arrange dock widgets
873 bottom = Qt.BottomDockWidgetArea
874 top = Qt.TopDockWidgetArea
876 self.addDockWidget(top, self.statusdock)
877 self.addDockWidget(top, self.commitdock)
878 if self.browser_dockable:
879 self.addDockWidget(top, self.browserdock)
880 self.tabifyDockWidget(self.browserdock, self.commitdock)
882 self.addDockWidget(top, self.branchdock)
883 self.addDockWidget(top, self.submodulesdock)
884 self.addDockWidget(top, self.bookmarksdock)
885 self.addDockWidget(top, self.recentdock)
887 self.tabifyDockWidget(self.branchdock, self.submodulesdock)
888 self.tabifyDockWidget(self.submodulesdock, self.bookmarksdock)
889 self.tabifyDockWidget(self.bookmarksdock, self.recentdock)
890 self.branchdock.raise_()
892 self.addDockWidget(bottom, self.diffdock)
893 self.addDockWidget(bottom, self.actionsdock)
894 self.addDockWidget(bottom, self.logdock)
895 self.tabifyDockWidget(self.actionsdock, self.logdock)
897 # Listen for model notifications
898 self.model.updated.connect(self.refresh, type=Qt.QueuedConnection)
899 self.model.mode_changed.connect(
900 lambda mode: self.refresh(), type=Qt.QueuedConnection
903 prefs_model.config_updated.connect(self._config_updated)
905 # Set a default value
906 self.show_cursor_position(1, 0)
908 self.commit_menu.aboutToShow.connect(self.update_menu_actions)
909 self.open_recent_menu.aboutToShow.connect(self.build_recent_menu)
910 self.commiteditor.cursor_changed.connect(self.show_cursor_position)
912 self.diffeditor.options_changed.connect(self.statuswidget.refresh)
913 self.diffeditor.up.connect(self.statuswidget.move_up)
914 self.diffeditor.down.connect(self.statuswidget.move_down)
916 self.commiteditor.up.connect(self.statuswidget.move_up)
917 self.commiteditor.down.connect(self.statuswidget.move_down)
919 self.config_actions_changed.connect(
920 lambda names_and_shortcuts: _install_config_actions(
921 context,
922 self.actions_menu,
923 names_and_shortcuts,
925 type=Qt.QueuedConnection,
927 self.init_state(context.settings, self.set_initial_size)
929 # Route command output here
930 Interaction.log_status = self.logwidget.log_status
931 Interaction.log = self.logwidget.log
932 # Focus the status widget; this must be deferred
933 QtCore.QTimer.singleShot(0, self.initialize)
935 def initialize(self):
936 context = self.context
937 git_version = version.git_version_str(context)
938 if git_version:
939 ok = True
940 Interaction.log(
941 git_version + '\n' + N_('git cola version %s') % version.version()
943 else:
944 ok = False
945 error_msg = N_('error: unable to execute git')
946 Interaction.log(error_msg)
948 if ok:
949 self.statuswidget.setFocus()
950 else:
951 title = N_('error: unable to execute git')
952 msg = title
953 details = ''
954 if WIN32:
955 details = git.win32_git_error_hint()
956 Interaction.critical(title, message=msg, details=details)
957 self.context.app.exit(2)
959 def set_initial_size(self):
960 # Default size; this is thrown out when save/restore is used
961 width, height = qtutils.desktop_size()
962 self.resize((width * 3) // 4, height)
963 self.statuswidget.set_initial_size()
964 self.commiteditor.set_initial_size()
966 def set_filter(self, txt):
967 self.statuswidget.set_filter(txt)
969 # Qt overrides
970 def closeEvent(self, event):
971 """Save state in the settings"""
972 commit_msg = self.commiteditor.commit_message(raw=True)
973 self.model.save_commitmsg(msg=commit_msg)
974 for browser in list(self.context.browser_windows):
975 browser.close()
976 standard.MainWindow.closeEvent(self, event)
978 def create_view_menu(self):
979 menu = qtutils.create_menu(N_('View'), self)
980 self.build_view_menu(menu)
981 return menu
983 def build_view_menu(self, menu):
984 menu.clear()
985 menu.addAction(self.browse_action)
986 menu.addAction(self.dag_action)
987 menu.addSeparator()
989 popup_menu = self.createPopupMenu()
990 for menu_action in popup_menu.actions():
991 menu_action.setParent(menu)
992 menu.addAction(menu_action)
994 context = self.context
995 menu_action = menu.addAction(
996 N_('New Toolbar'), partial(toolbar.add_toolbar, context, self)
998 menu_action.setIcon(icons.add())
999 menu.addSeparator()
1001 dockwidgets = [
1002 self.logdock,
1003 self.commitdock,
1004 self.statusdock,
1005 self.diffdock,
1006 self.actionsdock,
1007 self.bookmarksdock,
1008 self.recentdock,
1009 self.branchdock,
1010 self.submodulesdock,
1012 if self.browser_dockable:
1013 dockwidgets.append(self.browserdock)
1015 for dockwidget in dockwidgets:
1016 # Associate the action with the shortcut
1017 toggleview = dockwidget.toggleViewAction()
1018 menu.addAction(toggleview)
1020 menu.addSeparator()
1021 menu.addAction(self.lock_layout_action)
1022 menu.addAction(self.reset_layout_action)
1024 return menu
1026 def contextMenuEvent(self, event):
1027 menu = self.create_view_menu()
1028 menu.exec_(event.globalPos())
1030 def build_recent_menu(self):
1031 cmd = cmds.OpenRepo
1032 context = self.context
1033 settings = context.settings
1034 settings.load()
1035 menu = self.open_recent_menu
1036 menu.clear()
1037 worktree = context.git.worktree()
1039 for entry in settings.recent:
1040 directory = entry['path']
1041 if directory == worktree:
1042 # Omit the current worktree from the "Open Recent" menu.
1043 continue
1044 name = entry['name']
1045 text = f'{name} {uchr(0x2192)} {directory}'
1046 menu.addAction(text, cmds.run(cmd, context, directory))
1048 # Accessors
1049 mode = property(lambda self: self.model.mode)
1051 def _config_updated(self, _source, config, value):
1052 if config == prefs.FONTDIFF:
1053 # The diff font
1054 font = QtGui.QFont()
1055 if not font.fromString(value):
1056 return
1057 self.logwidget.setFont(font)
1058 self.diffeditor.setFont(font)
1059 self.commiteditor.setFont(font)
1061 elif config == prefs.TABWIDTH:
1062 # This can be set locally or globally, so we have to use the
1063 # effective value otherwise we'll update when we shouldn't.
1064 # For example, if this value is overridden locally, and the
1065 # global value is tweaked, we should not update.
1066 value = prefs.tabwidth(self.context)
1067 self.diffeditor.set_tabwidth(value)
1068 self.commiteditor.set_tabwidth(value)
1070 elif config == prefs.EXPANDTAB:
1071 self.commiteditor.set_expandtab(value)
1073 elif config == prefs.LINEBREAK:
1074 # enables automatic line breaks
1075 self.commiteditor.set_linebreak(value)
1077 elif config == prefs.SORT_BOOKMARKS:
1078 self.bookmarksdock.widget().reload_bookmarks()
1080 elif config == prefs.TEXTWIDTH:
1081 # Use the effective value for the same reason as tabwidth.
1082 value = prefs.textwidth(self.context)
1083 self.commiteditor.set_textwidth(value)
1085 elif config == prefs.SHOW_PATH:
1086 # the path in the window title was toggled
1087 self.refresh_window_title()
1089 def start(self, context):
1090 """Do the expensive "get_config_actions()" call in the background"""
1091 # Install .git-config-defined actions
1092 task = qtutils.SimpleTask(self.get_config_actions)
1093 context.runtask.start(task)
1095 def get_config_actions(self):
1096 actions = cfgactions.get_config_actions(self.context)
1097 self.config_actions_changed.emit(actions)
1099 def refresh(self):
1100 """Update the title with the current branch and directory name."""
1101 curbranch = self.model.currentbranch
1102 is_merging = self.model.is_merging
1103 is_rebasing = self.model.is_rebasing
1104 is_applying_patch = self.model.is_applying_patch
1105 is_cherry_picking = self.model.is_rebasing
1107 curdir = core.getcwd()
1108 msg = N_('Repository: %s') % curdir
1109 msg += '\n'
1110 msg += N_('Branch: %s') % curbranch
1112 if is_rebasing:
1113 msg += '\n\n'
1114 msg += N_(
1115 'This repository is currently being rebased.\n'
1116 'Resolve conflicts, commit changes, and run:\n'
1117 ' Rebase > Continue'
1119 elif is_applying_patch:
1120 msg += '\n\n'
1121 msg += N_(
1122 'This repository has unresolved conflicts after applying a patch.\n'
1123 'Resolve conflicts and commit changes.'
1125 elif is_cherry_picking:
1126 msg += '\n\n'
1127 msg += N_(
1128 'This repository is in the middle of a cherry-pick.\n'
1129 'Resolve conflicts and commit changes.'
1131 elif is_merging:
1132 msg += '\n\n'
1133 msg += N_(
1134 'This repository is in the middle of a merge.\n'
1135 'Resolve conflicts and commit changes.'
1138 self.refresh_window_title()
1140 if self.mode == self.model.mode_amend:
1141 self.commit_amend_action.setChecked(True)
1142 else:
1143 self.commit_amend_action.setChecked(False)
1145 self.commitdock.setToolTip(msg)
1147 self.actionswidget.set_mode(self.mode)
1148 self.commiteditor.set_mode(self.mode)
1149 self.statuswidget.set_mode(self.mode)
1151 self.update_actions()
1153 def refresh_window_title(self):
1154 """Refresh the window title when state changes"""
1155 alerts = []
1157 project = self.model.project
1158 curbranch = self.model.currentbranch
1159 is_cherry_picking = self.model.is_cherry_picking
1160 is_merging = self.model.is_merging
1161 is_rebasing = self.model.is_rebasing
1162 is_applying_patch = self.model.is_applying_patch
1163 is_diff_mode = self.model.is_diff_mode()
1164 is_amend_mode = self.mode == self.model.mode_amend
1166 prefix = uchr(0xAB)
1167 suffix = uchr(0xBB)
1169 if is_amend_mode:
1170 alerts.append(N_('Amending'))
1171 elif is_diff_mode:
1172 alerts.append(N_('Diff Mode'))
1173 elif is_cherry_picking:
1174 alerts.append(N_('Cherry-picking'))
1175 elif is_merging:
1176 alerts.append(N_('Merging'))
1177 elif is_rebasing:
1178 alerts.append(N_('Rebasing'))
1179 elif is_applying_patch:
1180 alerts.append(N_('Applying Patch'))
1182 if alerts:
1183 alert_text = (prefix + ' %s ' + suffix + ' ') % ', '.join(alerts)
1184 else:
1185 alert_text = ''
1187 if self.model.cfg.get(prefs.SHOW_PATH, True):
1188 path_text = self.git.worktree()
1189 else:
1190 path_text = ''
1192 title = f'{project}: {curbranch} {alert_text}{path_text}'
1193 self.setWindowTitle(title)
1195 def update_actions(self):
1196 is_rebasing = self.model.is_rebasing
1197 self.rebase_group.setEnabled(is_rebasing)
1199 enabled = not self.model.is_empty_repository()
1200 self.rename_branch_action.setEnabled(enabled)
1201 self.delete_branch_action.setEnabled(enabled)
1203 self.annex_init_action.setEnabled(not self.model.annex)
1204 self.lfs_init_action.setEnabled(not self.model.lfs)
1205 self.merge_abort_action.setEnabled(self.model.is_merging)
1206 self.cherry_pick_abort_action.setEnabled(self.model.is_cherry_picking)
1207 self.apply_patches_continue_action.setEnabled(self.model.is_applying_patch)
1208 self.apply_patches_skip_action.setEnabled(self.model.is_applying_patch)
1209 self.apply_patches_abort_action.setEnabled(self.model.is_applying_patch)
1211 diff_mode = self.model.mode == self.model.mode_diff
1212 self.exit_diff_mode_action.setEnabled(diff_mode)
1214 def update_menu_actions(self):
1215 # Enable the Prepare Commit Message action if the hook exists
1216 hook = gitcmds.prepare_commit_message_hook(self.context)
1217 enabled = os.path.exists(hook)
1218 self.prepare_commitmsg_hook_action.setEnabled(enabled)
1220 def export_state(self):
1221 state = standard.MainWindow.export_state(self)
1222 show_status_filter = self.statuswidget.filter_widget.isVisible()
1223 state['show_status_filter'] = show_status_filter
1224 state['toolbars'] = self.toolbar_state.export_state()
1225 state['ref_sort'] = self.model.ref_sort
1226 self.diffviewer.export_state(state)
1228 return state
1230 def apply_state(self, state):
1231 """Imports data for save/restore"""
1232 base_ok = standard.MainWindow.apply_state(self, state)
1233 lock_layout = state.get('lock_layout', False)
1234 self.lock_layout_action.setChecked(lock_layout)
1236 show_status_filter = state.get('show_status_filter', False)
1237 self.statuswidget.filter_widget.setVisible(show_status_filter)
1239 toolbars = state.get('toolbars', [])
1240 self.toolbar_state.apply_state(toolbars)
1242 sort_key = state.get('ref_sort', 0)
1243 self.model.set_ref_sort(sort_key)
1245 diff_ok = self.diffviewer.apply_state(state)
1246 return base_ok and diff_ok
1248 def setup_dockwidget_view_menu(self):
1249 # Hotkeys for toggling the dock widgets
1250 if utils.is_darwin():
1251 optkey = 'Meta'
1252 else:
1253 optkey = 'Ctrl'
1254 dockwidgets = (
1255 (optkey + '+0', self.logdock),
1256 (optkey + '+1', self.commitdock),
1257 (optkey + '+2', self.statusdock),
1258 (optkey + '+3', self.diffdock),
1259 (optkey + '+4', self.actionsdock),
1260 (optkey + '+5', self.bookmarksdock),
1261 (optkey + '+6', self.recentdock),
1262 (optkey + '+7', self.branchdock),
1263 (optkey + '+8', self.submodulesdock),
1265 for shortcut, dockwidget in dockwidgets:
1266 # Associate the action with the shortcut
1267 toggleview = dockwidget.toggleViewAction()
1268 toggleview.setShortcut('Shift+' + shortcut)
1270 def showdock(show, dockwidget=dockwidget):
1271 if show:
1272 dockwidget.raise_()
1273 dockwidget.widget().setFocus()
1274 else:
1275 self.setFocus()
1277 self.addAction(toggleview)
1278 qtutils.connect_action_bool(toggleview, showdock)
1280 # Create a new shortcut Shift+<shortcut> that gives focus
1281 toggleview = QtWidgets.QAction(self)
1282 toggleview.setShortcut(shortcut)
1284 def focusdock(dockwidget=dockwidget):
1285 focus_dock(dockwidget)
1287 self.addAction(toggleview)
1288 qtutils.connect_action(toggleview, focusdock)
1290 # These widgets warrant home-row hotkey status
1291 qtutils.add_action(
1292 self,
1293 'Focus Commit Message',
1294 lambda: focus_dock(self.commitdock),
1295 hotkeys.FOCUS,
1298 qtutils.add_action(
1299 self,
1300 'Focus Status Window',
1301 lambda: focus_dock(self.statusdock),
1302 hotkeys.FOCUS_STATUS,
1305 qtutils.add_action(
1306 self,
1307 'Focus Diff Editor',
1308 lambda: focus_dock(self.diffdock),
1309 hotkeys.FOCUS_DIFF,
1312 def git_dag(self):
1313 self.dag = dag.git_dag(self.context, existing_view=self.dag)
1315 def show_cursor_position(self, rows, cols):
1316 display_content = '%02d:%02d' % (rows, cols)
1317 css = """
1318 <style>
1319 .good {
1321 .first-warning {
1322 color: black;
1323 background-color: yellow;
1325 .second-warning {
1326 color: black;
1327 background-color: #f83;
1329 .error {
1330 color: white;
1331 background-color: red;
1333 </style>
1336 if cols > 78:
1337 cls = 'error'
1338 elif cols > 72:
1339 cls = 'second-warning'
1340 elif cols > 64:
1341 cls = 'first-warning'
1342 else:
1343 cls = 'good'
1344 div = f'<div class="{cls}">{display_content}</div>'
1345 self.position_label.setText(css + div)
1348 class FocusProxy:
1349 """Proxy over child widgets and operate on the focused widget"""
1351 def __init__(self, *widgets):
1352 self.widgets = widgets
1353 self.overrides = {}
1355 def override(self, name, widgets):
1356 self.overrides[name] = widgets
1358 def focus(self, name):
1359 """Return the currently focused widget"""
1360 widgets = self.overrides.get(name, self.widgets)
1361 # The parent must be the parent of all the proxied widgets
1362 parent = widgets[0]
1363 # The first widget is used as a fallback
1364 fallback = widgets[1]
1365 # We ignore the parent when delegating to child widgets
1366 widgets = widgets[1:]
1368 focus = parent.focusWidget()
1369 if focus not in widgets:
1370 focus = fallback
1371 return focus
1373 def __getattr__(self, name):
1374 """Return a callback that calls a common child method"""
1376 def callback():
1377 focus = self.focus(name)
1378 func = getattr(focus, name, None)
1379 if func:
1380 func()
1382 return callback
1384 def delete(self):
1385 """Specialized delete() to deal with QLineEdit vs QTextEdit"""
1386 focus = self.focus('delete')
1387 if hasattr(focus, 'del_'):
1388 focus.del_()
1389 elif hasattr(focus, 'textCursor'):
1390 focus.textCursor().deleteChar()
1393 def show_dock(dockwidget):
1394 dockwidget.raise_()
1395 dockwidget.widget().setFocus()
1398 def focus_dock(dockwidget):
1399 if get(dockwidget.toggleViewAction()):
1400 show_dock(dockwidget)
1401 else:
1402 dockwidget.toggleViewAction().trigger()
1405 def _install_config_actions(context, menu, names_and_shortcuts):
1406 """Install .gitconfig-defined actions"""
1407 if not names_and_shortcuts:
1408 return
1409 menu.addSeparator()
1410 cache = {}
1411 for name, shortcut in names_and_shortcuts:
1412 sub_menu, action_name = build_menus(name, menu, cache)
1413 callback = cmds.run(cmds.RunConfigAction, context, name)
1414 menu_action = sub_menu.addAction(action_name, callback)
1415 if shortcut:
1416 menu_action.setShortcut(shortcut)
1419 def build_menus(name, menu, cache):
1420 """Create a chain of QMenu entries parented under a root QMenu
1422 A name of "a/b/c" create a menu chain of menu -> QMenu("a") -> QMenu("b")
1423 and returns a tuple of (QMenu("b"), "c").
1425 :param name: The full entry path, ex: "a/b/c" where "a/b" is the menu chain.
1426 :param menu: The root menu under which to create the menu chain.
1427 :param cache: A dict cache of previously created menus to avoid duplicates.
1430 # NOTE: utils.split() and friends are used instead of os.path.split() because
1431 # slash '/' is the only supported "<menu>/name" separator. Use of os.path.split()
1432 # would introduce differences in behavior across platforms.
1434 # If the menu_path is empty then no parent menus need to be created.
1435 # The action will be added to the root menu.
1436 menu_path, text = utils.split(utils.normalize_slash(name))
1438 if not menu_path:
1439 return (menu, text)
1440 # When menu_path contains ex: "a/b" we will create two menus: "a" and "b".
1441 # The root menu is the parent of "a" and "a" is the parent of "b".
1442 # The menu returned to the caller is "b".
1444 # Loop over the individual menu basenames alongside the full subpath returned by
1445 # pathset(). The subpath is a cache key for finding previously created menus.
1446 menu_names = utils.splitpath(menu_path) # ['a', 'b']
1447 menu_pathset = utils.pathset(menu_path) # ['a', 'a/b']
1448 for menu_name, menu_id in zip(menu_names, menu_pathset):
1449 try:
1450 menu = cache[menu_id]
1451 except KeyError:
1452 menu = cache[menu_id] = menu.addMenu(menu_name)
1454 return (menu, text)