cmds: use version.check_git(...) in OpenParent
[git-cola.git] / cola / widgets / main.py
blob0733b828fcf5849ddf94cb69be6f2cbf2f93d659
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)
124 # "Submodule" widgets
125 self.submodulesdock = create_dock(
126 N_('Submodules'), self,
127 fn=partial(submodules.SubmodulesWidget, context))
128 self.submoduleswidget = self.submodulesdock.widget()
130 # "Commit Message Editor" widget
131 self.position_label = QtWidgets.QLabel()
132 self.position_label.setAlignment(Qt.AlignCenter)
133 font = qtutils.default_monospace_font()
134 font.setPointSize(int(font.pointSize() * 0.8))
135 self.position_label.setFont(font)
137 # make the position label fixed size to avoid layout issues
138 fm = self.position_label.fontMetrics()
139 width = fm.width('99:999') + defs.spacing
140 self.position_label.setMinimumWidth(width)
142 editor = commitmsg.CommitMessageEditor(context, self)
143 self.commiteditor = editor
144 self.commitdock = create_dock(N_('Commit'), self, widget=editor)
145 titlebar = self.commitdock.titleBarWidget()
146 titlebar.add_corner_widget(self.position_label)
148 # "Console" widget
149 self.logwidget = log.LogWidget(context)
150 self.logdock = create_dock(N_('Console'), self, widget=self.logwidget)
151 qtutils.hide_dock(self.logdock)
153 # "Diff Viewer" widget
154 self.diffdock = create_dock(
155 N_('Diff'), self,
156 fn=lambda dock: diff.Viewer(context, parent=dock))
157 self.diffviewer = self.diffdock.widget()
158 self.diffviewer.set_diff_type(self.model.diff_type)
160 self.diffeditor = self.diffviewer.text
161 titlebar = self.diffdock.titleBarWidget()
162 titlebar.add_corner_widget(self.diffviewer.options)
164 # All Actions
165 add_action = qtutils.add_action
166 add_action_bool = qtutils.add_action_bool
168 self.commit_amend_action = add_action_bool(
169 self, N_('Amend Last Commit'),
170 partial(cmds.do, cmds.AmendMode, context), False)
171 self.commit_amend_action.setShortcut(hotkeys.AMEND)
172 self.commit_amend_action.setShortcutContext(Qt.WidgetShortcut)
174 self.unstage_all_action = add_action(
175 self, N_('Unstage All'), cmds.run(cmds.UnstageAll, context))
176 self.unstage_all_action.setIcon(icons.remove())
178 self.unstage_selected_action = add_action(
179 self, N_('Unstage From Commit'),
180 cmds.run(cmds.UnstageSelected, context))
181 self.unstage_selected_action.setIcon(icons.remove())
183 self.show_diffstat_action = add_action(
184 self, N_('Diffstat'), self.statuswidget.select_header,
185 hotkeys.DIFFSTAT)
187 self.stage_modified_action = add_action(
188 self, N_('Stage Changed Files To Commit'),
189 cmds.run(cmds.StageModified, context), hotkeys.STAGE_MODIFIED)
190 self.stage_modified_action.setIcon(icons.add())
192 self.stage_untracked_action = add_action(
193 self, N_('Stage All Untracked'),
194 cmds.run(cmds.StageUntracked, context), hotkeys.STAGE_UNTRACKED)
195 self.stage_untracked_action.setIcon(icons.add())
197 self.apply_patches_action = add_action(
198 self, N_('Apply Patches...'),
199 partial(patch.apply_patches, context))
201 self.export_patches_action = add_action(
202 self, N_('Export Patches...'),
203 partial(guicmds.export_patches, context), hotkeys.EXPORT)
205 self.new_repository_action = add_action(
206 self, N_('New Repository...'),
207 partial(guicmds.open_new_repo, context))
208 self.new_repository_action.setIcon(icons.new())
210 self.new_bare_repository_action = add_action(
211 self, N_('New Bare Repository...'),
212 partial(guicmds.new_bare_repo, context))
213 self.new_bare_repository_action.setIcon(icons.new())
215 prefs_fn = partial(
216 prefs_widget.preferences, context, parent=self, model=prefs_model)
217 self.preferences_action = add_action(
218 self, N_('Preferences'), prefs_fn, QtGui.QKeySequence.Preferences)
220 self.edit_remotes_action = add_action(
221 self, N_('Edit Remotes...'), partial(editremotes.editor, context))
223 self.rescan_action = add_action(
224 self, cmds.Refresh.name(), cmds.run(cmds.Refresh, context),
225 *hotkeys.REFRESH_HOTKEYS)
226 self.rescan_action.setIcon(icons.sync())
228 self.find_files_action = add_action(
229 self, N_('Find Files'), partial(finder.finder, context),
230 hotkeys.FINDER, hotkeys.FINDER_SECONDARY)
231 self.find_files_action.setIcon(icons.zoom_in())
233 self.browse_recently_modified_action = add_action(
234 self, N_('Recently Modified Files...'),
235 partial(recent.browse_recent_files, context),
236 hotkeys.EDIT_SECONDARY)
238 self.cherry_pick_action = add_action(
239 self, N_('Cherry-Pick...'), partial(guicmds.cherry_pick, context),
240 hotkeys.CHERRY_PICK)
242 self.load_commitmsg_action = add_action(
243 self, N_('Load Commit Message...'),
244 partial(guicmds.load_commitmsg, context))
246 self.prepare_commitmsg_hook_action = add_action(
247 self, N_('Prepare Commit Message'),
248 cmds.run(cmds.PrepareCommitMessageHook, context),
249 hotkeys.PREPARE_COMMIT_MESSAGE)
251 self.save_tarball_action = add_action(
252 self, N_('Save As Tarball/Zip...'),
253 partial(archive.save_archive, context, self))
255 self.quit_action = add_action(
256 self, N_('Quit'), self.close, hotkeys.QUIT)
258 self.grep_action = add_action(
259 self, N_('Grep'), partial(grep.grep, context), hotkeys.GREP)
261 self.merge_local_action = add_action(
262 self, N_('Merge...'), partial(merge.local_merge, context),
263 hotkeys.MERGE)
265 self.merge_abort_action = add_action(
266 self, N_('Abort Merge...'), cmds.run(cmds.AbortMerge, context))
268 self.update_submodules_action = add_action(
269 self, N_('Update All Submodules...'),
270 cmds.run(cmds.SubmodulesUpdate, context))
272 self.fetch_action = add_action(
273 self, N_('Fetch...'), partial(remote.fetch, context),
274 hotkeys.FETCH)
275 self.push_action = add_action(
276 self, N_('Push...'), partial(remote.push, context), hotkeys.PUSH)
277 self.pull_action = add_action(
278 self, N_('Pull...'), partial(remote.pull, context), hotkeys.PULL)
280 self.open_repo_action = add_action(
281 self, N_('Open...'),
282 partial(guicmds.open_repo, context), hotkeys.OPEN)
283 self.open_repo_action.setIcon(icons.folder())
285 self.open_repo_new_action = add_action(
286 self, N_('Open in New Window...'),
287 partial(guicmds.open_repo_in_new_window, context))
288 self.open_repo_new_action.setIcon(icons.folder())
290 self.stash_action = add_action(
291 self, N_('Stash...'), partial(stash.view, context), hotkeys.STASH)
293 self.reset_branch_head_action = add_action(
294 self, N_('Reset Branch Head'),
295 partial(guicmds.reset_branch_head, context))
297 self.reset_worktree_action = add_action(
298 self, N_('Reset Worktree'),
299 partial(guicmds.reset_worktree, context))
301 self.clone_repo_action = add_action(
302 self, N_('Clone...'),
303 partial(clone.clone, context, settings=settings))
304 self.clone_repo_action.setIcon(icons.repo())
306 self.help_docs_action = add_action(
307 self, N_('Documentation'), resources.show_html_docs,
308 QtGui.QKeySequence.HelpContents)
310 self.help_shortcuts_action = add_action(
311 self, N_('Keyboard Shortcuts'), about.show_shortcuts,
312 hotkeys.QUESTION)
314 self.visualize_current_action = add_action(
315 self, N_('Visualize Current Branch...'),
316 cmds.run(cmds.VisualizeCurrent, context))
317 self.visualize_all_action = add_action(
318 self, N_('Visualize All Branches...'),
319 cmds.run(cmds.VisualizeAll, context))
320 self.search_commits_action = add_action(
321 self, N_('Search...'), partial(search.search, context))
323 self.browse_branch_action = add_action(
324 self, N_('Browse Current Branch...'),
325 partial(guicmds.browse_current, context))
326 self.browse_other_branch_action = add_action(
327 self, N_('Browse Other Branch...'),
328 partial(guicmds.browse_other, context))
329 self.load_commitmsg_template_action = add_action(
330 self, N_('Get Commit Message Template'),
331 cmds.run(cmds.LoadCommitMessageFromTemplate, context))
332 self.help_about_action = add_action(
333 self, N_('About'), partial(about.about_dialog, context))
335 self.diff_expression_action = add_action(
336 self, N_('Expression...'),
337 partial(guicmds.diff_expression, context))
338 self.branch_compare_action = add_action(
339 self, N_('Branches...'),
340 partial(compare.compare_branches, context))
342 self.create_tag_action = add_action(
343 self, N_('Create Tag...'),
344 partial(createtag.create_tag, context, settings=settings))
346 self.create_branch_action = add_action(
347 self, N_('Create...'),
348 partial(createbranch.create_new_branch, context,
349 settings=settings),
350 hotkeys.BRANCH)
351 self.create_branch_action.setIcon(icons.branch())
353 self.delete_branch_action = add_action(
354 self, N_('Delete...'),
355 partial(guicmds.delete_branch, context))
357 self.delete_remote_branch_action = add_action(
358 self, N_('Delete Remote Branch...'),
359 partial(guicmds.delete_remote_branch, context))
361 self.rename_branch_action = add_action(
362 self, N_('Rename Branch...'),
363 partial(guicmds.rename_branch, context))
365 self.checkout_branch_action = add_action(
366 self, N_('Checkout...'),
367 partial(guicmds.checkout_branch, context),
368 hotkeys.CHECKOUT)
369 self.branch_review_action = add_action(
370 self, N_('Review...'),
371 partial(guicmds.review_branch, context))
373 self.browse_action = add_action(
374 self, N_('File Browser...'),
375 partial(browse.worktree_browser, context))
376 self.browse_action.setIcon(icons.cola())
378 self.dag_action = add_action(self, N_('DAG...'), self.git_dag)
379 self.dag_action.setIcon(icons.cola())
381 self.rebase_start_action = add_action(
382 self, N_('Start Interactive Rebase...'),
383 cmds.run(cmds.Rebase, context), hotkeys.REBASE_START_AND_CONTINUE)
385 self.rebase_edit_todo_action = add_action(
386 self, N_('Edit...'), cmds.run(cmds.RebaseEditTodo, context))
388 self.rebase_continue_action = add_action(
389 self, N_('Continue'), cmds.run(cmds.RebaseContinue, context),
390 hotkeys.REBASE_START_AND_CONTINUE)
392 self.rebase_skip_action = add_action(
393 self, N_('Skip Current Patch'), cmds.run(cmds.RebaseSkip, context))
395 self.rebase_abort_action = add_action(
396 self, N_('Abort'), cmds.run(cmds.RebaseAbort, context))
398 # For "Start Rebase" only, reverse the first argument to setEnabled()
399 # so that we can operate on it as a group.
400 # We can do this because can_rebase == not is_rebasing
401 self.rebase_start_action_proxy = utils.Proxy(
402 self.rebase_start_action,
403 setEnabled=lambda x: self.rebase_start_action.setEnabled(not x))
405 self.rebase_group = utils.Group(self.rebase_start_action_proxy,
406 self.rebase_edit_todo_action,
407 self.rebase_continue_action,
408 self.rebase_skip_action,
409 self.rebase_abort_action)
411 self.annex_init_action = qtutils.add_action(
412 self, N_('Initialize Git Annex'),
413 cmds.run(cmds.AnnexInit, context))
415 self.lfs_init_action = qtutils.add_action(
416 self, N_('Initialize Git LFS'), cmds.run(cmds.LFSInstall, context))
418 self.lock_layout_action = add_action_bool(
419 self, N_('Lock Layout'), self.set_lock_layout, False)
421 # Create the application menu
422 self.menubar = QtWidgets.QMenuBar(self)
423 self.setMenuBar(self.menubar)
425 # File Menu
426 add_menu = qtutils.add_menu
427 self.file_menu = add_menu(N_('&File'), self.menubar)
428 # File->Open Recent menu
429 self.open_recent_menu = self.file_menu.addMenu(N_('Open Recent'))
430 self.open_recent_menu.setIcon(icons.folder())
431 self.file_menu.addAction(self.open_repo_action)
432 self.file_menu.addAction(self.open_repo_new_action)
433 self.file_menu.addSeparator()
434 self.file_menu.addAction(self.new_repository_action)
435 self.file_menu.addAction(self.new_bare_repository_action)
436 self.file_menu.addAction(self.clone_repo_action)
437 self.file_menu.addSeparator()
438 self.file_menu.addAction(self.rescan_action)
439 self.file_menu.addAction(self.find_files_action)
440 self.file_menu.addAction(self.edit_remotes_action)
441 self.file_menu.addAction(self.browse_recently_modified_action)
442 self.file_menu.addSeparator()
443 self.file_menu.addAction(self.apply_patches_action)
444 self.file_menu.addAction(self.export_patches_action)
445 self.file_menu.addAction(self.save_tarball_action)
447 # Git Annex / Git LFS
448 annex = core.find_executable('git-annex')
449 lfs = core.find_executable('git-lfs')
450 if annex or lfs:
451 self.file_menu.addSeparator()
452 if annex:
453 self.file_menu.addAction(self.annex_init_action)
454 if lfs:
455 self.file_menu.addAction(self.lfs_init_action)
457 self.file_menu.addSeparator()
458 self.file_menu.addAction(self.preferences_action)
459 self.file_menu.addAction(self.quit_action)
461 # Edit Menu
462 self.edit_proxy = edit_proxy = (
463 FocusProxy(editor, editor.summary, editor.description))
465 copy_widgets = (
466 self, editor.summary, editor.description, self.diffeditor,
467 bookmarkswidget.tree, recentwidget.tree,
469 edit_proxy.override('copy', copy_widgets)
470 edit_proxy.override('selectAll', copy_widgets)
472 edit_menu = self.edit_menu = add_menu(N_('&Edit'), self.menubar)
473 add_action(edit_menu, N_('Undo'), edit_proxy.undo, hotkeys.UNDO)
474 add_action(edit_menu, N_('Redo'), edit_proxy.redo, hotkeys.REDO)
475 edit_menu.addSeparator()
476 add_action(edit_menu, N_('Cut'), edit_proxy.cut, hotkeys.CUT)
477 add_action(edit_menu, N_('Copy'), edit_proxy.copy, hotkeys.COPY)
478 add_action(edit_menu, N_('Paste'), edit_proxy.paste, hotkeys.PASTE)
479 add_action(edit_menu, N_('Delete'), edit_proxy.delete, hotkeys.DELETE)
480 edit_menu.addSeparator()
481 add_action(edit_menu, N_('Select All'), edit_proxy.selectAll,
482 hotkeys.SELECT_ALL)
483 edit_menu.addSeparator()
485 commitmsg.add_menu_actions(edit_menu, self.commiteditor.menu_actions)
487 # Actions menu
488 self.actions_menu = add_menu(N_('Actions'), self.menubar)
489 self.actions_menu.addAction(self.fetch_action)
490 self.actions_menu.addAction(self.push_action)
491 self.actions_menu.addAction(self.pull_action)
492 self.actions_menu.addAction(self.stash_action)
493 self.actions_menu.addSeparator()
494 self.actions_menu.addAction(self.create_tag_action)
495 self.actions_menu.addAction(self.cherry_pick_action)
496 self.actions_menu.addAction(self.merge_local_action)
497 self.actions_menu.addAction(self.merge_abort_action)
498 self.actions_menu.addSeparator()
499 self.actions_menu.addAction(self.update_submodules_action)
500 self.actions_menu.addSeparator()
501 self.actions_reset_menu = self.actions_menu.addMenu(N_('Reset'))
502 self.actions_reset_menu.addAction(self.reset_branch_head_action)
503 self.actions_reset_menu.addAction(self.reset_worktree_action)
504 self.actions_menu.addSeparator()
505 self.actions_menu.addAction(self.grep_action)
506 self.actions_menu.addAction(self.search_commits_action)
508 # Commit Menu
509 self.commit_menu = add_menu(N_('Commit@@verb'), self.menubar)
510 self.commit_menu.setTitle(N_('Commit@@verb'))
511 self.commit_menu.addAction(self.commiteditor.commit_action)
512 self.commit_menu.addAction(self.commit_amend_action)
513 self.commit_menu.addSeparator()
514 self.commit_menu.addAction(self.stage_modified_action)
515 self.commit_menu.addAction(self.stage_untracked_action)
516 self.commit_menu.addSeparator()
517 self.commit_menu.addAction(self.unstage_all_action)
518 self.commit_menu.addAction(self.unstage_selected_action)
519 self.commit_menu.addSeparator()
520 self.commit_menu.addAction(self.load_commitmsg_action)
521 self.commit_menu.addAction(self.load_commitmsg_template_action)
522 self.commit_menu.addAction(self.prepare_commitmsg_hook_action)
524 # Diff Menu
525 self.diff_menu = add_menu(N_('Diff'), self.menubar)
526 self.diff_menu.addAction(self.diff_expression_action)
527 self.diff_menu.addAction(self.branch_compare_action)
528 self.diff_menu.addSeparator()
529 self.diff_menu.addAction(self.show_diffstat_action)
531 # Branch Menu
532 self.branch_menu = add_menu(N_('Branch'), self.menubar)
533 self.branch_menu.addAction(self.branch_review_action)
534 self.branch_menu.addSeparator()
535 self.branch_menu.addAction(self.create_branch_action)
536 self.branch_menu.addAction(self.checkout_branch_action)
537 self.branch_menu.addAction(self.delete_branch_action)
538 self.branch_menu.addAction(self.delete_remote_branch_action)
539 self.branch_menu.addAction(self.rename_branch_action)
540 self.branch_menu.addSeparator()
541 self.branch_menu.addAction(self.browse_branch_action)
542 self.branch_menu.addAction(self.browse_other_branch_action)
543 self.branch_menu.addSeparator()
544 self.branch_menu.addAction(self.visualize_current_action)
545 self.branch_menu.addAction(self.visualize_all_action)
547 # Rebase menu
548 self.rebase_menu = add_menu(N_('Rebase'), self.actions_menu)
549 self.rebase_menu.addAction(self.rebase_start_action)
550 self.rebase_menu.addAction(self.rebase_edit_todo_action)
551 self.rebase_menu.addSeparator()
552 self.rebase_menu.addAction(self.rebase_continue_action)
553 self.rebase_menu.addAction(self.rebase_skip_action)
554 self.rebase_menu.addSeparator()
555 self.rebase_menu.addAction(self.rebase_abort_action)
557 # View Menu
558 self.view_menu = add_menu(N_('View'), self.menubar)
559 self.view_menu.aboutToShow.connect(
560 lambda: self.build_view_menu(self.view_menu))
561 self.setup_dockwidget_view_menu()
562 if utils.is_darwin():
563 # TODO or self.menubar.setNativeMenuBar(False)
564 # Since native OSX menu doesn't show empty entries
565 self.build_view_menu(self.view_menu)
567 # Help Menu
568 self.help_menu = add_menu(N_('Help'), self.menubar)
569 self.help_menu.addAction(self.help_docs_action)
570 self.help_menu.addAction(self.help_shortcuts_action)
571 self.help_menu.addAction(self.help_about_action)
573 # Arrange dock widgets
574 bottom = Qt.BottomDockWidgetArea
575 top = Qt.TopDockWidgetArea
577 self.addDockWidget(top, self.statusdock)
578 self.addDockWidget(top, self.commitdock)
579 if self.browser_dockable:
580 self.addDockWidget(top, self.browserdock)
581 self.tabifyDockWidget(self.browserdock, self.commitdock)
582 self.addDockWidget(top, self.bookmarksdock)
583 self.addDockWidget(top, self.branchdock)
584 self.addDockWidget(top, self.recentdock)
585 self.addDockWidget(bottom, self.diffdock)
586 self.addDockWidget(bottom, self.actionsdock)
587 self.addDockWidget(bottom, self.logdock)
588 self.tabifyDockWidget(self.actionsdock, self.logdock)
590 # Listen for model notifications
591 model.add_observer(model.message_updated, self.updated.emit)
592 model.add_observer(model.message_mode_changed,
593 lambda mode: self.updated.emit())
595 prefs_model.add_observer(prefs_model.message_config_updated,
596 self._config_updated)
598 # Set a default value
599 self.show_cursor_position(1, 0)
601 self.commit_menu.aboutToShow.connect(self.update_menu_actions)
602 self.open_recent_menu.aboutToShow.connect(self.build_recent_menu)
603 self.commiteditor.cursor_changed.connect(self.show_cursor_position)
605 self.diffeditor.options_changed.connect(self.statuswidget.refresh)
606 self.diffeditor.up.connect(self.statuswidget.move_up)
607 self.diffeditor.down.connect(self.statuswidget.move_down)
609 self.commiteditor.up.connect(self.statuswidget.move_up)
610 self.commiteditor.down.connect(self.statuswidget.move_down)
612 self.updated.connect(self.refresh, type=Qt.QueuedConnection)
614 self.config_actions_changed.connect(self._install_config_actions,
615 type=Qt.QueuedConnection)
616 self.init_state(settings, self.set_initial_size)
618 # Route command output here
619 Interaction.log_status = self.logwidget.log_status
620 Interaction.log = self.logwidget.log
621 # Focus the status widget; this must be deferred
622 QtCore.QTimer.singleShot(0, self.initialize)
624 def initialize(self):
625 context = self.context
626 git_version = version.git_version_str(context)
627 if git_version:
628 ok = True
629 Interaction.log(git_version + '\n' +
630 N_('git cola version %s') % version.version())
631 else:
632 ok = False
633 error_msg = N_('error: unable to execute git')
634 Interaction.log(error_msg)
636 if ok:
637 self.statuswidget.setFocus()
638 else:
639 title = N_('error: unable to execute git')
640 msg = title
641 details = ''
642 if WIN32:
643 details = git.win32_git_error_hint()
644 Interaction.critical(title, message=msg, details=details)
645 self.context.app.exit(2)
647 def set_initial_size(self):
648 # Default size; this is thrown out when save/restore is used
649 width, height = qtutils.desktop_size()
650 self.resize((width*3)//4, height)
651 self.statuswidget.set_initial_size()
652 self.commiteditor.set_initial_size()
654 def set_filter(self, txt):
655 self.statuswidget.set_filter(txt)
657 # Qt overrides
658 def closeEvent(self, event):
659 """Save state in the settings"""
660 commit_msg = self.commiteditor.commit_message(raw=True)
661 self.model.save_commitmsg(msg=commit_msg)
662 standard.MainWindow.closeEvent(self, event)
664 def create_view_menu(self):
665 menu = qtutils.create_menu(N_('View'), self)
666 self.build_view_menu(menu)
667 return menu
669 def build_view_menu(self, menu):
670 menu.clear()
671 menu.addAction(self.browse_action)
672 menu.addAction(self.dag_action)
673 menu.addSeparator()
675 popup_menu = self.createPopupMenu()
676 for menu_action in popup_menu.actions():
677 menu_action.setParent(menu)
678 menu.addAction(menu_action)
680 menu.addSeparator()
681 context = self.context
682 menu_action = menu.addAction(
683 N_('Add Toolbar'), partial(toolbar.add_toolbar, context, self))
684 menu_action.setIcon(icons.add())
686 dockwidgets = [
687 self.logdock,
688 self.commitdock,
689 self.statusdock,
690 self.diffdock,
691 self.actionsdock,
692 self.bookmarksdock,
693 self.recentdock,
694 self.branchdock,
695 self.submodulesdock
697 if self.browser_dockable:
698 dockwidgets.append(self.browserdock)
700 for dockwidget in dockwidgets:
701 # Associate the action with the shortcut
702 toggleview = dockwidget.toggleViewAction()
703 menu.addAction(toggleview)
705 menu.addSeparator()
706 menu.addAction(self.lock_layout_action)
708 return menu
710 def contextMenuEvent(self, event):
711 menu = self.create_view_menu()
712 menu.exec_(event.globalPos())
714 def build_recent_menu(self):
715 settings = Settings()
716 settings.load()
717 cmd = cmds.OpenRepo
718 context = self.context
719 menu = self.open_recent_menu
720 menu.clear()
721 for entry in settings.recent:
722 name = entry['name']
723 directory = entry['path']
724 text = '%s %s %s' % (name, uchr(0x2192), directory)
725 menu.addAction(text, cmds.run(cmd, context, directory))
727 # Accessors
728 mode = property(lambda self: self.model.mode)
730 def _config_updated(self, _source, config, value):
731 if config == prefs.FONTDIFF:
732 # The diff font
733 font = QtGui.QFont()
734 if not font.fromString(value):
735 return
736 self.logwidget.setFont(font)
737 self.diffeditor.setFont(font)
738 self.commiteditor.setFont(font)
740 elif config == prefs.TABWIDTH:
741 # This can be set locally or globally, so we have to use the
742 # effective value otherwise we'll update when we shouldn't.
743 # For example, if this value is overridden locally, and the
744 # global value is tweaked, we should not update.
745 value = prefs.tabwidth(self.context)
746 self.diffeditor.set_tabwidth(value)
747 self.commiteditor.set_tabwidth(value)
749 elif config == prefs.EXPANDTAB:
750 self.commiteditor.set_expandtab(value)
752 elif config == prefs.LINEBREAK:
753 # enables automatic line breaks
754 self.commiteditor.set_linebreak(value)
756 elif config == prefs.SORT_BOOKMARKS:
757 self.bookmarksdock.widget().reload_bookmarks()
759 elif config == prefs.TEXTWIDTH:
760 # Use the effective value for the same reason as tabwidth.
761 value = prefs.textwidth(self.context)
762 self.commiteditor.set_textwidth(value)
764 elif config == prefs.SHOW_PATH:
765 # the path in the window title was toggled
766 self.refresh_window_title()
768 def start(self, context):
769 """Do the expensive "get_config_actions()" call in the background"""
770 # Install .git-config-defined actions
771 task = qtutils.SimpleTask(self, self.get_config_actions)
772 context.runtask.start(task)
774 def get_config_actions(self):
775 actions = cfgactions.get_config_actions(self.context)
776 self.config_actions_changed.emit(actions)
778 def _install_config_actions(self, names_and_shortcuts):
779 """Install .gitconfig-defined actions"""
780 if not names_and_shortcuts:
781 return
782 context = self.context
783 menu = self.actions_menu
784 menu.addSeparator()
785 for (name, shortcut) in names_and_shortcuts:
786 callback = cmds.run(cmds.RunConfigAction, context, name)
787 menu_action = menu.addAction(name, callback)
788 if shortcut:
789 menu_action.setShortcut(shortcut)
791 def refresh(self):
792 """Update the title with the current branch and directory name."""
793 curbranch = self.model.currentbranch
794 curdir = core.getcwd()
795 is_merging = self.model.is_merging
796 is_rebasing = self.model.is_rebasing
798 msg = N_('Repository: %s') % curdir
799 msg += '\n'
800 msg += N_('Branch: %s') % curbranch
802 if is_rebasing:
803 msg += '\n\n'
804 msg += N_('This repository is currently being rebased.\n'
805 'Resolve conflicts, commit changes, and run:\n'
806 ' Rebase > Continue')
808 elif is_merging:
809 msg += '\n\n'
810 msg += N_('This repository is in the middle of a merge.\n'
811 'Resolve conflicts and commit changes.')
813 self.refresh_window_title()
815 if self.mode == self.model.mode_amend:
816 self.commit_amend_action.setChecked(True)
817 else:
818 self.commit_amend_action.setChecked(False)
820 self.commitdock.setToolTip(msg)
821 self.commiteditor.set_mode(self.mode)
822 self.update_actions()
824 def refresh_window_title(self):
825 """Refresh the window title when state changes"""
826 alerts = []
828 project = self.model.project
829 curbranch = self.model.currentbranch
830 is_merging = self.model.is_merging
831 is_rebasing = self.model.is_rebasing
832 prefix = uchr(0xab)
833 suffix = uchr(0xbb)
835 if is_rebasing:
836 alerts.append(N_('Rebasing'))
837 elif is_merging:
838 alerts.append(N_('Merging'))
840 if self.mode == self.model.mode_amend:
841 alerts.append(N_('Amending'))
843 if alerts:
844 alert_text = (prefix + ' %s ' + suffix + ' ') % ', '.join(alerts)
845 else:
846 alert_text = ''
848 if self.model.cfg.get(prefs.SHOW_PATH, True):
849 path_text = self.git.worktree()
850 else:
851 path_text = ''
853 title = '%s: %s %s%s' % (project, curbranch, alert_text, path_text)
854 self.setWindowTitle(title)
856 def update_actions(self):
857 is_rebasing = self.model.is_rebasing
858 self.rebase_group.setEnabled(is_rebasing)
860 enabled = not self.model.is_empty_repository()
861 self.rename_branch_action.setEnabled(enabled)
862 self.delete_branch_action.setEnabled(enabled)
864 self.annex_init_action.setEnabled(not self.model.annex)
865 self.lfs_init_action.setEnabled(not self.model.lfs)
867 def update_menu_actions(self):
868 # Enable the Prepare Commit Message action if the hook exists
869 hook = gitcmds.prepare_commit_message_hook(self.context)
870 enabled = os.path.exists(hook)
871 self.prepare_commitmsg_hook_action.setEnabled(enabled)
873 def export_state(self):
874 state = standard.MainWindow.export_state(self)
875 show_status_filter = self.statuswidget.filter_widget.isVisible()
876 state['show_status_filter'] = show_status_filter
877 state['toolbars'] = self.toolbar_state.export_state()
878 self.diffviewer.export_state(state)
880 return state
882 def apply_state(self, state):
883 """Imports data for save/restore"""
884 base_ok = standard.MainWindow.apply_state(self, state)
885 lock_layout = state.get('lock_layout', False)
886 self.lock_layout_action.setChecked(lock_layout)
888 show_status_filter = state.get('show_status_filter', False)
889 self.statuswidget.filter_widget.setVisible(show_status_filter)
891 toolbars = state.get('toolbars', [])
892 self.toolbar_state.apply_state(toolbars)
894 diff_ok = self.diffviewer.apply_state(state)
895 return base_ok and diff_ok
897 def setup_dockwidget_view_menu(self):
898 # Hotkeys for toggling the dock widgets
899 if utils.is_darwin():
900 optkey = 'Meta'
901 else:
902 optkey = 'Ctrl'
903 dockwidgets = (
904 (optkey + '+0', self.logdock),
905 (optkey + '+1', self.commitdock),
906 (optkey + '+2', self.statusdock),
907 (optkey + '+3', self.diffdock),
908 (optkey + '+4', self.actionsdock),
909 (optkey + '+5', self.bookmarksdock),
910 (optkey + '+6', self.recentdock),
911 (optkey + '+7', self.branchdock),
912 (optkey + '+8', self.submodulesdock)
914 for shortcut, dockwidget in dockwidgets:
915 # Associate the action with the shortcut
916 toggleview = dockwidget.toggleViewAction()
917 toggleview.setShortcut('Shift+' + shortcut)
919 def showdock(show, dockwidget=dockwidget):
920 if show:
921 dockwidget.raise_()
922 dockwidget.widget().setFocus()
923 else:
924 self.setFocus()
926 self.addAction(toggleview)
927 qtutils.connect_action_bool(toggleview, showdock)
929 # Create a new shortcut Shift+<shortcut> that gives focus
930 toggleview = QtWidgets.QAction(self)
931 toggleview.setShortcut(shortcut)
933 def focusdock(dockwidget=dockwidget):
934 focus_dock(dockwidget)
935 self.addAction(toggleview)
936 qtutils.connect_action(toggleview, focusdock)
938 # These widgets warrant home-row hotkey status
939 qtutils.add_action(self, 'Focus Commit Message',
940 lambda: focus_dock(self.commitdock),
941 hotkeys.FOCUS)
943 qtutils.add_action(self, 'Focus Status Window',
944 lambda: focus_dock(self.statusdock),
945 hotkeys.FOCUS_STATUS)
947 qtutils.add_action(self, 'Focus Diff Editor',
948 lambda: focus_dock(self.diffdock),
949 hotkeys.FOCUS_DIFF)
951 def git_dag(self):
952 self.dag = dag.git_dag(self.context, existing_view=self.dag)
954 def show_cursor_position(self, rows, cols):
955 display = '%02d:%02d' % (rows, cols)
956 css = """
957 <style>
958 .good {
960 .first-warning {
961 color: black;
962 background-color: yellow;
964 .second-warning {
965 color: black;
966 background-color: #f83;
968 .error {
969 color: white;
970 background-color: red;
972 </style>
975 if cols > 78:
976 cls = 'error'
977 elif cols > 72:
978 cls = 'second-warning'
979 elif cols > 64:
980 cls = 'first-warning'
981 else:
982 cls = 'good'
983 div = ('<div class="%s">%s</div>' % (cls, display))
984 self.position_label.setText(css + div)
987 class FocusProxy(object):
988 """Proxy over child widgets and operate on the focused widget"""
990 def __init__(self, *widgets):
991 self.widgets = widgets
992 self.overrides = {}
994 def override(self, name, widgets):
995 self.overrides[name] = widgets
997 def focus(self, name):
998 """Return the currently focused widget"""
999 widgets = self.overrides.get(name, self.widgets)
1000 # The parent must be the parent of all the proxied widgets
1001 parent = widgets[0]
1002 # The first widget is used as a fallback
1003 fallback = widgets[1]
1004 # We ignore the parent when delegating to child widgets
1005 widgets = widgets[1:]
1007 focus = parent.focusWidget()
1008 if focus not in widgets:
1009 focus = fallback
1010 return focus
1012 def __getattr__(self, name):
1013 """Return a callback that calls a common child method"""
1014 def callback():
1015 focus = self.focus(name)
1016 fn = getattr(focus, name, None)
1017 if fn:
1018 fn()
1019 return callback
1021 def delete(self):
1022 """Specialized delete() to deal with QLineEdit vs QTextEdit"""
1023 focus = self.focus('delete')
1024 if hasattr(focus, 'del_'):
1025 focus.del_()
1026 elif hasattr(focus, 'textCursor'):
1027 focus.textCursor().deleteChar()
1030 def show_dock(dockwidget):
1031 dockwidget.raise_()
1032 dockwidget.widget().setFocus()
1035 def focus_dock(dockwidget):
1036 if get(dockwidget.toggleViewAction()):
1037 show_dock(dockwidget)
1038 else:
1039 dockwidget.toggleViewAction().trigger()