submodules: add a dialog for adding new submodules
[git-cola.git] / cola / widgets / main.py
blob58917c2ec978ff2503343b21f683ca68941e01dc
1 """This view provides the main git-cola user interface.
2 """
3 from __future__ import division, absolute_import, unicode_literals
4 import os
5 from functools import partial
7 from qtpy import QtCore
8 from qtpy import QtGui
9 from qtpy import QtWidgets
10 from qtpy.QtCore import Qt
11 from qtpy.QtCore import Signal
13 from ..compat import uchr
14 from ..compat import WIN32
15 from ..i18n import N_
16 from ..interaction import Interaction
17 from ..models import prefs
18 from ..qtutils import get
19 from ..settings import Settings
20 from .. import cmds
21 from .. import core
22 from .. import guicmds
23 from .. import git
24 from .. import gitcmds
25 from .. import hotkeys
26 from .. import icons
27 from .. import qtutils
28 from .. import resources
29 from .. import utils
30 from .. import version
31 from . import about
32 from . import action
33 from . import archive
34 from . import bookmarks
35 from . import branch
36 from . import submodules
37 from . import browse
38 from . import cfgactions
39 from . import clone
40 from . import commitmsg
41 from . import compare
42 from . import createbranch
43 from . import createtag
44 from . import dag
45 from . import defs
46 from . import diff
47 from . import finder
48 from . import editremotes
49 from . import grep
50 from . import log
51 from . import merge
52 from . import patch
53 from . import prefs as prefs_widget
54 from . import recent
55 from . import remote
56 from . import search
57 from . import standard
58 from . import status
59 from . import stash
60 from . import toolbar
63 class MainView(standard.MainWindow):
64 config_actions_changed = Signal(object)
65 updated = Signal()
67 def __init__(self, context, parent=None, settings=None):
68 standard.MainWindow.__init__(self, parent)
69 self.setAttribute(Qt.WA_DeleteOnClose)
71 self.context = context
72 self.git = context.git
73 self.dag = None
74 self.model = model = context.model
75 self.settings = settings
76 self.prefs_model = prefs_model = prefs.PreferencesModel(context)
77 self.toolbar_state = toolbar.ToolBarState(context, self)
79 # The widget version is used by import/export_state().
80 # Change this whenever dockwidgets are removed.
81 self.widget_version = 2
83 create_dock = qtutils.create_dock
84 cfg = context.cfg
85 self.browser_dockable = cfg.get('cola.browserdockable')
86 if self.browser_dockable:
87 browser = browse.worktree_browser(
88 context, parent=self, show=False, update=False
90 self.browserdock = create_dock(N_('Browser'), self, widget=browser)
92 # "Actions" widget
93 self.actionsdock = create_dock(
94 N_('Actions'), self, widget=action.ActionButtons(context, self)
96 qtutils.hide_dock(self.actionsdock)
98 # "Repository Status" widget
99 self.statusdock = create_dock(
100 N_('Status'),
101 self,
102 fn=lambda dock: status.StatusWidget(context, dock.titleBarWidget(), dock),
104 self.statuswidget = self.statusdock.widget()
106 # "Switch Repository" widgets
107 self.bookmarksdock = create_dock(
108 N_('Favorites'), self, fn=lambda dock: bookmarks.bookmark(context, dock)
110 bookmarkswidget = self.bookmarksdock.widget()
111 qtutils.hide_dock(self.bookmarksdock)
113 self.recentdock = create_dock(
114 N_('Recent'), self, fn=lambda dock: bookmarks.recent(context, dock)
116 recentwidget = self.recentdock.widget()
117 qtutils.hide_dock(self.recentdock)
118 bookmarkswidget.connect_to(recentwidget)
120 # "Branch" widgets
121 self.branchdock = create_dock(
122 N_('Branches'), self, fn=partial(branch.BranchesWidget, context)
124 self.branchwidget = self.branchdock.widget()
125 titlebar = self.branchdock.titleBarWidget()
126 titlebar.add_corner_widget(self.branchwidget.filter_button)
127 titlebar.add_corner_widget(self.branchwidget.sort_order_button)
129 # "Submodule" widgets
130 self.submodulesdock = create_dock(
131 N_('Submodules'), self, fn=partial(submodules.SubmodulesWidget, context)
133 self.submoduleswidget = self.submodulesdock.widget()
135 # "Commit Message Editor" widget
136 self.position_label = QtWidgets.QLabel()
137 self.position_label.setAlignment(Qt.AlignCenter)
138 font = qtutils.default_monospace_font()
139 font.setPointSize(int(font.pointSize() * 0.8))
140 self.position_label.setFont(font)
142 # make the position label fixed size to avoid layout issues
143 fm = self.position_label.fontMetrics()
144 width = fm.width('99:999') + defs.spacing
145 self.position_label.setMinimumWidth(width)
147 editor = commitmsg.CommitMessageEditor(context, self)
148 self.commiteditor = editor
149 self.commitdock = create_dock(N_('Commit'), self, widget=editor)
150 titlebar = self.commitdock.titleBarWidget()
151 titlebar.add_corner_widget(self.position_label)
153 # "Console" widget
154 self.logwidget = log.LogWidget(context)
155 self.logdock = create_dock(N_('Console'), self, widget=self.logwidget)
156 qtutils.hide_dock(self.logdock)
158 # "Diff Viewer" widget
159 self.diffdock = create_dock(
160 N_('Diff'), self, fn=lambda dock: diff.Viewer(context, parent=dock)
162 self.diffviewer = self.diffdock.widget()
163 self.diffviewer.set_diff_type(self.model.diff_type)
165 self.diffeditor = self.diffviewer.text
166 titlebar = self.diffdock.titleBarWidget()
167 titlebar.add_corner_widget(self.diffviewer.options)
169 # All Actions
170 add_action = qtutils.add_action
171 add_action_bool = qtutils.add_action_bool
173 self.commit_amend_action = add_action_bool(
174 self,
175 N_('Amend Last Commit'),
176 partial(cmds.do, cmds.AmendMode, context),
177 False,
179 self.commit_amend_action.setShortcut(hotkeys.AMEND)
180 self.commit_amend_action.setShortcutContext(Qt.WidgetShortcut)
182 self.unstage_all_action = add_action(
183 self, N_('Unstage All'), cmds.run(cmds.UnstageAll, context)
185 self.unstage_all_action.setIcon(icons.remove())
187 self.unstage_selected_action = add_action(
188 self, N_('Unstage From Commit'), cmds.run(cmds.UnstageSelected, context)
190 self.unstage_selected_action.setIcon(icons.remove())
192 self.show_diffstat_action = add_action(
193 self, N_('Diffstat'), self.statuswidget.select_header, hotkeys.DIFFSTAT
196 self.stage_modified_action = add_action(
197 self,
198 N_('Stage Changed Files To Commit'),
199 cmds.run(cmds.StageModified, context),
200 hotkeys.STAGE_MODIFIED,
202 self.stage_modified_action.setIcon(icons.add())
204 self.stage_untracked_action = add_action(
205 self,
206 N_('Stage All Untracked'),
207 cmds.run(cmds.StageUntracked, context),
208 hotkeys.STAGE_UNTRACKED,
210 self.stage_untracked_action.setIcon(icons.add())
212 self.apply_patches_action = add_action(
213 self, N_('Apply Patches...'), partial(patch.apply_patches, context)
216 self.export_patches_action = add_action(
217 self,
218 N_('Export Patches...'),
219 partial(guicmds.export_patches, context),
220 hotkeys.EXPORT,
223 self.new_repository_action = add_action(
224 self, N_('New Repository...'), partial(guicmds.open_new_repo, context)
226 self.new_repository_action.setIcon(icons.new())
228 self.new_bare_repository_action = add_action(
229 self, N_('New Bare Repository...'), partial(guicmds.new_bare_repo, context)
231 self.new_bare_repository_action.setIcon(icons.new())
233 prefs_fn = partial(
234 prefs_widget.preferences, context, parent=self, model=prefs_model
236 self.preferences_action = add_action(
237 self, N_('Preferences'), prefs_fn, QtGui.QKeySequence.Preferences
240 self.edit_remotes_action = add_action(
241 self, N_('Edit Remotes...'), partial(editremotes.editor, context)
244 self.rescan_action = add_action(
245 self,
246 cmds.Refresh.name(),
247 cmds.run(cmds.Refresh, context),
248 *hotkeys.REFRESH_HOTKEYS
250 self.rescan_action.setIcon(icons.sync())
252 self.find_files_action = add_action(
253 self,
254 N_('Find Files'),
255 partial(finder.finder, context),
256 hotkeys.FINDER,
257 hotkeys.FINDER_SECONDARY,
259 self.find_files_action.setIcon(icons.zoom_in())
261 self.browse_recently_modified_action = add_action(
262 self,
263 N_('Recently Modified Files...'),
264 partial(recent.browse_recent_files, context),
265 hotkeys.EDIT_SECONDARY,
268 self.cherry_pick_action = add_action(
269 self,
270 N_('Cherry-Pick...'),
271 partial(guicmds.cherry_pick, context),
272 hotkeys.CHERRY_PICK,
275 self.load_commitmsg_action = add_action(
276 self, N_('Load Commit Message...'), partial(guicmds.load_commitmsg, context)
279 self.prepare_commitmsg_hook_action = add_action(
280 self,
281 N_('Prepare Commit Message'),
282 cmds.run(cmds.PrepareCommitMessageHook, context),
283 hotkeys.PREPARE_COMMIT_MESSAGE,
286 self.save_tarball_action = add_action(
287 self, N_('Save As Tarball/Zip...'), partial(archive.save_archive, context)
290 self.quit_action = add_action(self, N_('Quit'), self.close, hotkeys.QUIT)
292 self.grep_action = add_action(
293 self, N_('Grep'), partial(grep.grep, context), hotkeys.GREP
296 self.merge_local_action = add_action(
297 self, N_('Merge...'), partial(merge.local_merge, context), hotkeys.MERGE
300 self.merge_abort_action = add_action(
301 self, N_('Abort Merge...'), cmds.run(cmds.AbortMerge, context)
304 self.update_submodules_action = add_action(
305 self,
306 N_('Update All Submodules...'),
307 cmds.run(cmds.SubmodulesUpdate, context),
310 self.add_submodule_action = add_action(
311 self, N_('Add Submodule...'),
312 partial(submodules.add_submodule, context, parent=self)
315 self.fetch_action = add_action(
316 self, N_('Fetch...'), partial(remote.fetch, context), hotkeys.FETCH
318 self.push_action = add_action(
319 self, N_('Push...'), partial(remote.push, context), hotkeys.PUSH
321 self.pull_action = add_action(
322 self, N_('Pull...'), partial(remote.pull, context), hotkeys.PULL
325 self.open_repo_action = add_action(
326 self, N_('Open...'), partial(guicmds.open_repo, context), hotkeys.OPEN
328 self.open_repo_action.setIcon(icons.folder())
330 self.open_repo_new_action = add_action(
331 self,
332 N_('Open in New Window...'),
333 partial(guicmds.open_repo_in_new_window, context),
335 self.open_repo_new_action.setIcon(icons.folder())
337 self.stash_action = add_action(
338 self, N_('Stash...'), partial(stash.view, context), hotkeys.STASH
341 self.reset_branch_head_action = add_action(
342 self, N_('Reset Branch Head'), partial(guicmds.reset_branch_head, context)
345 self.reset_worktree_action = add_action(
346 self, N_('Reset Worktree'), partial(guicmds.reset_worktree, context)
349 self.clone_repo_action = add_action(
350 self, N_('Clone...'), partial(clone.clone, context, settings=settings)
352 self.clone_repo_action.setIcon(icons.repo())
354 self.help_docs_action = add_action(
355 self,
356 N_('Documentation'),
357 resources.show_html_docs,
358 QtGui.QKeySequence.HelpContents,
361 self.help_shortcuts_action = add_action(
362 self, N_('Keyboard Shortcuts'), about.show_shortcuts, hotkeys.QUESTION
365 self.visualize_current_action = add_action(
366 self,
367 N_('Visualize Current Branch...'),
368 cmds.run(cmds.VisualizeCurrent, context),
370 self.visualize_all_action = add_action(
371 self, N_('Visualize All Branches...'), cmds.run(cmds.VisualizeAll, context)
373 self.search_commits_action = add_action(
374 self, N_('Search...'), partial(search.search, context)
377 self.browse_branch_action = add_action(
378 self,
379 N_('Browse Current Branch...'),
380 partial(guicmds.browse_current, context),
382 self.browse_other_branch_action = add_action(
383 self, N_('Browse Other Branch...'), partial(guicmds.browse_other, context)
385 self.load_commitmsg_template_action = add_action(
386 self,
387 N_('Get Commit Message Template'),
388 cmds.run(cmds.LoadCommitMessageFromTemplate, context),
390 self.help_about_action = add_action(
391 self, N_('About'), partial(about.about_dialog, context)
394 self.diff_expression_action = add_action(
395 self, N_('Expression...'), partial(guicmds.diff_expression, context)
397 self.branch_compare_action = add_action(
398 self, N_('Branches...'), partial(compare.compare_branches, context)
401 self.create_tag_action = add_action(
402 self,
403 N_('Create Tag...'),
404 partial(createtag.create_tag, context, settings=settings),
407 self.create_branch_action = add_action(
408 self,
409 N_('Create...'),
410 partial(createbranch.create_new_branch, context, settings=settings),
411 hotkeys.BRANCH,
413 self.create_branch_action.setIcon(icons.branch())
415 self.delete_branch_action = add_action(
416 self, N_('Delete...'), partial(guicmds.delete_branch, context)
419 self.delete_remote_branch_action = add_action(
420 self,
421 N_('Delete Remote Branch...'),
422 partial(guicmds.delete_remote_branch, context),
425 self.rename_branch_action = add_action(
426 self, N_('Rename Branch...'), partial(guicmds.rename_branch, context)
429 self.checkout_branch_action = add_action(
430 self,
431 N_('Checkout...'),
432 partial(guicmds.checkout_branch, context),
433 hotkeys.CHECKOUT,
435 self.branch_review_action = add_action(
436 self, N_('Review...'), partial(guicmds.review_branch, context)
439 self.browse_action = add_action(
440 self, N_('File Browser...'), partial(browse.worktree_browser, context)
442 self.browse_action.setIcon(icons.cola())
444 self.dag_action = add_action(self, N_('DAG...'), self.git_dag)
445 self.dag_action.setIcon(icons.cola())
447 self.rebase_start_action = add_action(
448 self,
449 N_('Start Interactive Rebase...'),
450 cmds.run(cmds.Rebase, context),
451 hotkeys.REBASE_START_AND_CONTINUE,
454 self.rebase_edit_todo_action = add_action(
455 self, N_('Edit...'), cmds.run(cmds.RebaseEditTodo, context)
458 self.rebase_continue_action = add_action(
459 self,
460 N_('Continue'),
461 cmds.run(cmds.RebaseContinue, context),
462 hotkeys.REBASE_START_AND_CONTINUE,
465 self.rebase_skip_action = add_action(
466 self, N_('Skip Current Patch'), cmds.run(cmds.RebaseSkip, context)
469 self.rebase_abort_action = add_action(
470 self, N_('Abort'), cmds.run(cmds.RebaseAbort, context)
473 # For "Start Rebase" only, reverse the first argument to setEnabled()
474 # so that we can operate on it as a group.
475 # We can do this because can_rebase == not is_rebasing
476 self.rebase_start_action_proxy = utils.Proxy(
477 self.rebase_start_action,
478 setEnabled=lambda x: self.rebase_start_action.setEnabled(not x),
481 self.rebase_group = utils.Group(
482 self.rebase_start_action_proxy,
483 self.rebase_edit_todo_action,
484 self.rebase_continue_action,
485 self.rebase_skip_action,
486 self.rebase_abort_action,
489 self.annex_init_action = qtutils.add_action(
490 self, N_('Initialize Git Annex'), cmds.run(cmds.AnnexInit, context)
493 self.lfs_init_action = qtutils.add_action(
494 self, N_('Initialize Git LFS'), cmds.run(cmds.LFSInstall, context)
497 self.lock_layout_action = add_action_bool(
498 self, N_('Lock Layout'), self.set_lock_layout, False
501 self.reset_layout_action = add_action(
502 self, N_('Reset Layout'), self.reset_layout
505 # Create the application menu
506 self.menubar = QtWidgets.QMenuBar(self)
507 self.setMenuBar(self.menubar)
509 # File Menu
510 add_menu = qtutils.add_menu
511 self.file_menu = add_menu(N_('&File'), self.menubar)
512 # File->Open Recent menu
513 self.open_recent_menu = self.file_menu.addMenu(N_('Open Recent'))
514 self.open_recent_menu.setIcon(icons.folder())
515 self.file_menu.addAction(self.open_repo_action)
516 self.file_menu.addAction(self.open_repo_new_action)
517 self.file_menu.addSeparator()
518 self.file_menu.addAction(self.new_repository_action)
519 self.file_menu.addAction(self.new_bare_repository_action)
520 self.file_menu.addAction(self.clone_repo_action)
521 self.file_menu.addSeparator()
522 self.file_menu.addAction(self.rescan_action)
523 self.file_menu.addAction(self.find_files_action)
524 self.file_menu.addAction(self.edit_remotes_action)
525 self.file_menu.addAction(self.browse_recently_modified_action)
526 self.file_menu.addSeparator()
527 self.file_menu.addAction(self.apply_patches_action)
528 self.file_menu.addAction(self.export_patches_action)
529 self.file_menu.addAction(self.save_tarball_action)
531 # Git Annex / Git LFS
532 annex = core.find_executable('git-annex')
533 lfs = core.find_executable('git-lfs')
534 if annex or lfs:
535 self.file_menu.addSeparator()
536 if annex:
537 self.file_menu.addAction(self.annex_init_action)
538 if lfs:
539 self.file_menu.addAction(self.lfs_init_action)
541 self.file_menu.addSeparator()
542 self.file_menu.addAction(self.preferences_action)
543 self.file_menu.addAction(self.quit_action)
545 # Edit Menu
546 self.edit_proxy = edit_proxy = FocusProxy(
547 editor, editor.summary, editor.description
550 copy_widgets = (
551 self,
552 editor.summary,
553 editor.description,
554 self.diffeditor,
555 bookmarkswidget.tree,
556 recentwidget.tree,
558 edit_proxy.override('copy', copy_widgets)
559 edit_proxy.override('selectAll', copy_widgets)
561 edit_menu = self.edit_menu = add_menu(N_('&Edit'), self.menubar)
562 add_action(edit_menu, N_('Undo'), edit_proxy.undo, hotkeys.UNDO)
563 add_action(edit_menu, N_('Redo'), edit_proxy.redo, hotkeys.REDO)
564 edit_menu.addSeparator()
565 add_action(edit_menu, N_('Cut'), edit_proxy.cut, hotkeys.CUT)
566 add_action(edit_menu, N_('Copy'), edit_proxy.copy, hotkeys.COPY)
567 add_action(edit_menu, N_('Paste'), edit_proxy.paste, hotkeys.PASTE)
568 add_action(edit_menu, N_('Delete'), edit_proxy.delete, hotkeys.DELETE)
569 edit_menu.addSeparator()
570 add_action(
571 edit_menu, N_('Select All'), edit_proxy.selectAll, hotkeys.SELECT_ALL
573 edit_menu.addSeparator()
575 commitmsg.add_menu_actions(edit_menu, self.commiteditor.menu_actions)
577 # Actions menu
578 self.actions_menu = add_menu(N_('Actions'), self.menubar)
579 self.actions_menu.addAction(self.fetch_action)
580 self.actions_menu.addAction(self.push_action)
581 self.actions_menu.addAction(self.pull_action)
582 self.actions_menu.addAction(self.stash_action)
583 self.actions_menu.addSeparator()
584 self.actions_menu.addAction(self.create_tag_action)
585 self.actions_menu.addAction(self.cherry_pick_action)
586 self.actions_menu.addAction(self.merge_local_action)
587 self.actions_menu.addAction(self.merge_abort_action)
588 self.actions_menu.addSeparator()
589 self.actions_menu.addAction(self.update_submodules_action)
590 self.actions_menu.addAction(self.add_submodule_action)
591 self.actions_menu.addSeparator()
592 self.actions_reset_menu = self.actions_menu.addMenu(N_('Reset'))
593 self.actions_reset_menu.addAction(self.reset_branch_head_action)
594 self.actions_reset_menu.addAction(self.reset_worktree_action)
595 self.actions_menu.addSeparator()
596 self.actions_menu.addAction(self.grep_action)
597 self.actions_menu.addAction(self.search_commits_action)
599 # Commit Menu
600 self.commit_menu = add_menu(N_('Commit@@verb'), self.menubar)
601 self.commit_menu.setTitle(N_('Commit@@verb'))
602 self.commit_menu.addAction(self.commiteditor.commit_action)
603 self.commit_menu.addAction(self.commit_amend_action)
604 self.commit_menu.addSeparator()
605 self.commit_menu.addAction(self.stage_modified_action)
606 self.commit_menu.addAction(self.stage_untracked_action)
607 self.commit_menu.addSeparator()
608 self.commit_menu.addAction(self.unstage_all_action)
609 self.commit_menu.addAction(self.unstage_selected_action)
610 self.commit_menu.addSeparator()
611 self.commit_menu.addAction(self.load_commitmsg_action)
612 self.commit_menu.addAction(self.load_commitmsg_template_action)
613 self.commit_menu.addAction(self.prepare_commitmsg_hook_action)
615 # Diff Menu
616 self.diff_menu = add_menu(N_('Diff'), self.menubar)
617 self.diff_menu.addAction(self.diff_expression_action)
618 self.diff_menu.addAction(self.branch_compare_action)
619 self.diff_menu.addSeparator()
620 self.diff_menu.addAction(self.show_diffstat_action)
622 # Branch Menu
623 self.branch_menu = add_menu(N_('Branch'), self.menubar)
624 self.branch_menu.addAction(self.branch_review_action)
625 self.branch_menu.addSeparator()
626 self.branch_menu.addAction(self.create_branch_action)
627 self.branch_menu.addAction(self.checkout_branch_action)
628 self.branch_menu.addAction(self.delete_branch_action)
629 self.branch_menu.addAction(self.delete_remote_branch_action)
630 self.branch_menu.addAction(self.rename_branch_action)
631 self.branch_menu.addSeparator()
632 self.branch_menu.addAction(self.browse_branch_action)
633 self.branch_menu.addAction(self.browse_other_branch_action)
634 self.branch_menu.addSeparator()
635 self.branch_menu.addAction(self.visualize_current_action)
636 self.branch_menu.addAction(self.visualize_all_action)
638 # Rebase menu
639 self.rebase_menu = add_menu(N_('Rebase'), self.actions_menu)
640 self.rebase_menu.addAction(self.rebase_start_action)
641 self.rebase_menu.addAction(self.rebase_edit_todo_action)
642 self.rebase_menu.addSeparator()
643 self.rebase_menu.addAction(self.rebase_continue_action)
644 self.rebase_menu.addAction(self.rebase_skip_action)
645 self.rebase_menu.addSeparator()
646 self.rebase_menu.addAction(self.rebase_abort_action)
648 # View Menu
649 self.view_menu = add_menu(N_('View'), self.menubar)
650 # pylint: disable=no-member
651 self.view_menu.aboutToShow.connect(lambda: self.build_view_menu(self.view_menu))
652 self.setup_dockwidget_view_menu()
653 if utils.is_darwin():
654 # TODO or self.menubar.setNativeMenuBar(False)
655 # Since native OSX menu doesn't show empty entries
656 self.build_view_menu(self.view_menu)
658 # Help Menu
659 self.help_menu = add_menu(N_('Help'), self.menubar)
660 self.help_menu.addAction(self.help_docs_action)
661 self.help_menu.addAction(self.help_shortcuts_action)
662 self.help_menu.addAction(self.help_about_action)
664 # Arrange dock widgets
665 bottom = Qt.BottomDockWidgetArea
666 top = Qt.TopDockWidgetArea
668 self.addDockWidget(top, self.statusdock)
669 self.addDockWidget(top, self.commitdock)
670 if self.browser_dockable:
671 self.addDockWidget(top, self.browserdock)
672 self.tabifyDockWidget(self.browserdock, self.commitdock)
674 self.addDockWidget(top, self.branchdock)
675 self.addDockWidget(top, self.submodulesdock)
676 self.addDockWidget(top, self.bookmarksdock)
677 self.addDockWidget(top, self.recentdock)
679 self.tabifyDockWidget(self.branchdock, self.submodulesdock)
680 self.tabifyDockWidget(self.submodulesdock, self.bookmarksdock)
681 self.tabifyDockWidget(self.bookmarksdock, self.recentdock)
682 self.branchdock.raise_()
684 self.addDockWidget(bottom, self.diffdock)
685 self.addDockWidget(bottom, self.actionsdock)
686 self.addDockWidget(bottom, self.logdock)
687 self.tabifyDockWidget(self.actionsdock, self.logdock)
689 # Listen for model notifications
690 model.add_observer(model.message_updated, self.updated.emit)
691 model.add_observer(model.message_mode_changed, lambda mode: self.updated.emit())
693 prefs_model.add_observer(
694 prefs_model.message_config_updated, self._config_updated
697 # Set a default value
698 self.show_cursor_position(1, 0)
700 self.commit_menu.aboutToShow.connect(self.update_menu_actions)
701 self.open_recent_menu.aboutToShow.connect(self.build_recent_menu)
702 self.commiteditor.cursor_changed.connect(self.show_cursor_position)
704 self.diffeditor.options_changed.connect(self.statuswidget.refresh)
705 self.diffeditor.up.connect(self.statuswidget.move_up)
706 self.diffeditor.down.connect(self.statuswidget.move_down)
708 self.commiteditor.up.connect(self.statuswidget.move_up)
709 self.commiteditor.down.connect(self.statuswidget.move_down)
711 self.updated.connect(self.refresh, type=Qt.QueuedConnection)
713 self.config_actions_changed.connect(
714 self._install_config_actions, type=Qt.QueuedConnection
716 self.init_state(settings, self.set_initial_size)
718 # Route command output here
719 Interaction.log_status = self.logwidget.log_status
720 Interaction.log = self.logwidget.log
721 # Focus the status widget; this must be deferred
722 QtCore.QTimer.singleShot(0, self.initialize)
724 def initialize(self):
725 context = self.context
726 git_version = version.git_version_str(context)
727 if git_version:
728 ok = True
729 Interaction.log(
730 git_version + '\n' + N_('git cola version %s') % version.version()
732 else:
733 ok = False
734 error_msg = N_('error: unable to execute git')
735 Interaction.log(error_msg)
737 if ok:
738 self.statuswidget.setFocus()
739 else:
740 title = N_('error: unable to execute git')
741 msg = title
742 details = ''
743 if WIN32:
744 details = git.win32_git_error_hint()
745 Interaction.critical(title, message=msg, details=details)
746 self.context.app.exit(2)
748 def set_initial_size(self):
749 # Default size; this is thrown out when save/restore is used
750 width, height = qtutils.desktop_size()
751 self.resize((width * 3) // 4, height)
752 self.statuswidget.set_initial_size()
753 self.commiteditor.set_initial_size()
755 def set_filter(self, txt):
756 self.statuswidget.set_filter(txt)
758 # Qt overrides
759 def closeEvent(self, event):
760 """Save state in the settings"""
761 commit_msg = self.commiteditor.commit_message(raw=True)
762 self.model.save_commitmsg(msg=commit_msg)
763 standard.MainWindow.closeEvent(self, event)
765 def create_view_menu(self):
766 menu = qtutils.create_menu(N_('View'), self)
767 self.build_view_menu(menu)
768 return menu
770 def build_view_menu(self, menu):
771 menu.clear()
772 menu.addAction(self.browse_action)
773 menu.addAction(self.dag_action)
774 menu.addSeparator()
776 popup_menu = self.createPopupMenu()
777 for menu_action in popup_menu.actions():
778 menu_action.setParent(menu)
779 menu.addAction(menu_action)
781 menu.addSeparator()
782 context = self.context
783 menu_action = menu.addAction(
784 N_('Add Toolbar'), partial(toolbar.add_toolbar, context, self)
786 menu_action.setIcon(icons.add())
788 dockwidgets = [
789 self.logdock,
790 self.commitdock,
791 self.statusdock,
792 self.diffdock,
793 self.actionsdock,
794 self.bookmarksdock,
795 self.recentdock,
796 self.branchdock,
797 self.submodulesdock,
799 if self.browser_dockable:
800 dockwidgets.append(self.browserdock)
802 for dockwidget in dockwidgets:
803 # Associate the action with the shortcut
804 toggleview = dockwidget.toggleViewAction()
805 menu.addAction(toggleview)
807 menu.addSeparator()
808 menu.addAction(self.lock_layout_action)
809 menu.addAction(self.reset_layout_action)
811 return menu
813 def contextMenuEvent(self, event):
814 menu = self.create_view_menu()
815 menu.exec_(event.globalPos())
817 def build_recent_menu(self):
818 settings = Settings()
819 settings.load()
820 cmd = cmds.OpenRepo
821 context = self.context
822 menu = self.open_recent_menu
823 menu.clear()
824 worktree = self.git.worktree()
825 for entry in settings.recent:
826 directory = entry['path']
827 if directory == worktree:
828 # Omit the current worktree from the "Open Recent" menu.
829 continue
830 name = entry['name']
831 text = '%s %s %s' % (name, uchr(0x2192), directory)
832 menu.addAction(text, cmds.run(cmd, context, directory))
834 # Accessors
835 mode = property(lambda self: self.model.mode)
837 def _config_updated(self, _source, config, value):
838 if config == prefs.FONTDIFF:
839 # The diff font
840 font = QtGui.QFont()
841 if not font.fromString(value):
842 return
843 self.logwidget.setFont(font)
844 self.diffeditor.setFont(font)
845 self.commiteditor.setFont(font)
847 elif config == prefs.TABWIDTH:
848 # This can be set locally or globally, so we have to use the
849 # effective value otherwise we'll update when we shouldn't.
850 # For example, if this value is overridden locally, and the
851 # global value is tweaked, we should not update.
852 value = prefs.tabwidth(self.context)
853 self.diffeditor.set_tabwidth(value)
854 self.commiteditor.set_tabwidth(value)
856 elif config == prefs.EXPANDTAB:
857 self.commiteditor.set_expandtab(value)
859 elif config == prefs.LINEBREAK:
860 # enables automatic line breaks
861 self.commiteditor.set_linebreak(value)
863 elif config == prefs.SORT_BOOKMARKS:
864 self.bookmarksdock.widget().reload_bookmarks()
866 elif config == prefs.TEXTWIDTH:
867 # Use the effective value for the same reason as tabwidth.
868 value = prefs.textwidth(self.context)
869 self.commiteditor.set_textwidth(value)
871 elif config == prefs.SHOW_PATH:
872 # the path in the window title was toggled
873 self.refresh_window_title()
875 def start(self, context):
876 """Do the expensive "get_config_actions()" call in the background"""
877 # Install .git-config-defined actions
878 task = qtutils.SimpleTask(self, self.get_config_actions)
879 context.runtask.start(task)
881 def get_config_actions(self):
882 actions = cfgactions.get_config_actions(self.context)
883 self.config_actions_changed.emit(actions)
885 def _install_config_actions(self, names_and_shortcuts):
886 """Install .gitconfig-defined actions"""
887 if not names_and_shortcuts:
888 return
889 context = self.context
890 menu = self.actions_menu
891 menu.addSeparator()
892 for (name, shortcut) in names_and_shortcuts:
893 callback = cmds.run(cmds.RunConfigAction, context, name)
894 menu_action = menu.addAction(name, callback)
895 if shortcut:
896 menu_action.setShortcut(shortcut)
898 def refresh(self):
899 """Update the title with the current branch and directory name."""
900 curbranch = self.model.currentbranch
901 curdir = core.getcwd()
902 is_merging = self.model.is_merging
903 is_rebasing = self.model.is_rebasing
905 msg = N_('Repository: %s') % curdir
906 msg += '\n'
907 msg += N_('Branch: %s') % curbranch
909 if is_rebasing:
910 msg += '\n\n'
911 msg += N_(
912 'This repository is currently being rebased.\n'
913 'Resolve conflicts, commit changes, and run:\n'
914 ' Rebase > Continue'
917 elif is_merging:
918 msg += '\n\n'
919 msg += N_(
920 'This repository is in the middle of a merge.\n'
921 'Resolve conflicts and commit changes.'
924 self.refresh_window_title()
926 if self.mode == self.model.mode_amend:
927 self.commit_amend_action.setChecked(True)
928 else:
929 self.commit_amend_action.setChecked(False)
931 self.commitdock.setToolTip(msg)
932 self.commiteditor.set_mode(self.mode)
933 self.update_actions()
935 def refresh_window_title(self):
936 """Refresh the window title when state changes"""
937 alerts = []
939 project = self.model.project
940 curbranch = self.model.currentbranch
941 is_merging = self.model.is_merging
942 is_rebasing = self.model.is_rebasing
943 prefix = uchr(0xAB)
944 suffix = uchr(0xBB)
946 if is_rebasing:
947 alerts.append(N_('Rebasing'))
948 elif is_merging:
949 alerts.append(N_('Merging'))
951 if self.mode == self.model.mode_amend:
952 alerts.append(N_('Amending'))
954 if alerts:
955 alert_text = (prefix + ' %s ' + suffix + ' ') % ', '.join(alerts)
956 else:
957 alert_text = ''
959 if self.model.cfg.get(prefs.SHOW_PATH, True):
960 path_text = self.git.worktree()
961 else:
962 path_text = ''
964 title = '%s: %s %s%s' % (project, curbranch, alert_text, path_text)
965 self.setWindowTitle(title)
967 def update_actions(self):
968 is_rebasing = self.model.is_rebasing
969 self.rebase_group.setEnabled(is_rebasing)
971 enabled = not self.model.is_empty_repository()
972 self.rename_branch_action.setEnabled(enabled)
973 self.delete_branch_action.setEnabled(enabled)
975 self.annex_init_action.setEnabled(not self.model.annex)
976 self.lfs_init_action.setEnabled(not self.model.lfs)
978 def update_menu_actions(self):
979 # Enable the Prepare Commit Message action if the hook exists
980 hook = gitcmds.prepare_commit_message_hook(self.context)
981 enabled = os.path.exists(hook)
982 self.prepare_commitmsg_hook_action.setEnabled(enabled)
984 def export_state(self):
985 state = standard.MainWindow.export_state(self)
986 show_status_filter = self.statuswidget.filter_widget.isVisible()
987 state['show_status_filter'] = show_status_filter
988 state['toolbars'] = self.toolbar_state.export_state()
989 state['ref_sort'] = self.model.ref_sort
990 self.diffviewer.export_state(state)
992 return state
994 def apply_state(self, state):
995 """Imports data for save/restore"""
996 base_ok = standard.MainWindow.apply_state(self, state)
997 lock_layout = state.get('lock_layout', False)
998 self.lock_layout_action.setChecked(lock_layout)
1000 show_status_filter = state.get('show_status_filter', False)
1001 self.statuswidget.filter_widget.setVisible(show_status_filter)
1003 toolbars = state.get('toolbars', [])
1004 self.toolbar_state.apply_state(toolbars)
1006 sort_key = state.get('ref_sort', 0)
1007 self.model.set_ref_sort(sort_key)
1009 diff_ok = self.diffviewer.apply_state(state)
1010 return base_ok and diff_ok
1012 def setup_dockwidget_view_menu(self):
1013 # Hotkeys for toggling the dock widgets
1014 if utils.is_darwin():
1015 optkey = 'Meta'
1016 else:
1017 optkey = 'Ctrl'
1018 dockwidgets = (
1019 (optkey + '+0', self.logdock),
1020 (optkey + '+1', self.commitdock),
1021 (optkey + '+2', self.statusdock),
1022 (optkey + '+3', self.diffdock),
1023 (optkey + '+4', self.actionsdock),
1024 (optkey + '+5', self.bookmarksdock),
1025 (optkey + '+6', self.recentdock),
1026 (optkey + '+7', self.branchdock),
1027 (optkey + '+8', self.submodulesdock),
1029 for shortcut, dockwidget in dockwidgets:
1030 # Associate the action with the shortcut
1031 toggleview = dockwidget.toggleViewAction()
1032 toggleview.setShortcut('Shift+' + shortcut)
1034 def showdock(show, dockwidget=dockwidget):
1035 if show:
1036 dockwidget.raise_()
1037 dockwidget.widget().setFocus()
1038 else:
1039 self.setFocus()
1041 self.addAction(toggleview)
1042 qtutils.connect_action_bool(toggleview, showdock)
1044 # Create a new shortcut Shift+<shortcut> that gives focus
1045 toggleview = QtWidgets.QAction(self)
1046 toggleview.setShortcut(shortcut)
1048 def focusdock(dockwidget=dockwidget):
1049 focus_dock(dockwidget)
1051 self.addAction(toggleview)
1052 qtutils.connect_action(toggleview, focusdock)
1054 # These widgets warrant home-row hotkey status
1055 qtutils.add_action(
1056 self,
1057 'Focus Commit Message',
1058 lambda: focus_dock(self.commitdock),
1059 hotkeys.FOCUS,
1062 qtutils.add_action(
1063 self,
1064 'Focus Status Window',
1065 lambda: focus_dock(self.statusdock),
1066 hotkeys.FOCUS_STATUS,
1069 qtutils.add_action(
1070 self,
1071 'Focus Diff Editor',
1072 lambda: focus_dock(self.diffdock),
1073 hotkeys.FOCUS_DIFF,
1076 def git_dag(self):
1077 self.dag = dag.git_dag(self.context, existing_view=self.dag)
1079 def show_cursor_position(self, rows, cols):
1080 display = '%02d:%02d' % (rows, cols)
1081 css = """
1082 <style>
1083 .good {
1085 .first-warning {
1086 color: black;
1087 background-color: yellow;
1089 .second-warning {
1090 color: black;
1091 background-color: #f83;
1093 .error {
1094 color: white;
1095 background-color: red;
1097 </style>
1100 if cols > 78:
1101 cls = 'error'
1102 elif cols > 72:
1103 cls = 'second-warning'
1104 elif cols > 64:
1105 cls = 'first-warning'
1106 else:
1107 cls = 'good'
1108 div = '<div class="%s">%s</div>' % (cls, display)
1109 self.position_label.setText(css + div)
1112 class FocusProxy(object):
1113 """Proxy over child widgets and operate on the focused widget"""
1115 def __init__(self, *widgets):
1116 self.widgets = widgets
1117 self.overrides = {}
1119 def override(self, name, widgets):
1120 self.overrides[name] = widgets
1122 def focus(self, name):
1123 """Return the currently focused widget"""
1124 widgets = self.overrides.get(name, self.widgets)
1125 # The parent must be the parent of all the proxied widgets
1126 parent = widgets[0]
1127 # The first widget is used as a fallback
1128 fallback = widgets[1]
1129 # We ignore the parent when delegating to child widgets
1130 widgets = widgets[1:]
1132 focus = parent.focusWidget()
1133 if focus not in widgets:
1134 focus = fallback
1135 return focus
1137 def __getattr__(self, name):
1138 """Return a callback that calls a common child method"""
1140 def callback():
1141 focus = self.focus(name)
1142 fn = getattr(focus, name, None)
1143 if fn:
1144 fn()
1146 return callback
1148 def delete(self):
1149 """Specialized delete() to deal with QLineEdit vs QTextEdit"""
1150 focus = self.focus('delete')
1151 if hasattr(focus, 'del_'):
1152 focus.del_()
1153 elif hasattr(focus, 'textCursor'):
1154 focus.textCursor().deleteChar()
1157 def show_dock(dockwidget):
1158 dockwidget.raise_()
1159 dockwidget.widget().setFocus()
1162 def focus_dock(dockwidget):
1163 if get(dockwidget.toggleViewAction()):
1164 show_dock(dockwidget)
1165 else:
1166 dockwidget.toggleViewAction().trigger()