1
# -*- coding: UTF-8 -*-
4
This module contains the code to manage the branch information window,
5
which contains both the revision graph and details panes.
8
__copyright__ = "Copyright © 2005 Canonical Ltd."
9
__author__ = "Scott James Remnant <scott@ubuntu.com>"
16
from bzrlib.plugins.gtk import icon_path
17
from bzrlib.plugins.gtk.branchview import TreeView, treemodel
18
from bzrlib.plugins.gtk.tags import AddTagDialog
19
from bzrlib.plugins.gtk.preferences import PreferencesWindow
20
from bzrlib.plugins.gtk.revisionmenu import RevisionMenu
21
from bzrlib.plugins.gtk.window import Window
23
from bzrlib.config import BranchConfig, GlobalConfig
24
from bzrlib.revision import Revision, NULL_REVISION
25
from bzrlib.trace import mutter
27
class BranchWindow(Window):
30
This object represents and manages a single window containing information
31
for a particular branch.
34
def __init__(self, branch, start_revs, maxnum, parent=None):
35
"""Create a new BranchWindow.
37
:param branch: Branch object for branch to show.
38
:param start_revs: Revision ids of top revisions.
39
:param maxnum: Maximum number of revisions to display,
43
Window.__init__(self, parent=parent)
44
self.set_border_width(0)
47
self.start_revs = start_revs
49
self.config = GlobalConfig()
51
self._sizes = {} # window and widget sizes
53
if self.config.get_user_option('viz-compact-view') == 'yes':
54
self.compact_view = True
56
self.compact_view = False
58
self.set_title(branch._get_nick(local=True) + " - revision history")
60
# user-configured window size
61
size = self._load_size('viz-window-size')
65
# Use three-quarters of the screen by default
66
screen = self.get_screen()
67
monitor = screen.get_monitor_geometry(0)
68
width = int(monitor.width * 0.75)
69
height = int(monitor.height * 0.75)
70
self.set_default_size(width, height)
71
self.set_size_request(width/3, height/3)
72
self.connect("size-allocate", self._on_size_allocate, 'viz-window-size')
75
icon = self.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
78
gtk.accel_map_add_entry("<viz>/Go/Next Revision", gtk.keysyms.Up, gtk.gdk.MOD1_MASK)
79
gtk.accel_map_add_entry("<viz>/Go/Previous Revision", gtk.keysyms.Down, gtk.gdk.MOD1_MASK)
80
gtk.accel_map_add_entry("<viz>/View/Refresh", gtk.keysyms.F5, 0)
82
self.accel_group = gtk.AccelGroup()
83
self.add_accel_group(self.accel_group)
85
gtk.Action.set_tool_item_type(gtk.MenuToolButton)
87
self.prev_rev_action = gtk.Action("prev-rev", "_Previous Revision", "Go to the previous revision", gtk.STOCK_GO_DOWN)
88
self.prev_rev_action.set_accel_path("<viz>/Go/Previous Revision")
89
self.prev_rev_action.set_accel_group(self.accel_group)
90
self.prev_rev_action.connect("activate", self._back_clicked_cb)
91
self.prev_rev_action.connect_accelerator()
93
self.next_rev_action = gtk.Action("next-rev", "_Next Revision", "Go to the next revision", gtk.STOCK_GO_UP)
94
self.next_rev_action.set_accel_path("<viz>/Go/Next Revision")
95
self.next_rev_action.set_accel_group(self.accel_group)
96
self.next_rev_action.connect("activate", self._fwd_clicked_cb)
97
self.next_rev_action.connect_accelerator()
99
self.refresh_action = gtk.Action("refresh", "_Refresh", "Refresh view", gtk.STOCK_REFRESH)
100
self.refresh_action.set_accel_path("<viz>/View/Refresh")
101
self.refresh_action.set_accel_group(self.accel_group)
102
self.refresh_action.connect("activate", self._refresh_clicked)
103
self.refresh_action.connect_accelerator()
107
def set_revision(self, revid):
108
self.treeview.set_revision_id(revid)
111
"""Construct the window contents."""
112
vbox = gtk.VBox(spacing=0)
115
self.paned = gtk.VPaned()
116
self.paned.pack1(self.construct_top(), resize=False, shrink=True)
117
self.paned.pack2(self.construct_bottom(), resize=True, shrink=False)
120
nav = self.construct_navigation()
121
menubar = self.construct_menubar()
122
vbox.pack_start(menubar, expand=False, fill=True)
123
vbox.pack_start(nav, expand=False, fill=True)
125
vbox.pack_start(self.paned, expand=True, fill=True)
126
vbox.set_focus_child(self.paned)
128
self.treeview.connect('revision-selected',
129
self._treeselection_changed_cb)
130
self.treeview.connect('revision-activated',
131
self._tree_revision_activated)
133
self.treeview.connect('tag-added', lambda w, t, r: self._update_tags())
136
def construct_menubar(self):
137
menubar = gtk.MenuBar()
139
file_menu = gtk.Menu()
140
file_menuitem = gtk.MenuItem("_File")
141
file_menuitem.set_submenu(file_menu)
143
file_menu_close = gtk.ImageMenuItem(gtk.STOCK_CLOSE, self.accel_group)
144
file_menu_close.connect('activate', lambda x: self.destroy())
146
file_menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT, self.accel_group)
147
file_menu_quit.connect('activate', lambda x: gtk.main_quit())
149
if self._parent is not None:
150
file_menu.add(file_menu_close)
151
file_menu.add(file_menu_quit)
153
edit_menu = gtk.Menu()
154
edit_menuitem = gtk.MenuItem("_Edit")
155
edit_menuitem.set_submenu(edit_menu)
157
edit_menu_branchopts = gtk.MenuItem("Branch Settings")
158
edit_menu_branchopts.connect('activate', lambda x: PreferencesWindow(self.branch.get_config()).show())
160
edit_menu_globopts = gtk.MenuItem("Global Settings")
161
edit_menu_globopts.connect('activate', lambda x: PreferencesWindow().show())
163
edit_menu.add(edit_menu_branchopts)
164
edit_menu.add(edit_menu_globopts)
166
view_menu = gtk.Menu()
167
view_menuitem = gtk.MenuItem("_View")
168
view_menuitem.set_submenu(view_menu)
170
view_menu_refresh = self.refresh_action.create_menu_item()
171
view_menu_refresh.connect('activate', self._refresh_clicked)
173
view_menu.add(view_menu_refresh)
174
view_menu.add(gtk.SeparatorMenuItem())
176
view_menu_toolbar = gtk.CheckMenuItem("Show Toolbar")
177
view_menu_toolbar.set_active(True)
178
if self.config.get_user_option('viz-toolbar-visible') == 'False':
179
view_menu_toolbar.set_active(False)
181
view_menu_toolbar.connect('toggled', self._toolbar_visibility_changed)
183
view_menu_compact = gtk.CheckMenuItem("Show Compact Graph")
184
view_menu_compact.set_active(self.compact_view)
185
view_menu_compact.connect('activate', self._brokenlines_toggled_cb)
187
view_menu_diffs = gtk.CheckMenuItem("Show Diffs")
188
view_menu_diffs.set_active(False)
189
if self.config.get_user_option('viz-show-diffs') == 'True':
190
view_menu_diffs.set_active(True)
191
view_menu_diffs.connect('toggled', self._diff_visibility_changed)
193
view_menu_wide_diffs = gtk.CheckMenuItem("Wide Diffs")
194
view_menu_wide_diffs.set_active(False)
195
if self.config.get_user_option('viz-wide-diffs') == 'True':
196
view_menu_wide_diffs.set_active(True)
197
view_menu_wide_diffs.connect('toggled', self._diff_placement_changed)
199
view_menu_wrap_diffs = gtk.CheckMenuItem("Wrap _Long Lines in Diffs")
200
view_menu_wrap_diffs.set_active(False)
201
if self.config.get_user_option('viz-wrap-diffs') == 'True':
202
view_menu_wrap_diffs.set_active(True)
203
view_menu_wrap_diffs.connect('toggled', self._diff_wrap_changed)
205
view_menu.add(view_menu_toolbar)
206
view_menu.add(view_menu_compact)
207
view_menu.add(gtk.SeparatorMenuItem())
208
view_menu.add(view_menu_diffs)
209
view_menu.add(view_menu_wide_diffs)
210
view_menu.add(view_menu_wrap_diffs)
211
view_menu.add(gtk.SeparatorMenuItem())
213
self.mnu_show_revno_column = gtk.CheckMenuItem("Show Revision _Number Column")
214
self.mnu_show_date_column = gtk.CheckMenuItem("Show _Date Column")
216
# Revision numbers are pointless if there are multiple branches
217
if len(self.start_revs) > 1:
218
self.mnu_show_revno_column.set_sensitive(False)
219
self.treeview.set_property('revno-column-visible', False)
221
for (col, name) in [(self.mnu_show_revno_column, "revno"),
222
(self.mnu_show_date_column, "date")]:
223
col.set_active(self.treeview.get_property(name + "-column-visible"))
224
col.connect('toggled', self._col_visibility_changed, name)
228
go_menu.set_accel_group(self.accel_group)
229
go_menuitem = gtk.MenuItem("_Go")
230
go_menuitem.set_submenu(go_menu)
232
go_menu_next = self.next_rev_action.create_menu_item()
233
go_menu_prev = self.prev_rev_action.create_menu_item()
235
tag_image = gtk.Image()
236
tag_image.set_from_file(icon_path("tag-16.png"))
237
self.go_menu_tags = gtk.ImageMenuItem("_Tags")
238
self.go_menu_tags.set_image(tag_image)
239
self.treeview.connect('refreshed', lambda w: self._update_tags())
241
go_menu.add(go_menu_next)
242
go_menu.add(go_menu_prev)
243
go_menu.add(gtk.SeparatorMenuItem())
244
go_menu.add(self.go_menu_tags)
246
self.revision_menu = RevisionMenu(self.branch.repository, [], self.branch, parent=self)
247
revision_menuitem = gtk.MenuItem("_Revision")
248
revision_menuitem.set_submenu(self.revision_menu)
250
branch_menu = gtk.Menu()
251
branch_menuitem = gtk.MenuItem("_Branch")
252
branch_menuitem.set_submenu(branch_menu)
254
branch_menu.add(gtk.MenuItem("Pu_ll Revisions"))
255
branch_menu.add(gtk.MenuItem("Pu_sh Revisions"))
258
from bzrlib.plugins import search
260
mutter("Didn't find search plugin")
262
branch_menu.add(gtk.SeparatorMenuItem())
264
branch_index_menuitem = gtk.MenuItem("_Index")
265
branch_index_menuitem.connect('activate', self._branch_index_cb)
266
branch_menu.add(branch_index_menuitem)
268
branch_search_menuitem = gtk.MenuItem("_Search")
269
branch_search_menuitem.connect('activate', self._branch_search_cb)
270
branch_menu.add(branch_search_menuitem)
272
help_menu = gtk.Menu()
273
help_menuitem = gtk.MenuItem("_Help")
274
help_menuitem.set_submenu(help_menu)
276
help_about_menuitem = gtk.ImageMenuItem(gtk.STOCK_ABOUT, self.accel_group)
277
help_about_menuitem.connect('activate', self._about_dialog_cb)
279
help_menu.add(help_about_menuitem)
281
menubar.add(file_menuitem)
282
menubar.add(edit_menuitem)
283
menubar.add(view_menuitem)
284
menubar.add(go_menuitem)
285
menubar.add(revision_menuitem)
286
menubar.add(branch_menuitem)
287
menubar.add(help_menuitem)
292
def construct_top(self):
293
"""Construct the top-half of the window."""
294
# FIXME: Make broken_line_length configurable
296
self.treeview = TreeView(self.branch, self.start_revs, self.maxnum, self.compact_view)
298
for col in ["revno", "date"]:
299
option = self.config.get_user_option(col + '-column-visible')
300
if option is not None:
301
self.treeview.set_property(col + '-column-visible', option == 'True')
303
self.treeview.set_property(col + '-column-visible', False)
307
align = gtk.Alignment(0.0, 0.0, 1.0, 1.0)
308
align.set_padding(5, 0, 0, 0)
309
align.add(self.treeview)
310
# user-configured size
311
size = self._load_size('viz-graph-size')
314
align.set_size_request(width, height)
316
(width, height) = self.get_size()
317
align.set_size_request(width, int(height / 2.5))
318
align.connect('size-allocate', self._on_size_allocate, 'viz-graph-size')
323
def construct_navigation(self):
324
"""Construct the navigation buttons."""
325
self.toolbar = gtk.Toolbar()
326
self.toolbar.set_style(gtk.TOOLBAR_BOTH_HORIZ)
328
self.prev_button = self.prev_rev_action.create_tool_item()
329
self.toolbar.insert(self.prev_button, -1)
331
self.next_button = self.next_rev_action.create_tool_item()
332
self.toolbar.insert(self.next_button, -1)
334
self.toolbar.insert(gtk.SeparatorToolItem(), -1)
336
refresh_button = gtk.ToolButton(gtk.STOCK_REFRESH)
337
refresh_button.connect('clicked', self._refresh_clicked)
338
self.toolbar.insert(refresh_button, -1)
340
self.toolbar.show_all()
344
def construct_bottom(self):
345
"""Construct the bottom half of the window."""
346
if self.config.get_user_option('viz-wide-diffs') == 'True':
347
self.diff_paned = gtk.VPaned()
349
self.diff_paned = gtk.HPaned()
350
(width, height) = self.get_size()
351
self.diff_paned.set_size_request(20, 20) # shrinkable
353
from bzrlib.plugins.gtk.revisionview import RevisionView
354
self.revisionview = RevisionView(branch=self.branch)
355
self.revisionview.set_size_request(width/3, int(height / 2.5))
356
# user-configured size
357
size = self._load_size('viz-revisionview-size')
360
self.revisionview.set_size_request(width, height)
361
self.revisionview.connect('size-allocate', self._on_size_allocate, 'viz-revisionview-size')
362
self.revisionview.show()
363
self.revisionview.set_show_callback(self._show_clicked_cb)
364
self.revisionview.connect('notify::revision', self._go_clicked_cb)
365
self.treeview.connect('tag-added', lambda w, t, r: self.revisionview.update_tags())
366
self.diff_paned.pack1(self.revisionview)
368
from bzrlib.plugins.gtk.diff import DiffWidget
369
self.diff = DiffWidget()
370
self.diff_paned.pack2(self.diff)
372
self.diff_paned.show_all()
373
if self.config.get_user_option('viz-show-diffs') != 'True':
376
return self.diff_paned
378
def _tag_selected_cb(self, menuitem, revid):
379
self.treeview.set_revision_id(revid)
381
def _treeselection_changed_cb(self, selection, *args):
382
"""callback for when the treeview changes."""
383
revision = self.treeview.get_revision()
384
parents = self.treeview.get_parents()
385
children = self.treeview.get_children()
387
self.revision_menu.set_revision_ids([revision.revision_id])
389
if revision and revision != NULL_REVISION:
390
prev_menu = gtk.Menu()
392
self.prev_rev_action.set_sensitive(True)
393
for parent_id in parents:
394
if parent_id and parent_id != NULL_REVISION:
395
parent = self.branch.repository.get_revision(parent_id)
397
str = ' (' + parent.properties['branch-nick'] + ')'
401
item = gtk.MenuItem(parent.message.split("\n")[0] + str)
402
item.connect('activate', self._set_revision_cb, parent_id)
406
self.prev_rev_action.set_sensitive(False)
409
self.prev_button.set_menu(prev_menu)
411
next_menu = gtk.Menu()
412
if len(children) > 0:
413
self.next_rev_action.set_sensitive(True)
414
for child_id in children:
415
child = self.branch.repository.get_revision(child_id)
417
str = ' (' + child.properties['branch-nick'] + ')'
421
item = gtk.MenuItem(child.message.split("\n")[0] + str)
422
item.connect('activate', self._set_revision_cb, child_id)
426
self.next_rev_action.set_sensitive(False)
429
self.next_button.set_menu(next_menu)
431
self.revisionview.set_revision(revision)
432
self.revisionview.set_children(children)
433
self.update_diff_panel(revision, parents)
435
def _tree_revision_activated(self, widget, path, col):
436
# TODO: more than one parent
437
"""Callback for when a treeview row gets activated."""
438
revision = self.treeview.get_revision()
439
parents = self.treeview.get_parents()
441
if len(parents) == 0:
442
parent_id = NULL_REVISION
444
parent_id = parents[0]
446
self.show_diff(revision.revision_id, parent_id)
447
self.treeview.grab_focus()
449
def _back_clicked_cb(self, *args):
450
"""Callback for when the back button is clicked."""
453
def _fwd_clicked_cb(self, *args):
454
"""Callback for when the forward button is clicked."""
455
self.treeview.forward()
457
def _go_clicked_cb(self, w, p):
458
"""Callback for when the go button for a parent is clicked."""
459
if self.revisionview.get_revision() is not None:
460
self.treeview.set_revision(self.revisionview.get_revision())
462
def _show_clicked_cb(self, revid, parentid):
463
"""Callback for when the show button for a parent is clicked."""
464
self.show_diff(revid, parentid)
465
self.treeview.grab_focus()
467
def _set_revision_cb(self, w, revision_id):
468
self.treeview.set_revision_id(revision_id)
470
def _brokenlines_toggled_cb(self, button):
471
self.compact_view = button.get_active()
473
if self.compact_view:
478
self.config.set_user_option('viz-compact-view', option)
479
self.treeview.set_property('compact', self.compact_view)
480
self.treeview.refresh()
482
def _branch_index_cb(self, w):
483
from bzrlib.plugins.search import index as _mod_index
484
_mod_index.index_url(self.branch.base)
486
def _branch_search_cb(self, w):
487
from bzrlib.plugins.search import index as _mod_index
488
from bzrlib.plugins.gtk.search import SearchDialog
489
from bzrlib.plugins.search import errors as search_errors
492
index = _mod_index.open_index_url(self.branch.base)
493
except search_errors.NoSearchIndex:
494
dialog = gtk.MessageDialog(self, type=gtk.MESSAGE_QUESTION,
495
buttons=gtk.BUTTONS_OK_CANCEL,
496
message_format="This branch has not been indexed yet. "
498
if dialog.run() == gtk.RESPONSE_OK:
500
index = _mod_index.index_url(self.branch.base)
505
dialog = SearchDialog(index)
507
if dialog.run() == gtk.RESPONSE_OK:
508
self.set_revision(dialog.get_revision())
512
def _about_dialog_cb(self, w):
513
from bzrlib.plugins.gtk.about import AboutDialog
517
def _col_visibility_changed(self, col, property):
518
self.config.set_user_option(property + '-column-visible', col.get_active())
519
self.treeview.set_property(property + '-column-visible', col.get_active())
521
def _toolbar_visibility_changed(self, col):
526
self.config.set_user_option('viz-toolbar-visible', col.get_active())
528
def _make_diff_nonzero_size(self):
529
"""make sure the diff isn't zero-width or zero-height"""
530
alloc = self.diff.get_allocation()
531
if (alloc.width < 10) or (alloc.height < 10):
532
width, height = self.get_size()
533
self.revisionview.set_size_request(width/3, int(height / 2.5))
535
def _diff_visibility_changed(self, col):
536
"""Hide or show the diff panel."""
539
self._make_diff_nonzero_size()
542
self.config.set_user_option('viz-show-diffs', str(col.get_active()))
543
self.update_diff_panel()
545
def _diff_placement_changed(self, col):
546
"""Toggle the diff panel's position."""
547
self.config.set_user_option('viz-wide-diffs', str(col.get_active()))
549
old = self.paned.get_child2()
550
self.paned.remove(old)
551
self.paned.pack2(self.construct_bottom(), resize=True, shrink=False)
552
self._make_diff_nonzero_size()
554
self.treeview.emit('revision-selected')
556
def _diff_wrap_changed(self, widget):
557
"""Toggle word wrap in the diff widget."""
558
self.config.set_user_option('viz-wrap-diffs', widget.get_active())
559
self.diff._on_wraplines_toggled(widget)
561
def _show_about_cb(self, w):
562
dialog = AboutDialog()
563
dialog.connect('response', lambda d,r: d.destroy())
566
def _refresh_clicked(self, w):
567
self.treeview.refresh()
569
def _update_tags(self):
572
if self.branch.supports_tags():
573
tags = self.branch.tags.get_tag_dict().items()
574
tags.sort(reverse=True)
575
for tag, revid in tags:
576
tag_image = gtk.Image()
577
tag_image.set_from_file(icon_path('tag-16.png'))
578
tag_item = gtk.ImageMenuItem(tag.replace('_', '__'))
579
tag_item.set_image(tag_image)
580
tag_item.connect('activate', self._tag_selected_cb, revid)
581
tag_item.set_sensitive(self.treeview.has_revision_id(revid))
583
self.go_menu_tags.set_submenu(menu)
585
self.go_menu_tags.set_sensitive(len(tags) != 0)
587
self.go_menu_tags.set_sensitive(False)
589
self.go_menu_tags.show_all()
591
def _load_size(self, name):
592
"""Read and parse 'name' from self.config.
593
The value is a string, formatted as WIDTHxHEIGHT
594
Returns None, or (width, height)
596
size = self.config.get_user_option(name)
598
width, height = [int(num) for num in size.split('x')]
599
# avoid writing config every time we start
600
self._sizes[name] = (width, height)
604
def _on_size_allocate(self, widget, allocation, name):
605
"""When window has been resized, save the new size."""
607
if name in self._sizes:
608
width, height = self._sizes[name]
610
size_changed = (width != allocation.width) or \
611
(height != allocation.height)
614
width, height = allocation.width, allocation.height
615
self._sizes[name] = (width, height)
616
value = '%sx%s' % (width, height)
617
self.config.set_user_option(name, value)
619
def show_diff(self, revid=None, parentid=NULL_REVISION):
620
"""Open a new window to show a diff between the given revisions."""
621
from bzrlib.plugins.gtk.diff import DiffWindow
622
window = DiffWindow(parent=self)
624
rev_tree = self.branch.repository.revision_tree(revid)
625
parent_tree = self.branch.repository.revision_tree(parentid)
627
description = revid + " - " + self.branch._get_nick(local=True)
628
window.set_diff(description, rev_tree, parent_tree)
631
def update_diff_panel(self, revision=None, parents=None):
632
"""Show the current revision in the diff panel."""
633
if self.config.get_user_option('viz-show-diffs') != 'True':
636
if not revision: # default to selected row
637
revision = self.treeview.get_revision()
638
if revision == NULL_REVISION:
641
if not parents: # default to selected row's parents
642
parents = self.treeview.get_parents()
643
if len(parents) == 0:
644
parent_id = NULL_REVISION
646
parent_id = parents[0]
648
rev_tree = self.branch.repository.revision_tree(revision.revision_id)
649
parent_tree = self.branch.repository.revision_tree(parent_id)
651
self.diff.set_diff(rev_tree, parent_tree)
652
if self.config.get_user_option('viz-wrap-diffs') == 'True':
653
self.diff._on_wraplines_toggled(wrap=True)