3
This module contains the code to manage the branch information window,
4
which contains both the revision graph and details panes.
7
__copyright__ = "Copyright (c) 2005 Canonical Ltd."
8
__author__ = "Scott James Remnant <scott@ubuntu.com>"
11
from gi.repository import Gdk
12
from gi.repository import Gtk
14
from bzrlib.plugins.gtk import icon_path
15
from bzrlib.plugins.gtk.branchview import TreeView
16
from bzrlib.plugins.gtk.preferences import PreferencesWindow
17
from bzrlib.plugins.gtk.revisionmenu import RevisionMenu
18
from bzrlib.plugins.gtk.window import Window
20
from bzrlib.config import GlobalConfig
21
from bzrlib.revision import NULL_REVISION
22
from bzrlib.trace import mutter
25
class BranchWindow(Window):
28
This object represents and manages a single window containing information
29
for a particular branch.
32
def __init__(self, branch, start_revs, maxnum, parent=None):
33
"""Create a new BranchWindow.
35
:param branch: Branch object for branch to show.
36
:param start_revs: Revision ids of top revisions.
37
:param maxnum: Maximum number of revisions to display,
41
super(BranchWindow, self).__init__(parent=parent)
42
self.set_border_width(0)
45
self.start_revs = start_revs
47
self.config = GlobalConfig()
49
if self.config.get_user_option('viz-compact-view') == 'yes':
50
self.compact_view = True
52
self.compact_view = False
54
self.set_title(branch._get_nick(local=True) + " - revision history")
56
# user-configured window size
57
size = self._load_size('viz-window-size')
61
# Use three-quarters of the screen by default
62
screen = self.get_screen()
63
monitor = screen.get_monitor_geometry(0)
64
width = int(monitor.width * 0.75)
65
height = int(monitor.height * 0.75)
66
self.set_default_size(width, height)
67
self.set_size_request(width/3, height/3)
68
self._save_size_on_destroy(self, 'viz-window-size')
71
icon = self.render_icon_pixbuf(Gtk.STOCK_INDEX, Gtk.IconSize.BUTTON)
74
Gtk.AccelMap.add_entry("<viz>/Go/Next Revision", Gdk.KEY_Up, Gdk.ModifierType.MOD1_MASK)
75
Gtk.AccelMap.add_entry("<viz>/Go/Previous Revision", Gdk.KEY_Down, Gdk.ModifierType.MOD1_MASK)
76
Gtk.AccelMap.add_entry("<viz>/View/Refresh", Gdk.KEY_F5, 0)
78
self.accel_group = Gtk.AccelGroup()
79
self.add_accel_group(self.accel_group)
81
self.prev_rev_action = Gtk.Action("prev-rev", "_Previous Revision", "Go to the previous revision", Gtk.STOCK_GO_DOWN)
82
self.prev_rev_action.set_accel_path("<viz>/Go/Previous Revision")
83
self.prev_rev_action.set_accel_group(self.accel_group)
84
self.prev_rev_action.connect("activate", self._back_clicked_cb)
85
self.prev_rev_action.connect_accelerator()
87
self.next_rev_action = Gtk.Action("next-rev", "_Next Revision", "Go to the next revision", Gtk.STOCK_GO_UP)
88
self.next_rev_action.set_accel_path("<viz>/Go/Next Revision")
89
self.next_rev_action.set_accel_group(self.accel_group)
90
self.next_rev_action.connect("activate", self._fwd_clicked_cb)
91
self.next_rev_action.connect_accelerator()
93
self.refresh_action = Gtk.Action("refresh", "_Refresh", "Refresh view", Gtk.STOCK_REFRESH)
94
self.refresh_action.set_accel_path("<viz>/View/Refresh")
95
self.refresh_action.set_accel_group(self.accel_group)
96
self.refresh_action.connect("activate", self._refresh_clicked)
97
self.refresh_action.connect_accelerator()
99
self.vbox = self.construct()
101
def _save_size_on_destroy(self, widget, config_name):
102
"""Creates a hook that saves the size of widget to config option
103
config_name when the window is destroyed/closed."""
105
allocation = widget.get_allocation()
106
width, height = allocation.width, allocation.height
107
value = '%sx%s' % (width, height)
108
self.config.set_user_option(config_name, value)
109
self.connect("destroy", save_size)
111
def set_revision(self, revid):
112
self.treeview.set_revision_id(revid)
115
"""Construct the window contents."""
116
vbox = Gtk.VBox(spacing=0)
119
# order is important here
120
paned = self.construct_paned()
121
nav = self.construct_navigation()
122
menubar = self.construct_menubar()
124
vbox.pack_start(menubar, False, True, 0)
125
vbox.pack_start(nav, False, True, 0)
126
vbox.pack_start(paned, True, True, 0)
127
vbox.set_focus_child(paned)
134
def construct_paned(self):
135
"""Construct the main HPaned/VPaned contents."""
136
if self.config.get_user_option('viz-vertical') == 'True':
137
self.paned = Gtk.HPaned()
139
self.paned = Gtk.VPaned()
141
self.paned.pack1(self.construct_top(), resize=False, shrink=True)
142
self.paned.pack2(self.construct_bottom(), resize=True, shrink=False)
147
def construct_menubar(self):
148
menubar = Gtk.MenuBar()
150
file_menu = Gtk.Menu()
151
file_menuitem = Gtk.MenuItem.new_with_mnemonic("_File")
152
file_menuitem.set_submenu(file_menu)
154
file_menu_close = Gtk.ImageMenuItem.new_from_stock(
155
Gtk.STOCK_CLOSE, self.accel_group)
156
file_menu_close.connect('activate', lambda x: self.destroy())
158
file_menu_quit = Gtk.ImageMenuItem.new_from_stock(
159
Gtk.STOCK_QUIT, self.accel_group)
160
file_menu_quit.connect('activate', lambda x: Gtk.main_quit())
162
if self._parent is not None:
163
file_menu.add(file_menu_close)
164
file_menu.add(file_menu_quit)
166
edit_menu = Gtk.Menu()
167
edit_menuitem = Gtk.MenuItem.new_with_mnemonic("_Edit")
168
edit_menuitem.set_submenu(edit_menu)
170
edit_menu_branchopts = Gtk.MenuItem(label="Branch Settings")
171
edit_menu_branchopts.connect('activate', lambda x: PreferencesWindow(self.branch.get_config()).show())
173
edit_menu_globopts = Gtk.MenuItem(label="Global Settings")
174
edit_menu_globopts.connect('activate', lambda x: PreferencesWindow().show())
176
edit_menu.add(edit_menu_branchopts)
177
edit_menu.add(edit_menu_globopts)
179
view_menu = Gtk.Menu()
180
view_menuitem = Gtk.MenuItem.new_with_mnemonic("_View")
181
view_menuitem.set_submenu(view_menu)
183
view_menu_refresh = self.refresh_action.create_menu_item()
184
view_menu_refresh.connect('activate', self._refresh_clicked)
186
view_menu.add(view_menu_refresh)
187
view_menu.add(Gtk.SeparatorMenuItem())
189
view_menu_toolbar = Gtk.CheckMenuItem(label="Show Toolbar")
190
view_menu_toolbar.set_active(True)
191
if self.config.get_user_option('viz-toolbar-visible') == 'False':
192
view_menu_toolbar.set_active(False)
194
view_menu_toolbar.connect('toggled', self._toolbar_visibility_changed)
196
view_menu_compact = Gtk.CheckMenuItem(label="Show Compact Graph")
197
view_menu_compact.set_active(self.compact_view)
198
view_menu_compact.connect('activate', self._brokenlines_toggled_cb)
200
view_menu_vertical = Gtk.CheckMenuItem(label="Side-by-side Layout")
201
view_menu_vertical.set_active(False)
202
if self.config.get_user_option('viz-vertical') == 'True':
203
view_menu_vertical.set_active(True)
204
view_menu_vertical.connect('toggled', self._vertical_layout)
206
view_menu_diffs = Gtk.CheckMenuItem(label="Show Diffs")
207
view_menu_diffs.set_active(False)
208
if self.config.get_user_option('viz-show-diffs') == 'True':
209
view_menu_diffs.set_active(True)
210
view_menu_diffs.connect('toggled', self._diff_visibility_changed)
212
view_menu_wide_diffs = Gtk.CheckMenuItem(label="Wide Diffs")
213
view_menu_wide_diffs.set_active(False)
214
if self.config.get_user_option('viz-wide-diffs') == 'True':
215
view_menu_wide_diffs.set_active(True)
216
view_menu_wide_diffs.connect('toggled', self._diff_placement_changed)
218
view_menu_wrap_diffs = Gtk.CheckMenuItem.new_with_mnemonic(
219
"Wrap _Long Lines in Diffs")
220
view_menu_wrap_diffs.set_active(False)
221
if self.config.get_user_option('viz-wrap-diffs') == 'True':
222
view_menu_wrap_diffs.set_active(True)
223
view_menu_wrap_diffs.connect('toggled', self._diff_wrap_changed)
225
view_menu.add(view_menu_toolbar)
226
view_menu.add(view_menu_compact)
227
view_menu.add(view_menu_vertical)
228
view_menu.add(Gtk.SeparatorMenuItem())
229
view_menu.add(view_menu_diffs)
230
view_menu.add(view_menu_wide_diffs)
231
view_menu.add(view_menu_wrap_diffs)
232
view_menu.add(Gtk.SeparatorMenuItem())
234
self.mnu_show_revno_column = Gtk.CheckMenuItem.new_with_mnemonic(
235
"Show Revision _Number Column")
236
self.mnu_show_date_column = Gtk.CheckMenuItem.new_with_mnemonic(
239
# Revision numbers are pointless if there are multiple branches
240
if len(self.start_revs) > 1:
241
self.mnu_show_revno_column.set_sensitive(False)
242
self.treeview.set_property('revno-column-visible', False)
244
for (col, name) in [(self.mnu_show_revno_column, "revno"),
245
(self.mnu_show_date_column, "date")]:
246
col.set_active(self.treeview.get_property(name + "-column-visible"))
247
col.connect('toggled', self._col_visibility_changed, name)
251
go_menu.set_accel_group(self.accel_group)
252
go_menuitem = Gtk.MenuItem.new_with_mnemonic("_Go")
253
go_menuitem.set_submenu(go_menu)
255
go_menu_next = self.next_rev_action.create_menu_item()
256
go_menu_prev = self.prev_rev_action.create_menu_item()
258
tag_image = Gtk.Image()
259
tag_image.set_from_file(icon_path("tag-16.png"))
260
self.go_menu_tags = Gtk.ImageMenuItem.new_with_mnemonic("_Tags")
261
self.go_menu_tags.set_image(tag_image)
262
self.treeview.connect('refreshed', lambda w: self._update_tags())
264
go_menu.add(go_menu_next)
265
go_menu.add(go_menu_prev)
266
go_menu.add(Gtk.SeparatorMenuItem())
267
go_menu.add(self.go_menu_tags)
269
self.revision_menu = RevisionMenu(self.branch.repository, [],
270
self.branch, parent=self)
271
revision_menuitem = Gtk.MenuItem.new_with_mnemonic("_Revision")
272
revision_menuitem.set_submenu(self.revision_menu)
274
branch_menu = Gtk.Menu()
275
branch_menuitem = Gtk.MenuItem.new_with_mnemonic("_Branch")
276
branch_menuitem.set_submenu(branch_menu)
278
branch_menu.add(Gtk.MenuItem.new_with_mnemonic("Pu_ll Revisions"))
279
branch_menu.add(Gtk.MenuItem.new_with_mnemonic("Pu_sh Revisions"))
282
from bzrlib.plugins import search
284
mutter("Didn't find search plugin")
286
branch_menu.add(Gtk.SeparatorMenuItem())
288
branch_index_menuitem = Gtk.MenuItem.new_with_mnemonic("_Index")
289
branch_index_menuitem.connect('activate', self._branch_index_cb)
290
branch_menu.add(branch_index_menuitem)
292
branch_search_menuitem = Gtk.MenuItem.new_with_mnemonic("_Search")
293
branch_search_menuitem.connect('activate', self._branch_search_cb)
294
branch_menu.add(branch_search_menuitem)
296
help_menu = Gtk.Menu()
297
help_menuitem = Gtk.MenuItem.new_with_mnemonic("_Help")
298
help_menuitem.set_submenu(help_menu)
300
help_about_menuitem = Gtk.ImageMenuItem.new_from_stock(
301
Gtk.STOCK_ABOUT, self.accel_group)
302
help_about_menuitem.connect('activate', self._about_dialog_cb)
304
help_menu.add(help_about_menuitem)
306
menubar.add(file_menuitem)
307
menubar.add(edit_menuitem)
308
menubar.add(view_menuitem)
309
menubar.add(go_menuitem)
310
menubar.add(revision_menuitem)
311
menubar.add(branch_menuitem)
312
menubar.add(help_menuitem)
317
def construct_top(self):
318
"""Construct the top-half of the window."""
319
# FIXME: Make broken_line_length configurable
321
self.treeview = TreeView(self.branch, self.start_revs, self.maxnum,
324
for col in ["revno", "date"]:
325
option = self.config.get_user_option(col + '-column-visible')
326
if option is not None:
327
self.treeview.set_property(col + '-column-visible',
330
self.treeview.set_property(col + '-column-visible', False)
334
align = Gtk.Alignment.new(0.0, 0.0, 1.0, 1.0)
335
align.set_padding(5, 0, 0, 0)
336
align.add(self.treeview)
337
# user-configured size
338
size = self._load_size('viz-graph-size')
341
align.set_size_request(width, height)
343
(width, height) = self.get_size()
344
align.set_size_request(width, int(height / 2.5))
345
self._save_size_on_destroy(align, 'viz-graph-size')
350
def construct_navigation(self):
351
"""Construct the navigation buttons."""
352
self.toolbar = Gtk.Toolbar()
353
self.toolbar.set_style(Gtk.ToolbarStyle.BOTH_HORIZ)
355
self.prev_button = self.prev_rev_action.create_tool_item()
356
self.toolbar.insert(self.prev_button, -1)
358
self.next_button = self.next_rev_action.create_tool_item()
359
self.toolbar.insert(self.next_button, -1)
361
self.toolbar.insert(Gtk.SeparatorToolItem(), -1)
363
refresh_button = Gtk.ToolButton.new_from_stock(Gtk.STOCK_REFRESH)
364
refresh_button.connect('clicked', self._refresh_clicked)
365
self.toolbar.insert(refresh_button, -1)
367
self.toolbar.show_all()
371
def construct_bottom(self):
372
"""Construct the bottom half of the window."""
373
if self.config.get_user_option('viz-wide-diffs') == 'True':
374
self.diff_paned = Gtk.VPaned()
376
self.diff_paned = Gtk.HPaned()
377
(width, height) = self.get_size()
378
self.diff_paned.set_size_request(20, 20) # shrinkable
380
from bzrlib.plugins.gtk.revisionview import RevisionView
381
self.revisionview = RevisionView(branch=self.branch)
382
self.revisionview.set_size_request(width/3, int(height / 2.5))
383
# user-configured size
384
size = self._load_size('viz-revisionview-size')
387
self.revisionview.set_size_request(width, height)
388
self._save_size_on_destroy(self.revisionview, 'viz-revisionview-size')
389
self.revisionview.show()
390
self.revisionview.set_show_callback(self._show_clicked_cb)
391
self.revisionview.connect('notify::revision', self._go_clicked_cb)
392
self.treeview.connect('tag-added',
393
lambda w, t, r: self.revisionview.update_tags())
394
self.treeview.connect('revision-selected',
395
self._treeselection_changed_cb)
396
self.treeview.connect('revision-activated',
397
self._tree_revision_activated)
398
self.diff_paned.pack1(self.revisionview)
400
from bzrlib.plugins.gtk.diff import DiffWidget
401
self.diff = DiffWidget()
402
self.diff_paned.pack2(self.diff)
404
self.diff_paned.show_all()
405
if self.config.get_user_option('viz-show-diffs') != 'True':
408
return self.diff_paned
410
def _tag_selected_cb(self, menuitem, revid):
411
self.treeview.set_revision_id(revid)
413
def _treeselection_changed_cb(self, selection, *args):
414
"""callback for when the treeview changes."""
415
revision = self.treeview.get_revision()
416
parents = self.treeview.get_parents()
417
children = self.treeview.get_children()
419
if revision and revision.revision_id != NULL_REVISION:
420
self.revision_menu.set_revision_ids([revision.revision_id])
421
prev_menu = Gtk.Menu()
423
self.prev_rev_action.set_sensitive(True)
424
for parent_id in parents:
425
if parent_id and parent_id != NULL_REVISION:
426
parent = self.branch.repository.get_revision(parent_id)
428
str = ' (%s)' % parent.properties['branch-nick']
433
label=parent.message.split("\n")[0] + str)
434
item.connect('activate', self._set_revision_cb, parent_id)
438
self.prev_rev_action.set_sensitive(False)
441
if getattr(self.prev_button, 'set_menu', None) is not None:
442
self.prev_button.set_menu(prev_menu)
444
next_menu = Gtk.Menu()
445
if len(children) > 0:
446
self.next_rev_action.set_sensitive(True)
447
for child_id in children:
448
child = self.branch.repository.get_revision(child_id)
450
str = ' (%s)' % child.properties['branch-nick']
455
label=child.message.split("\n")[0] + str)
456
item.connect('activate', self._set_revision_cb, child_id)
460
self.next_rev_action.set_sensitive(False)
463
if getattr(self.next_button, 'set_menu', None) is not None:
464
self.next_button.set_menu(next_menu)
466
self.revisionview.set_revision(revision)
467
self.revisionview.set_children(children)
468
self.update_diff_panel(revision, parents)
470
def _tree_revision_activated(self, widget, path, col):
471
# TODO: more than one parent
472
"""Callback for when a treeview row gets activated."""
473
revision = self.treeview.get_revision()
474
parents = self.treeview.get_parents()
476
if len(parents) == 0:
477
parent_id = NULL_REVISION
479
parent_id = parents[0]
481
self.show_diff(revision.revision_id, parent_id)
482
self.treeview.grab_focus()
484
def _back_clicked_cb(self, *args):
485
"""Callback for when the back button is clicked."""
488
def _fwd_clicked_cb(self, *args):
489
"""Callback for when the forward button is clicked."""
490
self.treeview.forward()
492
def _go_clicked_cb(self, w, p):
493
"""Callback for when the go button for a parent is clicked."""
494
if self.revisionview.get_revision() is not None:
495
self.treeview.set_revision(self.revisionview.get_revision())
497
def _show_clicked_cb(self, revid, parentid):
498
"""Callback for when the show button for a parent is clicked."""
499
self.show_diff(revid, parentid)
500
self.treeview.grab_focus()
502
def _set_revision_cb(self, w, revision_id):
503
self.treeview.set_revision_id(revision_id)
505
def _brokenlines_toggled_cb(self, button):
506
self.compact_view = button.get_active()
508
if self.compact_view:
513
self.config.set_user_option('viz-compact-view', option)
514
self.treeview.set_property('compact', self.compact_view)
515
self.treeview.refresh()
517
def _branch_index_cb(self, w):
518
from bzrlib.plugins.search import index as _mod_index
519
_mod_index.index_url(self.branch.base)
521
def _branch_search_cb(self, w):
522
from bzrlib.plugins.search import (
524
errors as search_errors,
526
from bzrlib.plugins.gtk.search import SearchDialog
529
index = _mod_index.open_index_url(self.branch.base)
530
except search_errors.NoSearchIndex:
531
dialog = Gtk.MessageDialog(self, type=Gtk.MessageType.QUESTION,
532
buttons=Gtk.ButtonsType.OK_CANCEL,
533
message_format="This branch has not been indexed yet. "
535
if dialog.run() == Gtk.ResponseType.OK:
537
index = _mod_index.index_url(self.branch.base)
542
dialog = SearchDialog(index)
544
if dialog.run() == Gtk.ResponseType.OK:
545
revid = dialog.get_revision()
546
if revid is not None:
547
self.set_revision(revid)
551
def _about_dialog_cb(self, w):
552
from bzrlib.plugins.gtk.about import AboutDialog
555
def _col_visibility_changed(self, col, property):
556
self.config.set_user_option(property + '-column-visible', col.get_active())
557
self.treeview.set_property(property + '-column-visible', col.get_active())
559
def _toolbar_visibility_changed(self, col):
564
self.config.set_user_option('viz-toolbar-visible', col.get_active())
566
def _vertical_layout(self, col):
567
"""Toggle the layout vertical/horizontal"""
568
self.config.set_user_option('viz-vertical', str(col.get_active()))
571
self.vbox.remove(old)
572
self.vbox.pack_start(
573
self.construct_paned(), True, True, 0)
574
self._make_diff_paned_nonzero_size()
575
self._make_diff_nonzero_size()
577
self.treeview.emit('revision-selected')
579
def _make_diff_paned_nonzero_size(self):
580
"""make sure the diff/revision pane isn't zero-width or zero-height"""
581
alloc = self.diff_paned.get_allocation()
582
if (alloc.width < 10) or (alloc.height < 10):
583
width, height = self.get_size()
584
self.diff_paned.set_size_request(width/3, int(height / 2.5))
586
def _make_diff_nonzero_size(self):
587
"""make sure the diff isn't zero-width or zero-height"""
588
alloc = self.diff.get_allocation()
589
if (alloc.width < 10) or (alloc.height < 10):
590
width, height = self.get_size()
591
self.revisionview.set_size_request(width/3, int(height / 2.5))
593
def _diff_visibility_changed(self, col):
594
"""Hide or show the diff panel."""
597
self._make_diff_nonzero_size()
600
self.config.set_user_option('viz-show-diffs', str(col.get_active()))
601
self.update_diff_panel()
603
def _diff_placement_changed(self, col):
604
"""Toggle the diff panel's position."""
605
self.config.set_user_option('viz-wide-diffs', str(col.get_active()))
607
old = self.paned.get_child2()
608
self.paned.remove(old)
609
self.paned.pack2(self.construct_bottom(), resize=True, shrink=False)
610
self._make_diff_nonzero_size()
612
self.treeview.emit('revision-selected')
614
def _diff_wrap_changed(self, widget):
615
"""Toggle word wrap in the diff widget."""
616
self.config.set_user_option('viz-wrap-diffs', widget.get_active())
617
self.diff._on_wraplines_toggled(widget)
619
def _refresh_clicked(self, w):
620
self.treeview.refresh()
622
def _update_tags(self):
625
if self.branch.supports_tags():
626
tags = self.branch.tags.get_tag_dict().items()
627
tags.sort(reverse=True)
628
for tag, revid in tags:
629
tag_image = Gtk.Image()
630
tag_image.set_from_file(icon_path('tag-16.png'))
631
tag_item = Gtk.ImageMenuItem.new_with_mnemonic(
632
tag.replace('_', '__'))
633
tag_item.set_image(tag_image)
634
tag_item.connect('activate', self._tag_selected_cb, revid)
635
tag_item.set_sensitive(self.treeview.has_revision_id(revid))
637
self.go_menu_tags.set_submenu(menu)
639
self.go_menu_tags.set_sensitive(len(tags) != 0)
641
self.go_menu_tags.set_sensitive(False)
643
self.go_menu_tags.show_all()
645
def _load_size(self, name):
646
"""Read and parse 'name' from self.config.
647
The value is a string, formatted as WIDTHxHEIGHT
648
Returns None, or (width, height)
650
size = self.config.get_user_option(name)
652
width, height = [int(num) for num in size.split('x')]
653
# avoid writing config every time we start
657
def show_diff(self, revid=None, parentid=NULL_REVISION):
658
"""Open a new window to show a diff between the given revisions."""
659
from bzrlib.plugins.gtk.diff import DiffWindow
660
window = DiffWindow(parent=self)
662
rev_tree = self.branch.repository.revision_tree(revid)
663
parent_tree = self.branch.repository.revision_tree(parentid)
665
description = revid + " - " + self.branch._get_nick(local=True)
666
window.set_diff(description, rev_tree, parent_tree)
669
def update_diff_panel(self, revision=None, parents=None):
670
"""Show the current revision in the diff panel."""
671
if self.config.get_user_option('viz-show-diffs') != 'True':
674
if not revision: # default to selected row
675
revision = self.treeview.get_revision()
676
if revision == NULL_REVISION:
679
if not parents: # default to selected row's parents
680
parents = self.treeview.get_parents()
681
if len(parents) == 0:
682
parent_id = NULL_REVISION
684
parent_id = parents[0]
686
rev_tree = self.branch.repository.revision_tree(revision.revision_id)
687
parent_tree = self.branch.repository.revision_tree(parent_id)
689
self.diff.set_diff(rev_tree, parent_tree)
690
if self.config.get_user_option('viz-wrap-diffs') == 'True':
691
self.diff._on_wraplines_toggled(wrap=True)