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>"
13
from bzrlib.plugins.gtk import icon_path
14
from bzrlib.plugins.gtk.branchview import TreeView
15
from bzrlib.plugins.gtk.preferences import PreferencesWindow
16
from bzrlib.plugins.gtk.revisionmenu import RevisionMenu
17
from bzrlib.plugins.gtk.window import Window
19
from bzrlib.config import GlobalConfig
20
from bzrlib.revision import NULL_REVISION
21
from bzrlib.trace import mutter
24
class BranchWindow(Window):
27
This object represents and manages a single window containing information
28
for a particular branch.
31
def __init__(self, branch, start_revs, maxnum, parent=None):
32
"""Create a new BranchWindow.
34
:param branch: Branch object for branch to show.
35
:param start_revs: Revision ids of top revisions.
36
:param maxnum: Maximum number of revisions to display,
40
Window.__init__(self, parent=parent)
41
self.set_border_width(0)
44
self.start_revs = start_revs
46
self.config = GlobalConfig()
48
if self.config.get_user_option('viz-compact-view') == 'yes':
49
self.compact_view = True
51
self.compact_view = False
53
self.set_title(branch._get_nick(local=True) + " - revision history")
55
# user-configured window size
56
size = self._load_size('viz-window-size')
60
# Use three-quarters of the screen by default
61
screen = self.get_screen()
62
monitor = screen.get_monitor_geometry(0)
63
width = int(monitor.width * 0.75)
64
height = int(monitor.height * 0.75)
65
self.set_default_size(width, height)
66
self.set_size_request(width/3, height/3)
67
self._save_size_on_destroy(self, 'viz-window-size')
70
icon = self.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
73
gtk.accel_map_add_entry("<viz>/Go/Next Revision", gtk.keysyms.Up, gtk.gdk.MOD1_MASK)
74
gtk.accel_map_add_entry("<viz>/Go/Previous Revision", gtk.keysyms.Down, gtk.gdk.MOD1_MASK)
75
gtk.accel_map_add_entry("<viz>/View/Refresh", gtk.keysyms.F5, 0)
77
self.accel_group = gtk.AccelGroup()
78
self.add_accel_group(self.accel_group)
80
if getattr(gtk.Action, 'set_tool_item_type', None) is not None:
81
# Not available before PyGtk-2.10
82
gtk.Action.set_tool_item_type(gtk.MenuToolButton)
84
self.prev_rev_action = gtk.Action("prev-rev", "_Previous Revision", "Go to the previous revision", gtk.STOCK_GO_DOWN)
85
self.prev_rev_action.set_accel_path("<viz>/Go/Previous Revision")
86
self.prev_rev_action.set_accel_group(self.accel_group)
87
self.prev_rev_action.connect("activate", self._back_clicked_cb)
88
self.prev_rev_action.connect_accelerator()
90
self.next_rev_action = gtk.Action("next-rev", "_Next Revision", "Go to the next revision", gtk.STOCK_GO_UP)
91
self.next_rev_action.set_accel_path("<viz>/Go/Next Revision")
92
self.next_rev_action.set_accel_group(self.accel_group)
93
self.next_rev_action.connect("activate", self._fwd_clicked_cb)
94
self.next_rev_action.connect_accelerator()
96
self.refresh_action = gtk.Action("refresh", "_Refresh", "Refresh view", gtk.STOCK_REFRESH)
97
self.refresh_action.set_accel_path("<viz>/View/Refresh")
98
self.refresh_action.set_accel_group(self.accel_group)
99
self.refresh_action.connect("activate", self._refresh_clicked)
100
self.refresh_action.connect_accelerator()
102
self.vbox = self.construct()
104
def _save_size_on_destroy(self, widget, config_name):
105
"""Creates a hook that saves the size of widget to config option
106
config_name when the window is destroyed/closed."""
108
width, height = widget.allocation.width, widget.allocation.height
109
value = '%sx%s' % (width, height)
110
self.config.set_user_option(config_name, value)
111
self.connect("destroy", save_size)
113
def set_revision(self, revid):
114
self.treeview.set_revision_id(revid)
117
"""Construct the window contents."""
118
vbox = gtk.VBox(spacing=0)
121
# order is important here
122
paned = self.construct_paned()
123
nav = self.construct_navigation()
124
menubar = self.construct_menubar()
126
vbox.pack_start(menubar, expand=False, fill=True)
127
vbox.pack_start(nav, expand=False, fill=True)
128
vbox.pack_start(paned, expand=True, fill=True)
129
vbox.set_focus_child(paned)
136
def construct_paned(self):
137
"""Construct the main HPaned/VPaned contents."""
138
if self.config.get_user_option('viz-vertical') == 'True':
139
self.paned = gtk.HPaned()
141
self.paned = gtk.VPaned()
143
self.paned.pack1(self.construct_top(), resize=False, shrink=True)
144
self.paned.pack2(self.construct_bottom(), resize=True, shrink=False)
149
def construct_menubar(self):
150
menubar = gtk.MenuBar()
152
file_menu = gtk.Menu()
153
file_menuitem = gtk.MenuItem("_File")
154
file_menuitem.set_submenu(file_menu)
156
file_menu_close = gtk.ImageMenuItem(gtk.STOCK_CLOSE, self.accel_group)
157
file_menu_close.connect('activate', lambda x: self.destroy())
159
file_menu_quit = gtk.ImageMenuItem(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("_Edit")
168
edit_menuitem.set_submenu(edit_menu)
170
edit_menu_branchopts = gtk.MenuItem("Branch Settings")
171
edit_menu_branchopts.connect('activate', lambda x: PreferencesWindow(self.branch.get_config()).show())
173
edit_menu_globopts = gtk.MenuItem("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("_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("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("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("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("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("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("Wrap _Long Lines in Diffs")
219
view_menu_wrap_diffs.set_active(False)
220
if self.config.get_user_option('viz-wrap-diffs') == 'True':
221
view_menu_wrap_diffs.set_active(True)
222
view_menu_wrap_diffs.connect('toggled', self._diff_wrap_changed)
224
view_menu.add(view_menu_toolbar)
225
view_menu.add(view_menu_compact)
226
view_menu.add(view_menu_vertical)
227
view_menu.add(gtk.SeparatorMenuItem())
228
view_menu.add(view_menu_diffs)
229
view_menu.add(view_menu_wide_diffs)
230
view_menu.add(view_menu_wrap_diffs)
231
view_menu.add(gtk.SeparatorMenuItem())
233
self.mnu_show_revno_column = gtk.CheckMenuItem("Show Revision _Number Column")
234
self.mnu_show_date_column = gtk.CheckMenuItem("Show _Date Column")
236
# Revision numbers are pointless if there are multiple branches
237
if len(self.start_revs) > 1:
238
self.mnu_show_revno_column.set_sensitive(False)
239
self.treeview.set_property('revno-column-visible', False)
241
for (col, name) in [(self.mnu_show_revno_column, "revno"),
242
(self.mnu_show_date_column, "date")]:
243
col.set_active(self.treeview.get_property(name + "-column-visible"))
244
col.connect('toggled', self._col_visibility_changed, name)
248
go_menu.set_accel_group(self.accel_group)
249
go_menuitem = gtk.MenuItem("_Go")
250
go_menuitem.set_submenu(go_menu)
252
go_menu_next = self.next_rev_action.create_menu_item()
253
go_menu_prev = self.prev_rev_action.create_menu_item()
255
tag_image = gtk.Image()
256
tag_image.set_from_file(icon_path("tag-16.png"))
257
self.go_menu_tags = gtk.ImageMenuItem("_Tags")
258
self.go_menu_tags.set_image(tag_image)
259
self.treeview.connect('refreshed', lambda w: self._update_tags())
261
go_menu.add(go_menu_next)
262
go_menu.add(go_menu_prev)
263
go_menu.add(gtk.SeparatorMenuItem())
264
go_menu.add(self.go_menu_tags)
266
self.revision_menu = RevisionMenu(self.branch.repository, [],
267
self.branch, parent=self)
268
revision_menuitem = gtk.MenuItem("_Revision")
269
revision_menuitem.set_submenu(self.revision_menu)
271
branch_menu = gtk.Menu()
272
branch_menuitem = gtk.MenuItem("_Branch")
273
branch_menuitem.set_submenu(branch_menu)
275
branch_menu.add(gtk.MenuItem("Pu_ll Revisions"))
276
branch_menu.add(gtk.MenuItem("Pu_sh Revisions"))
279
from bzrlib.plugins import search
281
mutter("Didn't find search plugin")
283
branch_menu.add(gtk.SeparatorMenuItem())
285
branch_index_menuitem = gtk.MenuItem("_Index")
286
branch_index_menuitem.connect('activate', self._branch_index_cb)
287
branch_menu.add(branch_index_menuitem)
289
branch_search_menuitem = gtk.MenuItem("_Search")
290
branch_search_menuitem.connect('activate', self._branch_search_cb)
291
branch_menu.add(branch_search_menuitem)
293
help_menu = gtk.Menu()
294
help_menuitem = gtk.MenuItem("_Help")
295
help_menuitem.set_submenu(help_menu)
297
help_about_menuitem = gtk.ImageMenuItem(gtk.STOCK_ABOUT,
299
help_about_menuitem.connect('activate', self._about_dialog_cb)
301
help_menu.add(help_about_menuitem)
303
menubar.add(file_menuitem)
304
menubar.add(edit_menuitem)
305
menubar.add(view_menuitem)
306
menubar.add(go_menuitem)
307
menubar.add(revision_menuitem)
308
menubar.add(branch_menuitem)
309
menubar.add(help_menuitem)
314
def construct_top(self):
315
"""Construct the top-half of the window."""
316
# FIXME: Make broken_line_length configurable
318
self.treeview = TreeView(self.branch, self.start_revs, self.maxnum,
321
for col in ["revno", "date"]:
322
option = self.config.get_user_option(col + '-column-visible')
323
if option is not None:
324
self.treeview.set_property(col + '-column-visible',
327
self.treeview.set_property(col + '-column-visible', False)
331
align = gtk.Alignment(0.0, 0.0, 1.0, 1.0)
332
align.set_padding(5, 0, 0, 0)
333
align.add(self.treeview)
334
# user-configured size
335
size = self._load_size('viz-graph-size')
338
align.set_size_request(width, height)
340
(width, height) = self.get_size()
341
align.set_size_request(width, int(height / 2.5))
342
self._save_size_on_destroy(align, 'viz-graph-size')
347
def construct_navigation(self):
348
"""Construct the navigation buttons."""
349
self.toolbar = gtk.Toolbar()
350
self.toolbar.set_style(gtk.TOOLBAR_BOTH_HORIZ)
352
self.prev_button = self.prev_rev_action.create_tool_item()
353
self.toolbar.insert(self.prev_button, -1)
355
self.next_button = self.next_rev_action.create_tool_item()
356
self.toolbar.insert(self.next_button, -1)
358
self.toolbar.insert(gtk.SeparatorToolItem(), -1)
360
refresh_button = gtk.ToolButton(gtk.STOCK_REFRESH)
361
refresh_button.connect('clicked', self._refresh_clicked)
362
self.toolbar.insert(refresh_button, -1)
364
self.toolbar.show_all()
368
def construct_bottom(self):
369
"""Construct the bottom half of the window."""
370
if self.config.get_user_option('viz-wide-diffs') == 'True':
371
self.diff_paned = gtk.VPaned()
373
self.diff_paned = gtk.HPaned()
374
(width, height) = self.get_size()
375
self.diff_paned.set_size_request(20, 20) # shrinkable
377
from bzrlib.plugins.gtk.revisionview import RevisionView
378
self.revisionview = RevisionView(branch=self.branch)
379
self.revisionview.set_size_request(width/3, int(height / 2.5))
380
# user-configured size
381
size = self._load_size('viz-revisionview-size')
384
self.revisionview.set_size_request(width, height)
385
self._save_size_on_destroy(self.revisionview, 'viz-revisionview-size')
386
self.revisionview.show()
387
self.revisionview.set_show_callback(self._show_clicked_cb)
388
self.revisionview.connect('notify::revision', self._go_clicked_cb)
389
self.treeview.connect('tag-added',
390
lambda w, t, r: self.revisionview.update_tags())
391
self.treeview.connect('revision-selected',
392
self._treeselection_changed_cb)
393
self.treeview.connect('revision-activated',
394
self._tree_revision_activated)
395
self.diff_paned.pack1(self.revisionview)
397
from bzrlib.plugins.gtk.diff import DiffWidget
398
self.diff = DiffWidget()
399
self.diff_paned.pack2(self.diff)
401
self.diff_paned.show_all()
402
if self.config.get_user_option('viz-show-diffs') != 'True':
405
return self.diff_paned
407
def _tag_selected_cb(self, menuitem, revid):
408
self.treeview.set_revision_id(revid)
410
def _treeselection_changed_cb(self, selection, *args):
411
"""callback for when the treeview changes."""
412
revision = self.treeview.get_revision()
413
parents = self.treeview.get_parents()
414
children = self.treeview.get_children()
416
if revision and revision.revision_id != NULL_REVISION:
417
self.revision_menu.set_revision_ids([revision.revision_id])
418
prev_menu = gtk.Menu()
420
self.prev_rev_action.set_sensitive(True)
421
for parent_id in parents:
422
if parent_id and parent_id != NULL_REVISION:
423
parent = self.branch.repository.get_revision(parent_id)
425
str = ' (%s)' % parent.properties['branch-nick']
429
item = gtk.MenuItem(parent.message.split("\n")[0] + str)
430
item.connect('activate', self._set_revision_cb, parent_id)
434
self.prev_rev_action.set_sensitive(False)
437
if getattr(self.prev_button, 'set_menu', None) is not None:
438
self.prev_button.set_menu(prev_menu)
440
next_menu = gtk.Menu()
441
if len(children) > 0:
442
self.next_rev_action.set_sensitive(True)
443
for child_id in children:
444
child = self.branch.repository.get_revision(child_id)
446
str = ' (%s)' % child.properties['branch-nick']
450
item = gtk.MenuItem(child.message.split("\n")[0] + str)
451
item.connect('activate', self._set_revision_cb, child_id)
455
self.next_rev_action.set_sensitive(False)
458
if getattr(self.next_button, 'set_menu', None) is not None:
459
self.next_button.set_menu(next_menu)
461
self.revisionview.set_revision(revision)
462
self.revisionview.set_children(children)
463
self.update_diff_panel(revision, parents)
465
def _tree_revision_activated(self, widget, path, col):
466
# TODO: more than one parent
467
"""Callback for when a treeview row gets activated."""
468
revision = self.treeview.get_revision()
469
parents = self.treeview.get_parents()
471
if len(parents) == 0:
472
parent_id = NULL_REVISION
474
parent_id = parents[0]
476
self.show_diff(revision.revision_id, parent_id)
477
self.treeview.grab_focus()
479
def _back_clicked_cb(self, *args):
480
"""Callback for when the back button is clicked."""
483
def _fwd_clicked_cb(self, *args):
484
"""Callback for when the forward button is clicked."""
485
self.treeview.forward()
487
def _go_clicked_cb(self, w, p):
488
"""Callback for when the go button for a parent is clicked."""
489
if self.revisionview.get_revision() is not None:
490
self.treeview.set_revision(self.revisionview.get_revision())
492
def _show_clicked_cb(self, revid, parentid):
493
"""Callback for when the show button for a parent is clicked."""
494
self.show_diff(revid, parentid)
495
self.treeview.grab_focus()
497
def _set_revision_cb(self, w, revision_id):
498
self.treeview.set_revision_id(revision_id)
500
def _brokenlines_toggled_cb(self, button):
501
self.compact_view = button.get_active()
503
if self.compact_view:
508
self.config.set_user_option('viz-compact-view', option)
509
self.treeview.set_property('compact', self.compact_view)
510
self.treeview.refresh()
512
def _branch_index_cb(self, w):
513
from bzrlib.plugins.search import index as _mod_index
514
_mod_index.index_url(self.branch.base)
516
def _branch_search_cb(self, w):
517
from bzrlib.plugins.search import (
519
errors as search_errors,
521
from bzrlib.plugins.gtk.search import SearchDialog
524
index = _mod_index.open_index_url(self.branch.base)
525
except search_errors.NoSearchIndex:
526
dialog = gtk.MessageDialog(self, type=gtk.MESSAGE_QUESTION,
527
buttons=gtk.BUTTONS_OK_CANCEL,
528
message_format="This branch has not been indexed yet. "
530
if dialog.run() == gtk.RESPONSE_OK:
532
index = _mod_index.index_url(self.branch.base)
537
dialog = SearchDialog(index)
539
if dialog.run() == gtk.RESPONSE_OK:
540
self.set_revision(dialog.get_revision())
544
def _about_dialog_cb(self, w):
545
from bzrlib.plugins.gtk.about import AboutDialog
548
def _col_visibility_changed(self, col, property):
549
self.config.set_user_option(property + '-column-visible', col.get_active())
550
self.treeview.set_property(property + '-column-visible', col.get_active())
552
def _toolbar_visibility_changed(self, col):
557
self.config.set_user_option('viz-toolbar-visible', col.get_active())
559
def _vertical_layout(self, col):
560
"""Toggle the layout vertical/horizontal"""
561
self.config.set_user_option('viz-vertical', str(col.get_active()))
564
self.vbox.remove(old)
565
self.vbox.pack_start(self.construct_paned(), expand=True, fill=True)
566
self._make_diff_paned_nonzero_size()
567
self._make_diff_nonzero_size()
569
self.treeview.emit('revision-selected')
571
def _make_diff_paned_nonzero_size(self):
572
"""make sure the diff/revision pane isn't zero-width or zero-height"""
573
alloc = self.diff_paned.get_allocation()
574
if (alloc.width < 10) or (alloc.height < 10):
575
width, height = self.get_size()
576
self.diff_paned.set_size_request(width/3, int(height / 2.5))
578
def _make_diff_nonzero_size(self):
579
"""make sure the diff isn't zero-width or zero-height"""
580
alloc = self.diff.get_allocation()
581
if (alloc.width < 10) or (alloc.height < 10):
582
width, height = self.get_size()
583
self.revisionview.set_size_request(width/3, int(height / 2.5))
585
def _diff_visibility_changed(self, col):
586
"""Hide or show the diff panel."""
589
self._make_diff_nonzero_size()
592
self.config.set_user_option('viz-show-diffs', str(col.get_active()))
593
self.update_diff_panel()
595
def _diff_placement_changed(self, col):
596
"""Toggle the diff panel's position."""
597
self.config.set_user_option('viz-wide-diffs', str(col.get_active()))
599
old = self.paned.get_child2()
600
self.paned.remove(old)
601
self.paned.pack2(self.construct_bottom(), resize=True, shrink=False)
602
self._make_diff_nonzero_size()
604
self.treeview.emit('revision-selected')
606
def _diff_wrap_changed(self, widget):
607
"""Toggle word wrap in the diff widget."""
608
self.config.set_user_option('viz-wrap-diffs', widget.get_active())
609
self.diff._on_wraplines_toggled(widget)
611
def _refresh_clicked(self, w):
612
self.treeview.refresh()
614
def _update_tags(self):
617
if self.branch.supports_tags():
618
tags = self.branch.tags.get_tag_dict().items()
619
tags.sort(reverse=True)
620
for tag, revid in tags:
621
tag_image = gtk.Image()
622
tag_image.set_from_file(icon_path('tag-16.png'))
623
tag_item = gtk.ImageMenuItem(tag.replace('_', '__'))
624
tag_item.set_image(tag_image)
625
tag_item.connect('activate', self._tag_selected_cb, revid)
626
tag_item.set_sensitive(self.treeview.has_revision_id(revid))
628
self.go_menu_tags.set_submenu(menu)
630
self.go_menu_tags.set_sensitive(len(tags) != 0)
632
self.go_menu_tags.set_sensitive(False)
634
self.go_menu_tags.show_all()
636
def _load_size(self, name):
637
"""Read and parse 'name' from self.config.
638
The value is a string, formatted as WIDTHxHEIGHT
639
Returns None, or (width, height)
641
size = self.config.get_user_option(name)
643
width, height = [int(num) for num in size.split('x')]
644
# avoid writing config every time we start
648
def show_diff(self, revid=None, parentid=NULL_REVISION):
649
"""Open a new window to show a diff between the given revisions."""
650
from bzrlib.plugins.gtk.diff import DiffWindow
651
window = DiffWindow(parent=self)
653
rev_tree = self.branch.repository.revision_tree(revid)
654
parent_tree = self.branch.repository.revision_tree(parentid)
656
description = revid + " - " + self.branch._get_nick(local=True)
657
window.set_diff(description, rev_tree, parent_tree)
660
def update_diff_panel(self, revision=None, parents=None):
661
"""Show the current revision in the diff panel."""
662
if self.config.get_user_option('viz-show-diffs') != 'True':
665
if not revision: # default to selected row
666
revision = self.treeview.get_revision()
667
if revision == NULL_REVISION:
670
if not parents: # default to selected row's parents
671
parents = self.treeview.get_parents()
672
if len(parents) == 0:
673
parent_id = NULL_REVISION
675
parent_id = parents[0]
677
rev_tree = self.branch.repository.revision_tree(revision.revision_id)
678
parent_tree = self.branch.repository.revision_tree(parent_id)
680
self.diff.set_diff(rev_tree, parent_tree)
681
if self.config.get_user_option('viz-wrap-diffs') == 'True':
682
self.diff._on_wraplines_toggled(wrap=True)