pylint: disable too-many-ancestors and no-member warnings
[git-cola.git] / cola / widgets / main.py
blobbfe171db3772eb0461463072f2929fafead31f00
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(context, parent=self,
88 show=False, update=False)
89 self.browserdock = create_dock(N_('Browser'), self, widget=browser)
91 # "Actions" widget
92 self.actionsdock = create_dock(
93 N_('Actions'), self, widget=action.ActionButtons(context, self))
94 qtutils.hide_dock(self.actionsdock)
96 # "Repository Status" widget
97 self.statusdock = create_dock(
98 N_('Status'), self,
99 fn=lambda dock: status.StatusWidget(
100 context, dock.titleBarWidget(), dock))
101 self.statuswidget = self.statusdock.widget()
103 # "Switch Repository" widgets
104 self.bookmarksdock = create_dock(
105 N_('Favorites'), self,
106 fn=lambda dock: bookmarks.bookmark(context, dock))
107 bookmarkswidget = self.bookmarksdock.widget()
108 qtutils.hide_dock(self.bookmarksdock)
110 self.recentdock = create_dock(
111 N_('Recent'), self,
112 fn=lambda dock: bookmarks.recent(context, dock))
113 recentwidget = self.recentdock.widget()
114 qtutils.hide_dock(self.recentdock)
115 bookmarkswidget.connect_to(recentwidget)
117 # "Branch" widgets
118 self.branchdock = create_dock(
119 N_('Branches'), self, fn=partial(branch.BranchesWidget, context))
120 self.branchwidget = self.branchdock.widget()
121 titlebar = self.branchdock.titleBarWidget()
122 titlebar.add_corner_widget(self.branchwidget.filter_button)
123 titlebar.add_corner_widget(self.branchwidget.sort_order_button)
125 # "Submodule" widgets
126 self.submodulesdock = create_dock(
127 N_('Submodules'), self,
128 fn=partial(submodules.SubmodulesWidget, context))
129 self.submoduleswidget = self.submodulesdock.widget()
131 # "Commit Message Editor" widget
132 self.position_label = QtWidgets.QLabel()
133 self.position_label.setAlignment(Qt.AlignCenter)
134 font = qtutils.default_monospace_font()
135 font.setPointSize(int(font.pointSize() * 0.8))
136 self.position_label.setFont(font)
138 # make the position label fixed size to avoid layout issues
139 fm = self.position_label.fontMetrics()
140 width = fm.width('99:999') + defs.spacing
141 self.position_label.setMinimumWidth(width)
143 editor = commitmsg.CommitMessageEditor(context, self)
144 self.commiteditor = editor
145 self.commitdock = create_dock(N_('Commit'), self, widget=editor)
146 titlebar = self.commitdock.titleBarWidget()
147 titlebar.add_corner_widget(self.position_label)
149 # "Console" widget
150 self.logwidget = log.LogWidget(context)
151 self.logdock = create_dock(N_('Console'), self, widget=self.logwidget)
152 qtutils.hide_dock(self.logdock)
154 # "Diff Viewer" widget
155 self.diffdock = create_dock(
156 N_('Diff'), self,
157 fn=lambda dock: diff.Viewer(context, parent=dock))
158 self.diffviewer = self.diffdock.widget()
159 self.diffviewer.set_diff_type(self.model.diff_type)
161 self.diffeditor = self.diffviewer.text
162 titlebar = self.diffdock.titleBarWidget()
163 titlebar.add_corner_widget(self.diffviewer.options)
165 # All Actions
166 add_action = qtutils.add_action
167 add_action_bool = qtutils.add_action_bool
169 self.commit_amend_action = add_action_bool(
170 self, N_('Amend Last Commit'),
171 partial(cmds.do, cmds.AmendMode, context), False)
172 self.commit_amend_action.setShortcut(hotkeys.AMEND)
173 self.commit_amend_action.setShortcutContext(Qt.WidgetShortcut)
175 self.unstage_all_action = add_action(
176 self, N_('Unstage All'), cmds.run(cmds.UnstageAll, context))
177 self.unstage_all_action.setIcon(icons.remove())
179 self.unstage_selected_action = add_action(
180 self, N_('Unstage From Commit'),
181 cmds.run(cmds.UnstageSelected, context))
182 self.unstage_selected_action.setIcon(icons.remove())
184 self.show_diffstat_action = add_action(
185 self, N_('Diffstat'), self.statuswidget.select_header,
186 hotkeys.DIFFSTAT)
188 self.stage_modified_action = add_action(
189 self, N_('Stage Changed Files To Commit'),
190 cmds.run(cmds.StageModified, context), hotkeys.STAGE_MODIFIED)
191 self.stage_modified_action.setIcon(icons.add())
193 self.stage_untracked_action = add_action(
194 self, N_('Stage All Untracked'),
195 cmds.run(cmds.StageUntracked, context), hotkeys.STAGE_UNTRACKED)
196 self.stage_untracked_action.setIcon(icons.add())
198 self.apply_patches_action = add_action(
199 self, N_('Apply Patches...'),
200 partial(patch.apply_patches, context))
202 self.export_patches_action = add_action(
203 self, N_('Export Patches...'),
204 partial(guicmds.export_patches, context), hotkeys.EXPORT)
206 self.new_repository_action = add_action(
207 self, N_('New Repository...'),
208 partial(guicmds.open_new_repo, context))
209 self.new_repository_action.setIcon(icons.new())
211 self.new_bare_repository_action = add_action(
212 self, N_('New Bare Repository...'),
213 partial(guicmds.new_bare_repo, context))
214 self.new_bare_repository_action.setIcon(icons.new())
216 prefs_fn = partial(
217 prefs_widget.preferences, context, parent=self, model=prefs_model)
218 self.preferences_action = add_action(
219 self, N_('Preferences'), prefs_fn, QtGui.QKeySequence.Preferences)
221 self.edit_remotes_action = add_action(
222 self, N_('Edit Remotes...'), partial(editremotes.editor, context))
224 self.rescan_action = add_action(
225 self, cmds.Refresh.name(), cmds.run(cmds.Refresh, context),
226 *hotkeys.REFRESH_HOTKEYS)
227 self.rescan_action.setIcon(icons.sync())
229 self.find_files_action = add_action(
230 self, N_('Find Files'), partial(finder.finder, context),
231 hotkeys.FINDER, hotkeys.FINDER_SECONDARY)
232 self.find_files_action.setIcon(icons.zoom_in())
234 self.browse_recently_modified_action = add_action(
235 self, N_('Recently Modified Files...'),
236 partial(recent.browse_recent_files, context),
237 hotkeys.EDIT_SECONDARY)
239 self.cherry_pick_action = add_action(
240 self, N_('Cherry-Pick...'), partial(guicmds.cherry_pick, context),
241 hotkeys.CHERRY_PICK)
243 self.load_commitmsg_action = add_action(
244 self, N_('Load Commit Message...'),
245 partial(guicmds.load_commitmsg, context))
247 self.prepare_commitmsg_hook_action = add_action(
248 self, N_('Prepare Commit Message'),
249 cmds.run(cmds.PrepareCommitMessageHook, context),
250 hotkeys.PREPARE_COMMIT_MESSAGE)
252 self.save_tarball_action = add_action(
253 self, N_('Save As Tarball/Zip...'),
254 partial(archive.save_archive, context))
256 self.quit_action = add_action(
257 self, N_('Quit'), self.close, hotkeys.QUIT)
259 self.grep_action = add_action(
260 self, N_('Grep'), partial(grep.grep, context), hotkeys.GREP)
262 self.merge_local_action = add_action(
263 self, N_('Merge...'), partial(merge.local_merge, context),
264 hotkeys.MERGE)
266 self.merge_abort_action = add_action(
267 self, N_('Abort Merge...'), cmds.run(cmds.AbortMerge, context))
269 self.update_submodules_action = add_action(
270 self, N_('Update All Submodules...'),
271 cmds.run(cmds.SubmodulesUpdate, context))
273 self.fetch_action = add_action(
274 self, N_('Fetch...'), partial(remote.fetch, context),
275 hotkeys.FETCH)
276 self.push_action = add_action(
277 self, N_('Push...'), partial(remote.push, context), hotkeys.PUSH)
278 self.pull_action = add_action(
279 self, N_('Pull...'), partial(remote.pull, context), hotkeys.PULL)
281 self.open_repo_action = add_action(
282 self, N_('Open...'),
283 partial(guicmds.open_repo, context), hotkeys.OPEN)
284 self.open_repo_action.setIcon(icons.folder())
286 self.open_repo_new_action = add_action(
287 self, N_('Open in New Window...'),
288 partial(guicmds.open_repo_in_new_window, context))
289 self.open_repo_new_action.setIcon(icons.folder())
291 self.stash_action = add_action(
292 self, N_('Stash...'), partial(stash.view, context), hotkeys.STASH)
294 self.reset_branch_head_action = add_action(
295 self, N_('Reset Branch Head'),
296 partial(guicmds.reset_branch_head, context))
298 self.reset_worktree_action = add_action(
299 self, N_('Reset Worktree'),
300 partial(guicmds.reset_worktree, context))
302 self.clone_repo_action = add_action(
303 self, N_('Clone...'),
304 partial(clone.clone, context, settings=settings))
305 self.clone_repo_action.setIcon(icons.repo())
307 self.help_docs_action = add_action(
308 self, N_('Documentation'), resources.show_html_docs,
309 QtGui.QKeySequence.HelpContents)
311 self.help_shortcuts_action = add_action(
312 self, N_('Keyboard Shortcuts'), about.show_shortcuts,
313 hotkeys.QUESTION)
315 self.visualize_current_action = add_action(
316 self, N_('Visualize Current Branch...'),
317 cmds.run(cmds.VisualizeCurrent, context))
318 self.visualize_all_action = add_action(
319 self, N_('Visualize All Branches...'),
320 cmds.run(cmds.VisualizeAll, context))
321 self.search_commits_action = add_action(
322 self, N_('Search...'), partial(search.search, context))
324 self.browse_branch_action = add_action(
325 self, N_('Browse Current Branch...'),
326 partial(guicmds.browse_current, context))
327 self.browse_other_branch_action = add_action(
328 self, N_('Browse Other Branch...'),
329 partial(guicmds.browse_other, context))
330 self.load_commitmsg_template_action = add_action(
331 self, N_('Get Commit Message Template'),
332 cmds.run(cmds.LoadCommitMessageFromTemplate, context))
333 self.help_about_action = add_action(
334 self, N_('About'), partial(about.about_dialog, context))
336 self.diff_expression_action = add_action(
337 self, N_('Expression...'),
338 partial(guicmds.diff_expression, context))
339 self.branch_compare_action = add_action(
340 self, N_('Branches...'),
341 partial(compare.compare_branches, context))
343 self.create_tag_action = add_action(
344 self, N_('Create Tag...'),
345 partial(createtag.create_tag, context, settings=settings))
347 self.create_branch_action = add_action(
348 self, N_('Create...'),
349 partial(createbranch.create_new_branch, context,
350 settings=settings),
351 hotkeys.BRANCH)
352 self.create_branch_action.setIcon(icons.branch())
354 self.delete_branch_action = add_action(
355 self, N_('Delete...'),
356 partial(guicmds.delete_branch, context))
358 self.delete_remote_branch_action = add_action(
359 self, N_('Delete Remote Branch...'),
360 partial(guicmds.delete_remote_branch, context))
362 self.rename_branch_action = add_action(
363 self, N_('Rename Branch...'),
364 partial(guicmds.rename_branch, context))
366 self.checkout_branch_action = add_action(
367 self, N_('Checkout...'),
368 partial(guicmds.checkout_branch, context),
369 hotkeys.CHECKOUT)
370 self.branch_review_action = add_action(
371 self, N_('Review...'),
372 partial(guicmds.review_branch, context))
374 self.browse_action = add_action(
375 self, N_('File Browser...'),
376 partial(browse.worktree_browser, context))
377 self.browse_action.setIcon(icons.cola())
379 self.dag_action = add_action(self, N_('DAG...'), self.git_dag)
380 self.dag_action.setIcon(icons.cola())
382 self.rebase_start_action = add_action(
383 self, N_('Start Interactive Rebase...'),
384 cmds.run(cmds.Rebase, context), hotkeys.REBASE_START_AND_CONTINUE)
386 self.rebase_edit_todo_action = add_action(
387 self, N_('Edit...'), cmds.run(cmds.RebaseEditTodo, context))
389 self.rebase_continue_action = add_action(
390 self, N_('Continue'), cmds.run(cmds.RebaseContinue, context),
391 hotkeys.REBASE_START_AND_CONTINUE)
393 self.rebase_skip_action = add_action(
394 self, N_('Skip Current Patch'), cmds.run(cmds.RebaseSkip, context))
396 self.rebase_abort_action = add_action(
397 self, N_('Abort'), cmds.run(cmds.RebaseAbort, context))
399 # For "Start Rebase" only, reverse the first argument to setEnabled()
400 # so that we can operate on it as a group.
401 # We can do this because can_rebase == not is_rebasing
402 self.rebase_start_action_proxy = utils.Proxy(
403 self.rebase_start_action,
404 setEnabled=lambda x: self.rebase_start_action.setEnabled(not x))
406 self.rebase_group = utils.Group(self.rebase_start_action_proxy,
407 self.rebase_edit_todo_action,
408 self.rebase_continue_action,
409 self.rebase_skip_action,
410 self.rebase_abort_action)
412 self.annex_init_action = qtutils.add_action(
413 self, N_('Initialize Git Annex'),
414 cmds.run(cmds.AnnexInit, context))
416 self.lfs_init_action = qtutils.add_action(
417 self, N_('Initialize Git LFS'), cmds.run(cmds.LFSInstall, context))
419 self.lock_layout_action = add_action_bool(
420 self, N_('Lock Layout'), self.set_lock_layout, False)
422 # Create the application menu
423 self.menubar = QtWidgets.QMenuBar(self)
424 self.setMenuBar(self.menubar)
426 # File Menu
427 add_menu = qtutils.add_menu
428 self.file_menu = add_menu(N_('&File'), self.menubar)
429 # File->Open Recent menu
430 self.open_recent_menu = self.file_menu.addMenu(N_('Open Recent'))
431 self.open_recent_menu.setIcon(icons.folder())
432 self.file_menu.addAction(self.open_repo_action)
433 self.file_menu.addAction(self.open_repo_new_action)
434 self.file_menu.addSeparator()
435 self.file_menu.addAction(self.new_repository_action)
436 self.file_menu.addAction(self.new_bare_repository_action)
437 self.file_menu.addAction(self.clone_repo_action)
438 self.file_menu.addSeparator()
439 self.file_menu.addAction(self.rescan_action)
440 self.file_menu.addAction(self.find_files_action)
441 self.file_menu.addAction(self.edit_remotes_action)
442 self.file_menu.addAction(self.browse_recently_modified_action)
443 self.file_menu.addSeparator()
444 self.file_menu.addAction(self.apply_patches_action)
445 self.file_menu.addAction(self.export_patches_action)
446 self.file_menu.addAction(self.save_tarball_action)
448 # Git Annex / Git LFS
449 annex = core.find_executable('git-annex')
450 lfs = core.find_executable('git-lfs')
451 if annex or lfs:
452 self.file_menu.addSeparator()
453 if annex:
454 self.file_menu.addAction(self.annex_init_action)
455 if lfs:
456 self.file_menu.addAction(self.lfs_init_action)
458 self.file_menu.addSeparator()
459 self.file_menu.addAction(self.preferences_action)
460 self.file_menu.addAction(self.quit_action)
462 # Edit Menu
463 self.edit_proxy = edit_proxy = (
464 FocusProxy(editor, editor.summary, editor.description))
466 copy_widgets = (
467 self, editor.summary, editor.description, self.diffeditor,
468 bookmarkswidget.tree, recentwidget.tree,
470 edit_proxy.override('copy', copy_widgets)
471 edit_proxy.override('selectAll', copy_widgets)
473 edit_menu = self.edit_menu = add_menu(N_('&Edit'), self.menubar)
474 add_action(edit_menu, N_('Undo'), edit_proxy.undo, hotkeys.UNDO)
475 add_action(edit_menu, N_('Redo'), edit_proxy.redo, hotkeys.REDO)
476 edit_menu.addSeparator()
477 add_action(edit_menu, N_('Cut'), edit_proxy.cut, hotkeys.CUT)
478 add_action(edit_menu, N_('Copy'), edit_proxy.copy, hotkeys.COPY)
479 add_action(edit_menu, N_('Paste'), edit_proxy.paste, hotkeys.PASTE)
480 add_action(edit_menu, N_('Delete'), edit_proxy.delete, hotkeys.DELETE)
481 edit_menu.addSeparator()
482 add_action(edit_menu, N_('Select All'), edit_proxy.selectAll,
483 hotkeys.SELECT_ALL)
484 edit_menu.addSeparator()
486 commitmsg.add_menu_actions(edit_menu, self.commiteditor.menu_actions)
488 # Actions menu
489 self.actions_menu = add_menu(N_('Actions'), self.menubar)
490 self.actions_menu.addAction(self.fetch_action)
491 self.actions_menu.addAction(self.push_action)
492 self.actions_menu.addAction(self.pull_action)
493 self.actions_menu.addAction(self.stash_action)
494 self.actions_menu.addSeparator()
495 self.actions_menu.addAction(self.create_tag_action)
496 self.actions_menu.addAction(self.cherry_pick_action)
497 self.actions_menu.addAction(self.merge_local_action)
498 self.actions_menu.addAction(self.merge_abort_action)
499 self.actions_menu.addSeparator()
500 self.actions_menu.addAction(self.update_submodules_action)
501 self.actions_menu.addSeparator()
502 self.actions_reset_menu = self.actions_menu.addMenu(N_('Reset'))
503 self.actions_reset_menu.addAction(self.reset_branch_head_action)
504 self.actions_reset_menu.addAction(self.reset_worktree_action)
505 self.actions_menu.addSeparator()
506 self.actions_menu.addAction(self.grep_action)
507 self.actions_menu.addAction(self.search_commits_action)
509 # Commit Menu
510 self.commit_menu = add_menu(N_('Commit@@verb'), self.menubar)
511 self.commit_menu.setTitle(N_('Commit@@verb'))
512 self.commit_menu.addAction(self.commiteditor.commit_action)
513 self.commit_menu.addAction(self.commit_amend_action)
514 self.commit_menu.addSeparator()
515 self.commit_menu.addAction(self.stage_modified_action)
516 self.commit_menu.addAction(self.stage_untracked_action)
517 self.commit_menu.addSeparator()
518 self.commit_menu.addAction(self.unstage_all_action)
519 self.commit_menu.addAction(self.unstage_selected_action)
520 self.commit_menu.addSeparator()
521 self.commit_menu.addAction(self.load_commitmsg_action)
522 self.commit_menu.addAction(self.load_commitmsg_template_action)
523 self.commit_menu.addAction(self.prepare_commitmsg_hook_action)
525 # Diff Menu
526 self.diff_menu = add_menu(N_('Diff'), self.menubar)
527 self.diff_menu.addAction(self.diff_expression_action)
528 self.diff_menu.addAction(self.branch_compare_action)
529 self.diff_menu.addSeparator()
530 self.diff_menu.addAction(self.show_diffstat_action)
532 # Branch Menu
533 self.branch_menu = add_menu(N_('Branch'), self.menubar)
534 self.branch_menu.addAction(self.branch_review_action)
535 self.branch_menu.addSeparator()
536 self.branch_menu.addAction(self.create_branch_action)
537 self.branch_menu.addAction(self.checkout_branch_action)
538 self.branch_menu.addAction(self.delete_branch_action)
539 self.branch_menu.addAction(self.delete_remote_branch_action)
540 self.branch_menu.addAction(self.rename_branch_action)
541 self.branch_menu.addSeparator()
542 self.branch_menu.addAction(self.browse_branch_action)
543 self.branch_menu.addAction(self.browse_other_branch_action)
544 self.branch_menu.addSeparator()
545 self.branch_menu.addAction(self.visualize_current_action)
546 self.branch_menu.addAction(self.visualize_all_action)
548 # Rebase menu
549 self.rebase_menu = add_menu(N_('Rebase'), self.actions_menu)
550 self.rebase_menu.addAction(self.rebase_start_action)
551 self.rebase_menu.addAction(self.rebase_edit_todo_action)
552 self.rebase_menu.addSeparator()
553 self.rebase_menu.addAction(self.rebase_continue_action)
554 self.rebase_menu.addAction(self.rebase_skip_action)
555 self.rebase_menu.addSeparator()
556 self.rebase_menu.addAction(self.rebase_abort_action)
558 # View Menu
559 self.view_menu = add_menu(N_('View'), self.menubar)
560 # pylint: disable=no-member
561 self.view_menu.aboutToShow.connect(
562 lambda: self.build_view_menu(self.view_menu))
563 self.setup_dockwidget_view_menu()
564 if utils.is_darwin():
565 # TODO or self.menubar.setNativeMenuBar(False)
566 # Since native OSX menu doesn't show empty entries
567 self.build_view_menu(self.view_menu)
569 # Help Menu
570 self.help_menu = add_menu(N_('Help'), self.menubar)
571 self.help_menu.addAction(self.help_docs_action)
572 self.help_menu.addAction(self.help_shortcuts_action)
573 self.help_menu.addAction(self.help_about_action)
575 # Arrange dock widgets
576 bottom = Qt.BottomDockWidgetArea
577 top = Qt.TopDockWidgetArea
579 self.addDockWidget(top, self.statusdock)
580 self.addDockWidget(top, self.commitdock)
581 if self.browser_dockable:
582 self.addDockWidget(top, self.browserdock)
583 self.tabifyDockWidget(self.browserdock, self.commitdock)
585 self.addDockWidget(top, self.branchdock)
586 self.addDockWidget(top, self.submodulesdock)
587 self.addDockWidget(top, self.bookmarksdock)
588 self.addDockWidget(top, self.recentdock)
590 self.tabifyDockWidget(self.branchdock, self.submodulesdock)
591 self.tabifyDockWidget(self.submodulesdock, self.bookmarksdock)
592 self.tabifyDockWidget(self.bookmarksdock, self.recentdock)
593 self.branchdock.raise_()
595 self.addDockWidget(bottom, self.diffdock)
596 self.addDockWidget(bottom, self.actionsdock)
597 self.addDockWidget(bottom, self.logdock)
598 self.tabifyDockWidget(self.actionsdock, self.logdock)
600 # Listen for model notifications
601 model.add_observer(model.message_updated, self.updated.emit)
602 model.add_observer(model.message_mode_changed,
603 lambda mode: self.updated.emit())
605 prefs_model.add_observer(prefs_model.message_config_updated,
606 self._config_updated)
608 # Set a default value
609 self.show_cursor_position(1, 0)
611 self.commit_menu.aboutToShow.connect(self.update_menu_actions)
612 self.open_recent_menu.aboutToShow.connect(self.build_recent_menu)
613 self.commiteditor.cursor_changed.connect(self.show_cursor_position)
615 self.diffeditor.options_changed.connect(self.statuswidget.refresh)
616 self.diffeditor.up.connect(self.statuswidget.move_up)
617 self.diffeditor.down.connect(self.statuswidget.move_down)
619 self.commiteditor.up.connect(self.statuswidget.move_up)
620 self.commiteditor.down.connect(self.statuswidget.move_down)
622 self.updated.connect(self.refresh, type=Qt.QueuedConnection)
624 self.config_actions_changed.connect(self._install_config_actions,
625 type=Qt.QueuedConnection)
626 self.init_state(settings, self.set_initial_size)
628 # Route command output here
629 Interaction.log_status = self.logwidget.log_status
630 Interaction.log = self.logwidget.log
631 # Focus the status widget; this must be deferred
632 QtCore.QTimer.singleShot(0, self.initialize)
634 def initialize(self):
635 context = self.context
636 git_version = version.git_version_str(context)
637 if git_version:
638 ok = True
639 Interaction.log(git_version + '\n' +
640 N_('git cola version %s') % version.version())
641 else:
642 ok = False
643 error_msg = N_('error: unable to execute git')
644 Interaction.log(error_msg)
646 if ok:
647 self.statuswidget.setFocus()
648 else:
649 title = N_('error: unable to execute git')
650 msg = title
651 details = ''
652 if WIN32:
653 details = git.win32_git_error_hint()
654 Interaction.critical(title, message=msg, details=details)
655 self.context.app.exit(2)
657 def set_initial_size(self):
658 # Default size; this is thrown out when save/restore is used
659 width, height = qtutils.desktop_size()
660 self.resize((width*3)//4, height)
661 self.statuswidget.set_initial_size()
662 self.commiteditor.set_initial_size()
664 def set_filter(self, txt):
665 self.statuswidget.set_filter(txt)
667 # Qt overrides
668 def closeEvent(self, event):
669 """Save state in the settings"""
670 commit_msg = self.commiteditor.commit_message(raw=True)
671 self.model.save_commitmsg(msg=commit_msg)
672 standard.MainWindow.closeEvent(self, event)
674 def create_view_menu(self):
675 menu = qtutils.create_menu(N_('View'), self)
676 self.build_view_menu(menu)
677 return menu
679 def build_view_menu(self, menu):
680 menu.clear()
681 menu.addAction(self.browse_action)
682 menu.addAction(self.dag_action)
683 menu.addSeparator()
685 popup_menu = self.createPopupMenu()
686 for menu_action in popup_menu.actions():
687 menu_action.setParent(menu)
688 menu.addAction(menu_action)
690 menu.addSeparator()
691 context = self.context
692 menu_action = menu.addAction(
693 N_('Add Toolbar'), partial(toolbar.add_toolbar, context, self))
694 menu_action.setIcon(icons.add())
696 dockwidgets = [
697 self.logdock,
698 self.commitdock,
699 self.statusdock,
700 self.diffdock,
701 self.actionsdock,
702 self.bookmarksdock,
703 self.recentdock,
704 self.branchdock,
705 self.submodulesdock
707 if self.browser_dockable:
708 dockwidgets.append(self.browserdock)
710 for dockwidget in dockwidgets:
711 # Associate the action with the shortcut
712 toggleview = dockwidget.toggleViewAction()
713 menu.addAction(toggleview)
715 menu.addSeparator()
716 menu.addAction(self.lock_layout_action)
718 return menu
720 def contextMenuEvent(self, event):
721 menu = self.create_view_menu()
722 menu.exec_(event.globalPos())
724 def build_recent_menu(self):
725 settings = Settings()
726 settings.load()
727 cmd = cmds.OpenRepo
728 context = self.context
729 menu = self.open_recent_menu
730 menu.clear()
731 worktree = self.git.worktree()
732 for entry in settings.recent:
733 directory = entry['path']
734 if directory == worktree:
735 # Omit the current worktree from the "Open Recent" menu.
736 continue
737 name = entry['name']
738 text = '%s %s %s' % (name, uchr(0x2192), directory)
739 menu.addAction(text, cmds.run(cmd, context, directory))
741 # Accessors
742 mode = property(lambda self: self.model.mode)
744 def _config_updated(self, _source, config, value):
745 if config == prefs.FONTDIFF:
746 # The diff font
747 font = QtGui.QFont()
748 if not font.fromString(value):
749 return
750 self.logwidget.setFont(font)
751 self.diffeditor.setFont(font)
752 self.commiteditor.setFont(font)
754 elif config == prefs.TABWIDTH:
755 # This can be set locally or globally, so we have to use the
756 # effective value otherwise we'll update when we shouldn't.
757 # For example, if this value is overridden locally, and the
758 # global value is tweaked, we should not update.
759 value = prefs.tabwidth(self.context)
760 self.diffeditor.set_tabwidth(value)
761 self.commiteditor.set_tabwidth(value)
763 elif config == prefs.EXPANDTAB:
764 self.commiteditor.set_expandtab(value)
766 elif config == prefs.LINEBREAK:
767 # enables automatic line breaks
768 self.commiteditor.set_linebreak(value)
770 elif config == prefs.SORT_BOOKMARKS:
771 self.bookmarksdock.widget().reload_bookmarks()
773 elif config == prefs.TEXTWIDTH:
774 # Use the effective value for the same reason as tabwidth.
775 value = prefs.textwidth(self.context)
776 self.commiteditor.set_textwidth(value)
778 elif config == prefs.SHOW_PATH:
779 # the path in the window title was toggled
780 self.refresh_window_title()
782 def start(self, context):
783 """Do the expensive "get_config_actions()" call in the background"""
784 # Install .git-config-defined actions
785 task = qtutils.SimpleTask(self, self.get_config_actions)
786 context.runtask.start(task)
788 def get_config_actions(self):
789 actions = cfgactions.get_config_actions(self.context)
790 self.config_actions_changed.emit(actions)
792 def _install_config_actions(self, names_and_shortcuts):
793 """Install .gitconfig-defined actions"""
794 if not names_and_shortcuts:
795 return
796 context = self.context
797 menu = self.actions_menu
798 menu.addSeparator()
799 for (name, shortcut) in names_and_shortcuts:
800 callback = cmds.run(cmds.RunConfigAction, context, name)
801 menu_action = menu.addAction(name, callback)
802 if shortcut:
803 menu_action.setShortcut(shortcut)
805 def refresh(self):
806 """Update the title with the current branch and directory name."""
807 curbranch = self.model.currentbranch
808 curdir = core.getcwd()
809 is_merging = self.model.is_merging
810 is_rebasing = self.model.is_rebasing
812 msg = N_('Repository: %s') % curdir
813 msg += '\n'
814 msg += N_('Branch: %s') % curbranch
816 if is_rebasing:
817 msg += '\n\n'
818 msg += N_('This repository is currently being rebased.\n'
819 'Resolve conflicts, commit changes, and run:\n'
820 ' Rebase > Continue')
822 elif is_merging:
823 msg += '\n\n'
824 msg += N_('This repository is in the middle of a merge.\n'
825 'Resolve conflicts and commit changes.')
827 self.refresh_window_title()
829 if self.mode == self.model.mode_amend:
830 self.commit_amend_action.setChecked(True)
831 else:
832 self.commit_amend_action.setChecked(False)
834 self.commitdock.setToolTip(msg)
835 self.commiteditor.set_mode(self.mode)
836 self.update_actions()
838 def refresh_window_title(self):
839 """Refresh the window title when state changes"""
840 alerts = []
842 project = self.model.project
843 curbranch = self.model.currentbranch
844 is_merging = self.model.is_merging
845 is_rebasing = self.model.is_rebasing
846 prefix = uchr(0xab)
847 suffix = uchr(0xbb)
849 if is_rebasing:
850 alerts.append(N_('Rebasing'))
851 elif is_merging:
852 alerts.append(N_('Merging'))
854 if self.mode == self.model.mode_amend:
855 alerts.append(N_('Amending'))
857 if alerts:
858 alert_text = (prefix + ' %s ' + suffix + ' ') % ', '.join(alerts)
859 else:
860 alert_text = ''
862 if self.model.cfg.get(prefs.SHOW_PATH, True):
863 path_text = self.git.worktree()
864 else:
865 path_text = ''
867 title = '%s: %s %s%s' % (project, curbranch, alert_text, path_text)
868 self.setWindowTitle(title)
870 def update_actions(self):
871 is_rebasing = self.model.is_rebasing
872 self.rebase_group.setEnabled(is_rebasing)
874 enabled = not self.model.is_empty_repository()
875 self.rename_branch_action.setEnabled(enabled)
876 self.delete_branch_action.setEnabled(enabled)
878 self.annex_init_action.setEnabled(not self.model.annex)
879 self.lfs_init_action.setEnabled(not self.model.lfs)
881 def update_menu_actions(self):
882 # Enable the Prepare Commit Message action if the hook exists
883 hook = gitcmds.prepare_commit_message_hook(self.context)
884 enabled = os.path.exists(hook)
885 self.prepare_commitmsg_hook_action.setEnabled(enabled)
887 def export_state(self):
888 state = standard.MainWindow.export_state(self)
889 show_status_filter = self.statuswidget.filter_widget.isVisible()
890 state['show_status_filter'] = show_status_filter
891 state['toolbars'] = self.toolbar_state.export_state()
892 state['ref_sort'] = self.model.ref_sort
893 self.diffviewer.export_state(state)
895 return state
897 def apply_state(self, state):
898 """Imports data for save/restore"""
899 base_ok = standard.MainWindow.apply_state(self, state)
900 lock_layout = state.get('lock_layout', False)
901 self.lock_layout_action.setChecked(lock_layout)
903 show_status_filter = state.get('show_status_filter', False)
904 self.statuswidget.filter_widget.setVisible(show_status_filter)
906 toolbars = state.get('toolbars', [])
907 self.toolbar_state.apply_state(toolbars)
909 sort_key = state.get('ref_sort', 0)
910 self.model.set_ref_sort(sort_key)
912 diff_ok = self.diffviewer.apply_state(state)
913 return base_ok and diff_ok
915 def setup_dockwidget_view_menu(self):
916 # Hotkeys for toggling the dock widgets
917 if utils.is_darwin():
918 optkey = 'Meta'
919 else:
920 optkey = 'Ctrl'
921 dockwidgets = (
922 (optkey + '+0', self.logdock),
923 (optkey + '+1', self.commitdock),
924 (optkey + '+2', self.statusdock),
925 (optkey + '+3', self.diffdock),
926 (optkey + '+4', self.actionsdock),
927 (optkey + '+5', self.bookmarksdock),
928 (optkey + '+6', self.recentdock),
929 (optkey + '+7', self.branchdock),
930 (optkey + '+8', self.submodulesdock)
932 for shortcut, dockwidget in dockwidgets:
933 # Associate the action with the shortcut
934 toggleview = dockwidget.toggleViewAction()
935 toggleview.setShortcut('Shift+' + shortcut)
937 def showdock(show, dockwidget=dockwidget):
938 if show:
939 dockwidget.raise_()
940 dockwidget.widget().setFocus()
941 else:
942 self.setFocus()
944 self.addAction(toggleview)
945 qtutils.connect_action_bool(toggleview, showdock)
947 # Create a new shortcut Shift+<shortcut> that gives focus
948 toggleview = QtWidgets.QAction(self)
949 toggleview.setShortcut(shortcut)
951 def focusdock(dockwidget=dockwidget):
952 focus_dock(dockwidget)
953 self.addAction(toggleview)
954 qtutils.connect_action(toggleview, focusdock)
956 # These widgets warrant home-row hotkey status
957 qtutils.add_action(self, 'Focus Commit Message',
958 lambda: focus_dock(self.commitdock),
959 hotkeys.FOCUS)
961 qtutils.add_action(self, 'Focus Status Window',
962 lambda: focus_dock(self.statusdock),
963 hotkeys.FOCUS_STATUS)
965 qtutils.add_action(self, 'Focus Diff Editor',
966 lambda: focus_dock(self.diffdock),
967 hotkeys.FOCUS_DIFF)
969 def git_dag(self):
970 self.dag = dag.git_dag(self.context, existing_view=self.dag)
972 def show_cursor_position(self, rows, cols):
973 display = '%02d:%02d' % (rows, cols)
974 css = """
975 <style>
976 .good {
978 .first-warning {
979 color: black;
980 background-color: yellow;
982 .second-warning {
983 color: black;
984 background-color: #f83;
986 .error {
987 color: white;
988 background-color: red;
990 </style>
993 if cols > 78:
994 cls = 'error'
995 elif cols > 72:
996 cls = 'second-warning'
997 elif cols > 64:
998 cls = 'first-warning'
999 else:
1000 cls = 'good'
1001 div = ('<div class="%s">%s</div>' % (cls, display))
1002 self.position_label.setText(css + div)
1005 class FocusProxy(object):
1006 """Proxy over child widgets and operate on the focused widget"""
1008 def __init__(self, *widgets):
1009 self.widgets = widgets
1010 self.overrides = {}
1012 def override(self, name, widgets):
1013 self.overrides[name] = widgets
1015 def focus(self, name):
1016 """Return the currently focused widget"""
1017 widgets = self.overrides.get(name, self.widgets)
1018 # The parent must be the parent of all the proxied widgets
1019 parent = widgets[0]
1020 # The first widget is used as a fallback
1021 fallback = widgets[1]
1022 # We ignore the parent when delegating to child widgets
1023 widgets = widgets[1:]
1025 focus = parent.focusWidget()
1026 if focus not in widgets:
1027 focus = fallback
1028 return focus
1030 def __getattr__(self, name):
1031 """Return a callback that calls a common child method"""
1032 def callback():
1033 focus = self.focus(name)
1034 fn = getattr(focus, name, None)
1035 if fn:
1036 fn()
1037 return callback
1039 def delete(self):
1040 """Specialized delete() to deal with QLineEdit vs QTextEdit"""
1041 focus = self.focus('delete')
1042 if hasattr(focus, 'del_'):
1043 focus.del_()
1044 elif hasattr(focus, 'textCursor'):
1045 focus.textCursor().deleteChar()
1048 def show_dock(dockwidget):
1049 dockwidget.raise_()
1050 dockwidget.widget().setFocus()
1053 def focus_dock(dockwidget):
1054 if get(dockwidget.toggleViewAction()):
1055 show_dock(dockwidget)
1056 else:
1057 dockwidget.toggleViewAction().trigger()