Fix disconnected widgets on macosx
[git-cola.git] / cola / controllers / __init__.py
blob3f4e9173140c4e502816598f5f58a33dc748672a
1 #!/usr/bin/env python
2 import os
3 import sys
4 import time
5 import glob
6 import platform
8 from PyQt4 import QtCore
9 from PyQt4 import QtGui
10 from PyQt4.QtGui import QDialog
11 from PyQt4.QtGui import QMessageBox
12 from PyQt4.QtGui import QMenu
13 from PyQt4.QtGui import QFont
15 from cola import utils
16 from cola import qtutils
17 from cola import defaults
18 from cola.qobserver import QObserver
20 # controllers namespace
21 import search
22 from util import logger
23 from remote import remote_action
24 from util import choose_from_list
25 from util import choose_from_combo
26 from util import select_commits
27 from util import update_options
28 from repobrowser import browse_git_branch
29 from createbranch import create_new_branch
30 from search import search_commits
31 from merge import local_merge
32 from merge import abort_merge
33 from bookmark import save_bookmark
34 from bookmark import manage_bookmarks
35 from stash import stash
38 class Controller(QObserver):
39 """Manages the interaction between models and views."""
41 MODE_NONE = 0
42 MODE_WORKTREE = 1
43 MODE_INDEX = 2
44 MODE_BRANCH = 3
46 def init(self, model, view):
47 """
48 State machine:
49 Modes are:
50 none -> do nothing, disables most context menus
51 branch -> diff against another branch and selectively choose changes
52 worktree -> selectively add working changes to the index
53 index -> selectively remove changes from the index
54 """
55 self.mode = Controller.MODE_NONE
57 # parent-less log window
58 qtutils.LOGGER = logger()
60 # Unstaged changes context menu
61 view.unstaged.contextMenuEvent = self.unstaged_context_menu_event
63 # Diff display context menu
64 view.display_text.contextMenuEvent = self.diff_context_menu_event
66 # Binds model params to their equivalent view widget
67 self.add_observables('commitmsg', 'staged', 'unstaged')
69 # When a model attribute changes, this runs a specific action
70 self.add_actions(staged = self.action_staged)
71 self.add_actions(unstaged = self.action_unstaged)
72 self.add_actions(global_cola_fontdiff = self.update_diff_font)
73 self.add_actions(global_cola_fontui = self.update_ui_font)
75 self.add_callbacks(
76 # Push Buttons
77 signoff_button = self.model.add_signoff,
78 stage_button = self.stage_selected,
79 commit_button = self.commit,
80 fetch_button = self.fetch,
81 push_button = self.push,
82 pull_button = self.pull,
83 # List Widgets
84 staged = self.diff_staged,
85 unstaged = self.diff_unstaged,
86 # Checkboxes
87 untracked_checkbox = self.rescan,
89 # File Menu
90 menu_quit = self.quit_app,
91 menu_open_repo = self.open_repo,
92 menu_manage_bookmarks = manage_bookmarks,
93 menu_save_bookmark = save_bookmark,
95 # Edit Menu
96 menu_options = self.options,
97 menu_cut = self.view.action_cut,
98 menu_copy = self.view.action_copy,
99 menu_paste = self.view.action_paste,
100 menu_delete = self.view.action_delete,
101 menu_select_all = self.view.action_select_all,
102 menu_undo = self.view.action_undo,
103 menu_redo = self.view.action_redo,
105 # Search Menu
106 menu_search_grep = self.grep,
107 menu_search_revision =
108 self.gen_search( search.REVISION_ID ),
109 menu_search_revision_range =
110 self.gen_search( search.REVISION_RANGE ),
111 menu_search_message =
112 self.gen_search( search.MESSAGE ),
113 menu_search_path =
114 self.gen_search( search.PATH, True ),
115 menu_search_date_range =
116 self.gen_search( search.DATE_RANGE ),
117 menu_search_diff =
118 self.gen_search( search.DIFF ),
119 menu_search_author =
120 self.gen_search( search.AUTHOR ),
121 menu_search_committer =
122 self.gen_search( search.COMMITTER ),
124 # Merge Menu
125 menu_merge_local =
126 lambda: local_merge( self.model, self.view ),
127 menu_merge_abort =
128 lambda: abort_merge( self.model, self.view ),
130 # Repository Menu
131 menu_visualize_current = self.viz_current,
132 menu_visualize_all = self.viz_all,
133 menu_browse_commits = self.browse_commits,
134 menu_browse_branch = self.browse_current,
135 menu_browse_other_branch = self.browse_other,
137 # Branch Menu
138 menu_create_branch = self.branch_create,
139 menu_checkout_branch = self.checkout_branch,
140 menu_diff_branch = self.diff_branch,
141 menu_diffedit_branch = self.diffedit_branch,
143 # Commit Menu
144 menu_rescan = self.rescan,
145 menu_delete_branch = self.branch_delete,
146 menu_rebase_branch = self.rebase,
147 menu_commit = self.commit,
148 menu_stage_selected = self.stage_selected,
149 menu_unstage_selected = self.unstage_selected,
150 menu_show_diffstat = self.show_diffstat,
151 menu_show_index = self.show_index,
152 menu_export_patches = self.export_patches,
153 menu_stash =
154 lambda: stash( self.model, self.view ),
155 menu_load_commitmsg = self.load_commitmsg,
156 menu_cherry_pick = self.cherry_pick,
157 menu_get_prev_commitmsg = model.get_prev_commitmsg,
158 menu_stage_modified =
159 lambda: self.log(self.model.stage_modified()),
160 menu_stage_untracked =
161 lambda: self.log(self.model.stage_untracked()),
162 menu_unstage_all =
163 lambda: self.log(self.model.unstage_all()),
166 # Delegate window events here
167 view.moveEvent = self.move_event
168 view.resizeEvent = self.resize_event
169 view.closeEvent = self.quit_app
170 view.staged.mousePressEvent = self.click_staged
171 view.unstaged.mousePressEvent = self.click_unstaged
173 # These are vanilla signal/slots since QObserver
174 # is already handling these signals.
175 self.connect(view.unstaged,
176 'itemDoubleClicked(QListWidgetItem*)',
177 self.stage_selected)
178 self.connect(view.staged,
179 'itemDoubleClicked(QListWidgetItem*)',
180 self.unstage_selected)
182 # Toolbar log button
183 self.connect(view.toolbar_show_log,
184 'triggered()', self.show_log)
186 self.connect(view.diff_dock,
187 'topLevelChanged(bool)',
188 lambda(b): self.setwindow(view.diff_dock, b))
190 self.connect(view.editor_dock,
191 'topLevelChanged(bool)',
192 lambda(b): self.setwindow(view.editor_dock, b))
194 self.connect(view.status_dock,
195 'topLevelChanged(bool)',
196 lambda(b): self.setwindow(view.status_dock, b))
198 self.init_log_window()
199 self.load_gui_settings()
200 self.rescan()
201 self.refresh_view('global_cola_fontdiff', 'global_cola_fontui')
202 self.start_inotify_thread()
204 def setwindow(self, dock, isfloating):
205 if isfloating:
206 if platform.system() != 'Windows' and 'Macintosh' not in platform.platform():
207 flags = ( QtCore.Qt.Window
208 | QtCore.Qt.FramelessWindowHint )
209 dock.setWindowFlags( flags )
210 dock.show()
212 #####################################################################
213 # handle when the listitem icons are clicked
214 def click_event(self, widget, action_callback, event):
215 result = QtGui.QListWidget.mousePressEvent(widget, event)
216 xpos = event.pos().x()
217 if xpos > 5 and xpos < 20:
218 action_callback()
219 return result
221 def click_staged(self, event):
222 return self.click_event(self.view.staged,
223 self.unstage_selected, event)
225 def click_unstaged(self, event):
226 return self.click_event(self.view.unstaged,
227 self.stage_selected, event)
229 #####################################################################
230 # event() is called in response to messages from the inotify thread
231 def event(self, msg):
232 if msg.type() == defaults.INOTIFY_EVENT:
233 self.rescan()
234 return True
235 else:
236 return False
238 #####################################################################
239 # Actions triggered during model updates
241 def action_staged(self, widget):
242 qtutils.update_listwidget(widget,
243 self.model.get_staged(),
244 staged=True)
245 self.view.editor_dock.raise_()
247 def action_unstaged(self, widget):
248 qtutils.update_listwidget(widget,
249 self.model.get_modified(),
250 staged=False)
251 if self.view.untracked_checkbox.isChecked():
252 qtutils.update_listwidget(widget,
253 self.model.get_untracked(),
254 staged=False,
255 append=True,
256 untracked=True)
258 #####################################################################
259 # Qt callbacks
260 def gen_search(self, searchtype, browse=False):
261 def search_handler():
262 search_commits(self.model, searchtype, browse)
263 return search_handler
265 def grep(self):
266 txt, ok = qtutils.input("grep")
267 if not ok:
268 return
269 stuff = self.model.grep(txt)
270 self.view.display_text.setText(stuff)
271 self.view.diff_dock.raise_()
273 def show_log(self, *rest):
274 qtutils.toggle_log_window()
276 def options(self):
277 update_options(self.model, self.view)
279 def branch_create(self):
280 if create_new_branch(self.model, self.view):
281 self.rescan()
283 def branch_delete(self):
284 branch = choose_from_combo('Delete Branch',
285 self.view,
286 self.model.get_local_branches())
287 if not branch:
288 return
289 self.log(self.model.delete_branch(branch))
291 def browse_current(self):
292 branch = self.model.get_currentbranch()
293 browse_git_branch(self.model, self.view, branch)
295 def browse_other(self):
296 # Prompt for a branch to browse
297 branch = choose_from_combo('Browse Branch Files',
298 self.view,
299 self.model.get_all_branches())
300 if not branch:
301 return
302 # Launch the repobrowser
303 browse_git_branch(self.model, self.view, branch)
305 def checkout_branch(self):
306 branch = choose_from_combo('Checkout Branch',
307 self.view,
308 self.model.get_local_branches())
309 if not branch:
310 return
311 self.log(self.model.checkout(branch))
313 def browse_commits(self):
314 self.select_commits_gui(self.tr('Browse Commits'),
315 *self.model.log_helper(all=True))
317 def cherry_pick(self):
318 commits = self.select_commits_gui(self.tr('Cherry-Pick Commits'),
319 *self.model.log_helper(all=True))
320 if not commits:
321 return
322 self.log(self.model.cherry_pick_list(commits))
324 def commit(self):
325 msg = self.model.get_commitmsg()
326 if not msg:
327 error_msg = self.tr(""
328 + "Please supply a commit message.\n"
329 + "\n"
330 + "A good commit message has the following format:\n"
331 + "\n"
332 + "- First line: Describe in one sentence what you did.\n"
333 + "- Second line: Blank\n"
334 + "- Remaining lines: Describe why this change is good.\n")
335 self.log(error_msg)
336 return
338 files = self.model.get_staged()
339 if not files and not self.view.amend_radio.isChecked():
340 error_msg = self.tr(""
341 + "No changes to commit.\n"
342 + "\n"
343 + "You must stage at least 1 file before you can commit.\n")
344 self.log(error_msg)
345 return
347 # Perform the commit
348 amend = self.view.amend_radio.isChecked()
349 output = self.model.commit_with_msg(msg, amend=amend)
351 # Reset state
352 self.view.new_commit_radio.setChecked(True)
353 self.view.amend_radio.setChecked(False)
354 self.model.set_commitmsg('')
355 self.log(output)
357 def view_diff(self, staged=True):
358 if staged:
359 self.mode = Controller.MODE_INDEX
360 widget = self.view.staged
361 else:
362 self.mode = Controller.MODE_WORKTREE
363 widget = self.view.unstaged
364 row, selected = qtutils.get_selected_row(widget)
365 if not selected:
366 self.mode = Controller.MODE_NONE
367 self.view.reset_display()
368 return
369 diff, status, filename = self.model.get_diff_details(row, staged=staged)
370 self.view.set_display(diff)
371 self.view.set_info(self.tr(status))
372 self.view.diff_dock.raise_()
373 qtutils.set_clipboard(filename)
375 def edit_file(self, staged=True):
376 if staged:
377 widget = self.view.staged
378 else:
379 widget = self.view.unstaged
380 row, selected = qtutils.get_selected_row(widget)
381 if not selected:
382 return
383 if staged:
384 filename = self.model.get_staged()[row]
385 else:
386 filename = self.model.get_unstaged()[row]
387 utils.fork(self.model.get_editor(), filename)
389 def launch_diffeditor(self, filename, tmpfile):
390 if self.model.get_cola_config('editdiffreverse'):
391 utils.fork(self.model.get_diffeditor(), tmpfile, filename)
392 else:
393 utils.fork(self.model.get_diffeditor(), filename, tmpfile)
395 def edit_diff(self, staged=True):
396 if staged:
397 widget = self.view.staged
398 else:
399 widget = self.view.unstaged
400 row, selected = qtutils.get_selected_row(widget)
401 diff, status, filename = self.model.get_diff_details(row, staged=staged)
402 if not selected:
403 return
404 contents = self.model.show("HEAD:"+filename, with_raw_output=True)
405 tmpfile = self.model.get_tmp_filename(filename)
406 fh = open(tmpfile, 'w')
407 fh.write(contents)
408 fh.close()
409 self.launch_diffeditor(filename, tmpfile)
411 # use *rest to handle being called from different signals
412 def diff_staged(self, *rest):
413 self.view_diff(staged=True)
415 # use *rest to handle being called from different signals
416 def diff_unstaged(self, *rest):
417 self.view_diff(staged=False)
419 def export_patches(self):
420 (revs, summaries) = self.model.log_helper()
421 to_export = self.select_commits_gui(self.tr('Export Patches'),
422 revs, summaries)
423 if not to_export:
424 return
425 to_export.reverse()
426 revs.reverse()
427 self.log(self.model.format_patch_helper(to_export,
428 revs,
429 output='patches'))
431 def open_repo(self):
432 """Spawns a new cola session"""
433 dirname = qtutils.opendir_dialog(self.view,
434 'Open Git Repository...',
435 os.getcwd())
436 if dirname:
437 utils.fork(sys.argv[0], dirname)
439 def quit_app(self, *args):
440 """Save config settings and cleanup any inotify threads."""
442 if self.model.save_at_exit():
443 self.model.save_gui_settings()
444 qtutils.close_log_window()
445 pattern = self.model.get_tmp_file_pattern()
446 for filename in glob.glob(pattern):
447 os.unlink(filename)
448 if self.inotify_thread and self.inotify_thread.isRunning():
449 self.inotify_thread.abort = True
450 self.inotify_thread.wait()
451 self.view.close()
453 def load_commitmsg(self):
454 file = qtutils.open_dialog(self.view,
455 'Load Commit Message...',
456 defaults.DIRECTORY)
457 if file:
458 defaults.DIRECTORY = os.path.dirname(file)
459 slushy = utils.slurp(file)
460 if slushy:
461 self.model.set_commitmsg(slushy)
463 def rebase(self):
464 branch = choose_from_combo('Rebase Branch',
465 self.view,
466 self.model.get_local_branches())
467 if not branch:
468 return
469 self.log(self.model.rebase(branch))
471 # use *rest to handle being called from the checkbox signal
472 def rescan(self, *rest):
473 """Populates view widgets with results from 'git status.'"""
475 # save entire selection
476 unstaged = qtutils.get_selection_list(self.view.unstaged,
477 self.model.get_unstaged())
478 staged = qtutils.get_selection_list(self.view.staged,
479 self.model.get_staged())
481 scrollbar = self.view.display_text.verticalScrollBar()
482 scrollvalue = scrollbar.value()
484 # save selected item
485 unstageditem = qtutils.get_selected_item(self.view.unstaged,
486 self.model.get_unstaged())
488 stageditem = qtutils.get_selected_item(self.view.staged,
489 self.model.get_staged())
491 # get new values
492 self.model.update_status()
494 # restore selection
495 update_staged = False
496 update_unstaged = False
497 updated_unstaged = self.model.get_unstaged()
498 updated_staged = self.model.get_staged()
500 for item in unstaged:
501 if item in updated_unstaged:
502 idx = updated_unstaged.index(item)
503 listitem = self.view.unstaged.item(idx)
504 if listitem:
505 listitem.setSelected(True)
506 self.view.unstaged.setItemSelected(listitem, True)
507 update_unstaged = True
508 self.view.unstaged.update()
509 for item in staged:
510 if item in updated_staged:
511 idx = updated_staged.index(item)
512 listitem = self.view.staged.item(idx)
513 if listitem:
514 listitem.setSelected(True)
515 self.view.staged.setItemSelected(listitem, True)
516 update_staged = True
518 # restore selected item
519 if update_unstaged and unstageditem:
520 idx = updated_unstaged.index(unstageditem)
521 item = self.view.unstaged.item(idx)
522 self.view.unstaged.setCurrentItem(item)
523 self.view_diff(False)
524 scrollbar.setValue(scrollvalue)
525 elif update_staged and stageditem:
526 idx = updated_staged.index(stageditem)
527 item = self.view.staged.item(idx)
528 self.view.staged.setCurrentItem(item)
529 self.view_diff(True)
530 scrollbar.setValue(scrollvalue)
532 # Update the title with the current branch
533 self.view.setWindowTitle('%s [%s]' % (
534 self.model.get_project(),
535 self.model.get_currentbranch()))
537 # Check if there's a message file in .git/
538 merge_msg_path = self.model.get_merge_message_path()
539 if merge_msg_path is None:
540 return
542 # A merge message file exists.
543 set_msg = False
544 if self.model.get_commitmsg():
545 # The commit message editor contains data.
546 # Prompt before overwriting the commit message
547 # with the contents of the merge message.
548 answer = qtutils.question(self.view,
549 self.tr('Import Commit Message?'),
550 self.tr('A commit message from an in-progress'
551 +' merge was found.\nImport it?'))
552 if answer:
553 set_msg = True
554 else:
555 set_msg = True
556 # Set the new commit message
557 if set_msg:
558 self.model.load_commitmsg(merge_msg_path)
559 self.view.editor_dock.raise_()
561 def fetch(self):
562 remote_action(self.model, self.view, "Fetch")
564 def push(self):
565 remote_action(self.model, self.view, "Push")
567 def pull(self):
568 remote_action(self.model, self.view, "Pull")
570 def show_diffstat(self):
571 """Show the diffstat from the latest commit."""
572 self.mode = Controller.MODE_NONE
573 self.view.set_info(self.tr('Diffstat'))
574 self.view.set_display(self.model.diffstat())
576 def show_index(self):
577 self.mode = Controller.MODE_NONE
578 self.view.set_info(self.tr('Index'))
579 self.view.set_display(self.model.diffindex())
581 #####################################################################
582 def diffedit_branch(self):
583 branch = choose_from_combo('Select Branch',
584 self.view,
585 self.model.get_all_branches())
586 if not branch:
587 return
588 zfiles_str = self.model.diff(branch, name_only=True, z=True)
589 files = zfiles_str.split('\0')
590 filename = choose_from_list('Select File', self.view, files)
591 if not filename:
592 return
593 contents = self.model.show('%s:%s' % (branch, filename), with_raw_output=True)
594 tmpfile = self.model.get_tmp_filename(filename)
595 fh = open(tmpfile, 'w')
596 fh.write(contents)
597 fh.close()
598 self.launch_diffeditor(filename, tmpfile)
600 # Set state machine to branch mode
601 self.mode = Controller.MODE_NONE
603 #####################################################################
604 # diff gui
605 def diff_branch(self):
606 branch = choose_from_combo('Select Branch',
607 self.view,
608 self.model.get_all_branches())
609 if not branch:
610 return
611 zfiles_str = self.model.diff(branch, name_only=True, z=True)
612 files = zfiles_str.split('\0')
613 filename = choose_from_list('Select File', self.view, files)
614 if not filename:
615 return
616 status = ('Diff of "%s" between the work tree and %s'
617 % (filename, branch))
619 diff = self.model.diff_helper(filename=filename,
620 cached=False,
621 reverse=True,
622 branch=branch)
623 self.view.set_display(diff)
624 self.view.set_info(self.tr(status))
625 self.view.diff_dock.raise_()
627 # Set state machine to branch mode
628 self.mode = Controller.MODE_BRANCH
629 self.branch = branch
630 self.filename = filename
632 def process_diff_selection(self, items, widget,
633 cached=True, selected=False, reverse=True):
635 if self.mode == Controller.MODE_BRANCH:
636 branch = self.branch
637 filename = self.filename
638 parser = utils.DiffParser(self.model,
639 filename=filename,
640 cached=False,
641 branch=branch)
642 offset, selection = self.view.diff_selection()
643 parser.process_diff_selection(selected, offset, selection,
644 branch=True)
645 self.rescan()
646 else:
647 filename = qtutils.get_selected_item(widget, items)
648 if not filename:
649 return
650 parser = utils.DiffParser(self.model,
651 filename=filename,
652 cached=cached)
653 offset, selection = self.view.diff_selection()
654 parser.process_diff_selection(selected, offset, selection)
655 self.rescan()
657 def stage_hunk(self):
658 self.process_diff_selection(self.model.get_unstaged(),
659 self.view.unstaged,
660 cached=False)
662 def stage_hunk_selection(self):
663 self.process_diff_selection(self.model.get_unstaged(),
664 self.view.unstaged,
665 cached=False,
666 selected=True)
668 def unstage_hunk(self, cached=True):
669 self.process_diff_selection(self.model.get_staged(),
670 self.view.staged,
671 cached=True)
673 def unstage_hunk_selection(self):
674 self.process_diff_selection(self.model.get_staged(),
675 self.view.staged,
676 cached=True,
677 selected=True)
679 # #######################################################################
680 # end diff gui
682 # *rest handles being called from different signals
683 def stage_selected(self,*rest):
684 """Use "git add" to add items to the git index.
685 This is a thin wrapper around map_to_listwidget."""
686 command = self.model.add_or_remove
687 widget = self.view.unstaged
688 items = self.model.get_unstaged()
689 self.map_to_listwidget(command, widget, items)
691 # *rest handles being called from different signals
692 def unstage_selected(self, *rest):
693 """Use "git reset" to remove items from the git index.
694 This is a thin wrapper around map_to_listwidget."""
695 command = self.model.reset_helper
696 widget = self.view.staged
697 items = self.model.get_staged()
698 self.map_to_listwidget(command, widget, items)
700 def undo_changes(self):
701 """Reverts local changes back to whatever's in HEAD."""
702 widget = self.view.unstaged
703 items = self.model.get_unstaged()
704 potential_items = qtutils.get_selection_list(widget, items)
705 items_to_undo = []
706 untracked = self.model.get_untracked()
707 for item in potential_items:
708 if item not in untracked:
709 items_to_undo.append(item)
710 if items_to_undo:
711 if not qtutils.question(self.view,
712 self.tr('Destroy Local Changes?'),
713 self.tr('This operation will drop all '
714 +'uncommitted changes. '
715 +'Continue?'),
716 default=False):
717 return
719 output = self.model.checkout('HEAD', '--', *items_to_undo)
720 self.log('git checkout HEAD -- '
721 +' '.join(items_to_undo)
722 +'\n' + output)
723 else:
724 msg = 'No files selected for checkout from HEAD.'
725 self.log(self.tr(msg))
727 def viz_all(self):
728 """Visualizes the entire git history using gitk."""
729 browser = self.model.get_history_browser()
730 utils.fork(browser,'--all')
732 def viz_current(self):
733 """Visualizes the current branch's history using gitk."""
734 browser = self.model.get_history_browser()
735 utils.fork(browser, self.model.get_currentbranch())
737 def move_event(self, event):
738 defaults.X = event.pos().x()
739 defaults.Y = event.pos().y()
741 def resize_event(self, event):
742 defaults.WIDTH = event.size().width()
743 defaults.HEIGHT = event.size().height()
745 def load_gui_settings(self):
746 if not self.model.remember_gui_settings():
747 return
748 (w,h,x,y,st0,st1,sb0,sb1) = self.model.get_window_geom()
749 self.view.resize(w,h)
750 self.view.move(x,y)
752 def log(self, output, rescan=True, quiet=False):
753 """Logs output and optionally rescans for changes."""
754 qtutils.log(output, quiet=quiet, doraise=False)
755 if rescan:
756 self.rescan()
758 def map_to_listwidget(self, command, widget, items):
759 """This is a helper method that retrieves the current
760 selection list, applies a command to that list,
761 displays a dialog showing the output of that command,
762 and calls rescan to pickup changes."""
763 apply_items = qtutils.get_selection_list(widget, items)
764 output = command(*apply_items)
765 self.log(output, quiet=True)
767 def unstaged_context_menu_event(self, event):
768 menu = self.unstaged_context_menu_setup()
769 unstaged = self.view.unstaged
770 menu.exec_(unstaged.mapToGlobal(event.pos()))
772 def unstaged_context_menu_setup(self):
773 unstaged_item = qtutils.get_selected_item(self.view.unstaged,
774 self.model.get_unstaged())
775 is_tracked = unstaged_item not in self.model.get_untracked()
776 enable_staging = self.mode == Controller.MODE_WORKTREE
777 enable_undo = enable_staging and is_tracked
779 menu = QMenu(self.view)
781 if enable_staging:
782 menu.addAction(self.tr('Stage Selected'), self.stage_selected)
783 if enable_undo:
784 menu.addAction(self.tr('Undo Local Changes'), self.undo_changes)
785 menu.addAction(self.tr('Launch Editor'),
786 lambda: self.edit_file(staged=False))
787 if enable_staging:
788 menu.addAction(self.tr('Launch Diff Editor'),
789 lambda: self.edit_diff(staged=False))
790 return menu
792 def diff_context_menu_event(self, event):
793 menu = self.diff_context_menu_setup()
794 textedit = self.view.display_text
795 menu.exec_(textedit.mapToGlobal(event.pos()))
797 def diff_context_menu_setup(self):
798 menu = QMenu(self.view)
799 if self.mode == Controller.MODE_WORKTREE:
800 unstaged_item = qtutils.get_selected_item(self.view.unstaged,
801 self.model.get_unstaged())
802 is_tracked= (unstaged_item
803 and unstaged_item not in self.model.get_untracked())
804 if is_tracked:
805 menu.addAction(self.tr('Stage Hunk For Commit'),
806 self.stage_hunk)
807 menu.addAction(self.tr('Stage Selected Lines'),
808 self.stage_hunk_selection)
810 elif self.mode == Controller.MODE_INDEX:
811 menu.addAction(self.tr('Unstage Hunk From Commit'),
812 self.unstage_hunk)
813 menu.addAction(self.tr('Unstage Selected Lines'),
814 self.unstage_hunk_selection)
816 elif self.mode == Controller.MODE_BRANCH:
817 menu.addAction(self.tr('Apply Diff To Work Tree'),
818 self.stage_hunk)
819 menu.addAction(self.tr('Apply Diff Selection To Work Tree'),
820 self.stage_hunk_selection)
822 menu.addAction(self.tr('Copy'),
823 self.view.copy_display)
824 return menu
826 def select_commits_gui(self, title, revs, summaries):
827 return select_commits(self.model, self.view, title, revs, summaries)
829 def update_diff_font(self):
830 font = self.model.get_global_cola_fontdiff()
831 if not font:
832 return
833 qfont = QFont()
834 qfont.fromString(font)
835 self.view.display_text.setFont(qfont)
836 self.view.commitmsg.setFont(qfont)
838 def update_ui_font(self):
839 font = self.model.get_global_cola_fontui()
840 if not font:
841 return
842 qfont = QFont()
843 qfont.fromString(font)
844 QtGui.qApp.setFont(qfont)
846 def init_log_window(self):
847 branch = self.model.get_currentbranch()
848 version = defaults.VERSION
849 qtutils.log(self.model.get_git_version()
850 +'\ncola version '+ version
851 +'\nCurrent Branch: '+ branch)
853 def start_inotify_thread(self):
854 # Do we have inotify? If not, return.
855 # Recommend installing inotify if we're on Linux.
856 self.inotify_thread = None
857 try:
858 from cola.inotify import GitNotifier
859 qtutils.log(self.tr('inotify support: enabled'))
860 except ImportError:
861 import platform
862 if platform.system() == 'Linux':
863 msg = self.tr('inotify: disabled\n'
864 'Note: To enable inotify, '
865 'install python-pyinotify.\n')
867 plat = platform.platform().lower()
868 if 'debian' in plat or 'ubuntu' in plat:
869 msg += self.tr('On Debian or Ubuntu systems, '
870 'try: sudo apt-get install '
871 'python-pyinotify')
872 qtutils.log(msg)
873 return
875 # Start the notification thread
876 self.inotify_thread = GitNotifier(self, os.getcwd())
877 self.inotify_thread.start()