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.nick + " - revision history")
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
# user-configured window size
66
size = self._load_size('viz-window-size')
69
self.set_default_size(width, height)
70
self.set_size_request(width/3, height/3)
71
self.connect("size-allocate", self._on_size_allocate, 'viz-window-size')
74
icon = self.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
77
gtk.accel_map_add_entry("<viz>/Go/Next Revision", gtk.keysyms.Up, gtk.gdk.MOD1_MASK)
78
gtk.accel_map_add_entry("<viz>/Go/Previous Revision", gtk.keysyms.Down, gtk.gdk.MOD1_MASK)
79
gtk.accel_map_add_entry("<viz>/View/Refresh", gtk.keysyms.F5, 0)
81
self.accel_group = gtk.AccelGroup()
82
self.add_accel_group(self.accel_group)
84
gtk.Action.set_tool_item_type(gtk.MenuToolButton)
86
self.prev_rev_action = gtk.Action("prev-rev", "_Previous Revision", "Go to the previous revision", gtk.STOCK_GO_DOWN)
87
self.prev_rev_action.set_accel_path("<viz>/Go/Previous Revision")
88
self.prev_rev_action.set_accel_group(self.accel_group)
89
self.prev_rev_action.connect("activate", self._back_clicked_cb)
90
self.prev_rev_action.connect_accelerator()
92
self.next_rev_action = gtk.Action("next-rev", "_Next Revision", "Go to the next revision", gtk.STOCK_GO_UP)
93
self.next_rev_action.set_accel_path("<viz>/Go/Next Revision")
94
self.next_rev_action.set_accel_group(self.accel_group)
95
self.next_rev_action.connect("activate", self._fwd_clicked_cb)
96
self.next_rev_action.connect_accelerator()
98
self.refresh_action = gtk.Action("refresh", "_Refresh", "Refresh view", gtk.STOCK_REFRESH)
99
self.refresh_action.set_accel_path("<viz>/View/Refresh")
100
self.refresh_action.set_accel_group(self.accel_group)
101
self.refresh_action.connect("activate", self._refresh_clicked)
102
self.refresh_action.connect_accelerator()
106
def set_revision(self, revid):
107
self.treeview.set_revision_id(revid)
110
"""Construct the window contents."""
111
vbox = gtk.VBox(spacing=0)
114
self.paned = gtk.VPaned()
115
self.paned.pack1(self.construct_top(), resize=False, shrink=True)
116
self.paned.pack2(self.construct_bottom(), resize=True, shrink=False)
119
nav = self.construct_navigation()
120
menubar = self.construct_menubar()
121
vbox.pack_start(menubar, expand=False, fill=True)
122
vbox.pack_start(nav, expand=False, fill=True)
124
vbox.pack_start(self.paned, expand=True, fill=True)
125
vbox.set_focus_child(self.paned)
129
def construct_menubar(self):
130
menubar = gtk.MenuBar()
132
file_menu = gtk.Menu()
133
file_menuitem = gtk.MenuItem("_File")
134
file_menuitem.set_submenu(file_menu)
136
file_menu_close = gtk.ImageMenuItem(gtk.STOCK_CLOSE, self.accel_group)
137
file_menu_close.connect('activate', lambda x: self.destroy())
139
file_menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT, self.accel_group)
140
file_menu_quit.connect('activate', lambda x: gtk.main_quit())
142
if self._parent is not None:
143
file_menu.add(file_menu_close)
144
file_menu.add(file_menu_quit)
146
edit_menu = gtk.Menu()
147
edit_menuitem = gtk.MenuItem("_Edit")
148
edit_menuitem.set_submenu(edit_menu)
150
edit_menu_branchopts = gtk.MenuItem("Branch Settings")
151
edit_menu_branchopts.connect('activate', lambda x: PreferencesWindow(self.branch.get_config()).show())
153
edit_menu_globopts = gtk.MenuItem("Global Settings")
154
edit_menu_globopts.connect('activate', lambda x: PreferencesWindow().show())
156
edit_menu.add(edit_menu_branchopts)
157
edit_menu.add(edit_menu_globopts)
159
view_menu = gtk.Menu()
160
view_menuitem = gtk.MenuItem("_View")
161
view_menuitem.set_submenu(view_menu)
163
view_menu_refresh = self.refresh_action.create_menu_item()
164
view_menu_refresh.connect('activate', self._refresh_clicked)
166
view_menu.add(view_menu_refresh)
167
view_menu.add(gtk.SeparatorMenuItem())
169
view_menu_toolbar = gtk.CheckMenuItem("Show Toolbar")
170
view_menu_toolbar.set_active(True)
171
if self.config.get_user_option('viz-toolbar-visible') == 'False':
172
view_menu_toolbar.set_active(False)
174
view_menu_toolbar.connect('toggled', self._toolbar_visibility_changed)
176
view_menu_compact = gtk.CheckMenuItem("Show Compact Graph")
177
view_menu_compact.set_active(self.compact_view)
178
view_menu_compact.connect('activate', self._brokenlines_toggled_cb)
180
view_menu_diffs = gtk.CheckMenuItem("Show Diffs")
181
view_menu_diffs.set_active(False)
182
if self.config.get_user_option('viz-show-diffs') == 'True':
183
view_menu_diffs.set_active(True)
184
view_menu_diffs.connect('toggled', self._diff_visibility_changed)
186
view_menu_wide_diffs = gtk.CheckMenuItem("Wide Diffs")
187
view_menu_wide_diffs.set_active(False)
188
if self.config.get_user_option('viz-wide-diffs') == 'True':
189
view_menu_wide_diffs.set_active(True)
190
view_menu_wide_diffs.connect('toggled', self._diff_placement_changed)
192
view_menu.add(view_menu_toolbar)
193
view_menu.add(view_menu_compact)
194
view_menu.add(view_menu_diffs)
195
view_menu.add(view_menu_wide_diffs)
196
view_menu.add(gtk.SeparatorMenuItem())
198
self.mnu_show_revno_column = gtk.CheckMenuItem("Show Revision _Number Column")
199
self.mnu_show_date_column = gtk.CheckMenuItem("Show _Date Column")
201
# Revision numbers are pointless if there are multiple branches
202
if len(self.start_revs) > 1:
203
self.mnu_show_revno_column.set_sensitive(False)
204
self.treeview.set_property('revno-column-visible', False)
206
for (col, name) in [(self.mnu_show_revno_column, "revno"),
207
(self.mnu_show_date_column, "date")]:
208
col.set_active(self.treeview.get_property(name + "-column-visible"))
209
col.connect('toggled', self._col_visibility_changed, name)
213
go_menu.set_accel_group(self.accel_group)
214
go_menuitem = gtk.MenuItem("_Go")
215
go_menuitem.set_submenu(go_menu)
217
go_menu_next = self.next_rev_action.create_menu_item()
218
go_menu_prev = self.prev_rev_action.create_menu_item()
220
tag_image = gtk.Image()
221
tag_image.set_from_file(icon_path("tag-16.png"))
222
self.go_menu_tags = gtk.ImageMenuItem("_Tags")
223
self.go_menu_tags.set_image(tag_image)
226
go_menu.add(go_menu_next)
227
go_menu.add(go_menu_prev)
228
go_menu.add(gtk.SeparatorMenuItem())
229
go_menu.add(self.go_menu_tags)
231
self.revision_menu = RevisionMenu(self.branch.repository, [], self.branch, parent=self)
232
revision_menuitem = gtk.MenuItem("_Revision")
233
revision_menuitem.set_submenu(self.revision_menu)
235
branch_menu = gtk.Menu()
236
branch_menuitem = gtk.MenuItem("_Branch")
237
branch_menuitem.set_submenu(branch_menu)
239
branch_menu.add(gtk.MenuItem("Pu_ll Revisions"))
240
branch_menu.add(gtk.MenuItem("Pu_sh Revisions"))
243
from bzrlib.plugins import search
245
mutter("Didn't find search plugin")
247
branch_menu.add(gtk.SeparatorMenuItem())
249
branch_index_menuitem = gtk.MenuItem("_Index")
250
branch_index_menuitem.connect('activate', self._branch_index_cb)
251
branch_menu.add(branch_index_menuitem)
253
branch_search_menuitem = gtk.MenuItem("_Search")
254
branch_search_menuitem.connect('activate', self._branch_search_cb)
255
branch_menu.add(branch_search_menuitem)
257
help_menu = gtk.Menu()
258
help_menuitem = gtk.MenuItem("_Help")
259
help_menuitem.set_submenu(help_menu)
261
help_about_menuitem = gtk.ImageMenuItem(gtk.STOCK_ABOUT, self.accel_group)
262
help_about_menuitem.connect('activate', self._about_dialog_cb)
264
help_menu.add(help_about_menuitem)
266
menubar.add(file_menuitem)
267
menubar.add(edit_menuitem)
268
menubar.add(view_menuitem)
269
menubar.add(go_menuitem)
270
menubar.add(revision_menuitem)
271
menubar.add(branch_menuitem)
272
menubar.add(help_menuitem)
277
def construct_top(self):
278
"""Construct the top-half of the window."""
279
# FIXME: Make broken_line_length configurable
281
self.treeview = TreeView(self.branch, self.start_revs, self.maxnum, self.compact_view)
283
self.treeview.connect('revision-selected',
284
self._treeselection_changed_cb)
285
self.treeview.connect('revision-activated',
286
self._tree_revision_activated)
288
self.treeview.connect('tag-added', lambda w, t, r: self._update_tags())
290
for col in ["revno", "date"]:
291
option = self.config.get_user_option(col + '-column-visible')
292
if option is not None:
293
self.treeview.set_property(col + '-column-visible', option == 'True')
295
self.treeview.set_property(col + '-column-visible', False)
299
align = gtk.Alignment(0.0, 0.0, 1.0, 1.0)
300
align.set_padding(5, 0, 0, 0)
301
align.add(self.treeview)
302
# user-configured size
303
size = self._load_size('viz-graph-size')
306
align.set_size_request(width, height)
308
(width, height) = self.get_size()
309
align.set_size_request(width, int(height / 2.5))
310
align.connect('size-allocate', self._on_size_allocate, 'viz-graph-size')
315
def construct_navigation(self):
316
"""Construct the navigation buttons."""
317
self.toolbar = gtk.Toolbar()
318
self.toolbar.set_style(gtk.TOOLBAR_BOTH_HORIZ)
320
self.prev_button = self.prev_rev_action.create_tool_item()
321
self.toolbar.insert(self.prev_button, -1)
323
self.next_button = self.next_rev_action.create_tool_item()
324
self.toolbar.insert(self.next_button, -1)
326
self.toolbar.insert(gtk.SeparatorToolItem(), -1)
328
refresh_button = gtk.ToolButton(gtk.STOCK_REFRESH)
329
refresh_button.connect('clicked', self._refresh_clicked)
330
self.toolbar.insert(refresh_button, -1)
332
self.toolbar.show_all()
336
def construct_bottom(self):
337
"""Construct the bottom half of the window."""
338
if self.config.get_user_option('viz-wide-diffs') == 'True':
339
self.diff_paned = gtk.VPaned()
341
self.diff_paned = gtk.HPaned()
342
(width, height) = self.get_size()
343
self.diff_paned.set_size_request(20, 20) # shrinkable
345
from bzrlib.plugins.gtk.revisionview import RevisionView
346
self.revisionview = RevisionView(branch=self.branch)
347
self.revisionview.set_size_request(width/3, int(height / 2.5))
348
# user-configured size
349
size = self._load_size('viz-revisionview-size')
352
self.revisionview.set_size_request(width, height)
353
self.revisionview.connect('size-allocate', self._on_size_allocate, 'viz-revisionview-size')
354
self.revisionview.show()
355
self.revisionview.set_show_callback(self._show_clicked_cb)
356
self.revisionview.connect('notify::revision', self._go_clicked_cb)
357
self.treeview.connect('tag-added', lambda w, t, r: self.revisionview.update_tags())
358
self.diff_paned.pack1(self.revisionview)
360
from bzrlib.plugins.gtk.diff import DiffWidget
361
self.diff = DiffWidget()
362
self.diff_paned.pack2(self.diff)
364
self.diff_paned.show_all()
365
if self.config.get_user_option('viz-show-diffs') != 'True':
368
return self.diff_paned
370
def _tag_selected_cb(self, menuitem, revid):
371
self.treeview.set_revision_id(revid)
373
def _treeselection_changed_cb(self, selection, *args):
374
"""callback for when the treeview changes."""
375
revision = self.treeview.get_revision()
376
parents = self.treeview.get_parents()
377
children = self.treeview.get_children()
379
self.revision_menu.set_revision_ids([revision.revision_id])
381
if revision and revision != NULL_REVISION:
382
prev_menu = gtk.Menu()
384
self.prev_rev_action.set_sensitive(True)
385
for parent_id in parents:
386
if parent_id and parent_id != NULL_REVISION:
387
parent = self.branch.repository.get_revision(parent_id)
389
str = ' (' + parent.properties['branch-nick'] + ')'
393
item = gtk.MenuItem(parent.message.split("\n")[0] + str)
394
item.connect('activate', self._set_revision_cb, parent_id)
398
self.prev_rev_action.set_sensitive(False)
401
self.prev_button.set_menu(prev_menu)
403
next_menu = gtk.Menu()
404
if len(children) > 0:
405
self.next_rev_action.set_sensitive(True)
406
for child_id in children:
407
child = self.branch.repository.get_revision(child_id)
409
str = ' (' + child.properties['branch-nick'] + ')'
413
item = gtk.MenuItem(child.message.split("\n")[0] + str)
414
item.connect('activate', self._set_revision_cb, child_id)
418
self.next_rev_action.set_sensitive(False)
421
self.next_button.set_menu(next_menu)
423
self.revisionview.set_revision(revision)
424
self.revisionview.set_children(children)
426
self.update_diff_panel(revision, parents)
428
def _tree_revision_activated(self, widget, path, col):
429
# TODO: more than one parent
430
"""Callback for when a treeview row gets activated."""
431
revision = self.treeview.get_revision()
432
parents = self.treeview.get_parents()
434
if len(parents) == 0:
437
parent_id = parents[0]
439
self.show_diff(revision.revision_id, parent_id)
440
self.treeview.grab_focus()
442
def _back_clicked_cb(self, *args):
443
"""Callback for when the back button is clicked."""
446
def _fwd_clicked_cb(self, *args):
447
"""Callback for when the forward button is clicked."""
448
self.treeview.forward()
450
def _go_clicked_cb(self, w, p):
451
"""Callback for when the go button for a parent is clicked."""
452
if self.revisionview.get_revision() is not None:
453
self.treeview.set_revision(self.revisionview.get_revision())
455
def _show_clicked_cb(self, revid, parentid):
456
"""Callback for when the show button for a parent is clicked."""
457
self.show_diff(revid, parentid)
458
self.treeview.grab_focus()
460
def _set_revision_cb(self, w, revision_id):
461
self.treeview.set_revision_id(revision_id)
463
def _brokenlines_toggled_cb(self, button):
464
self.compact_view = button.get_active()
466
if self.compact_view:
471
self.config.set_user_option('viz-compact-view', option)
472
self.treeview.set_property('compact', self.compact_view)
473
self.treeview.refresh()
475
def _branch_index_cb(self, w):
476
from bzrlib.plugins.search import index as _mod_index
477
_mod_index.index_url(self.branch.base)
479
def _branch_search_cb(self, w):
480
from bzrlib.plugins.search import index as _mod_index
481
from bzrlib.plugins.gtk.search import SearchDialog
482
from bzrlib.plugins.search import errors as search_errors
485
index = _mod_index.open_index_url(self.branch.base)
486
except search_errors.NoSearchIndex:
487
dialog = gtk.MessageDialog(self, type=gtk.MESSAGE_QUESTION,
488
buttons=gtk.BUTTONS_OK_CANCEL,
489
message_format="This branch has not been indexed yet. "
491
if dialog.run() == gtk.RESPONSE_OK:
493
index = _mod_index.index_url(self.branch.base)
498
dialog = SearchDialog(index)
500
if dialog.run() == gtk.RESPONSE_OK:
501
self.set_revision(dialog.get_revision())
505
def _about_dialog_cb(self, w):
506
from bzrlib.plugins.gtk.about import AboutDialog
510
def _col_visibility_changed(self, col, property):
511
self.config.set_user_option(property + '-column-visible', col.get_active())
512
self.treeview.set_property(property + '-column-visible', col.get_active())
514
def _toolbar_visibility_changed(self, col):
519
self.config.set_user_option('viz-toolbar-visible', col.get_active())
521
def _make_diff_nonzero_size(self):
522
"""make sure the diff isn't zero-width or zero-height"""
523
alloc = self.diff.get_allocation()
524
if (alloc.width < 10) or (alloc.height < 10):
525
width, height = self.get_size()
526
self.revisionview.set_size_request(width/3, int(height / 2.5))
528
def _diff_visibility_changed(self, col):
529
"""Hide or show the diff panel."""
532
self._make_diff_nonzero_size()
535
self.config.set_user_option('viz-show-diffs', str(col.get_active()))
536
self.update_diff_panel()
538
def _diff_placement_changed(self, col):
539
"""Toggle the diff panel's position."""
540
self.config.set_user_option('viz-wide-diffs', str(col.get_active()))
542
old = self.paned.get_child2()
543
self.paned.remove(old)
544
self.paned.pack2(self.construct_bottom(), resize=True, shrink=False)
545
self._make_diff_nonzero_size()
547
self.treeview.emit('revision-selected')
549
def _show_about_cb(self, w):
550
dialog = AboutDialog()
551
dialog.connect('response', lambda d,r: d.destroy())
554
def _refresh_clicked(self, w):
555
self.treeview.refresh()
557
def _update_tags(self):
560
if self.branch.supports_tags():
561
tags = self.branch.tags.get_tag_dict().items()
564
for tag, revid in tags:
565
tag_image = gtk.Image()
566
tag_image.set_from_file(icon_path('tag-16.png'))
567
tag_item = gtk.ImageMenuItem(tag.replace('_', '__'))
568
tag_item.set_image(tag_image)
569
tag_item.connect('activate', self._tag_selected_cb, revid)
571
self.go_menu_tags.set_submenu(menu)
573
self.go_menu_tags.set_sensitive(len(tags) != 0)
575
self.go_menu_tags.set_sensitive(False)
577
self.go_menu_tags.show_all()
579
def _load_size(self, name):
580
"""Read and parse 'name' from self.config.
581
The value is a string, formatted as WIDTHxHEIGHT
582
Returns None, or (width, height)
584
size = self.config.get_user_option(name)
586
width, height = [int(num) for num in size.split('x')]
587
# avoid writing config every time we start
588
self._sizes[name] = (width, height)
592
def _on_size_allocate(self, widget, allocation, name):
593
"""When window has been resized, save the new size."""
595
if name in self._sizes:
596
width, height = self._sizes[name]
598
size_changed = (width != allocation.width) or \
599
(height != allocation.height)
602
width, height = allocation.width, allocation.height
603
self._sizes[name] = (width, height)
604
value = '%sx%s' % (width, height)
605
self.config.set_user_option(name, value)
607
def show_diff(self, revid=None, parentid=None):
608
"""Open a new window to show a diff between the given revisions."""
609
from bzrlib.plugins.gtk.diff import DiffWindow
610
window = DiffWindow(parent=self)
613
parentid = NULL_REVISION
615
rev_tree = self.branch.repository.revision_tree(revid)
616
parent_tree = self.branch.repository.revision_tree(parentid)
618
description = revid + " - " + self.branch.nick
619
window.set_diff(description, rev_tree, parent_tree)
622
def update_diff_panel(self, revision=None, parents=None):
623
"""Show the current revision in the diff panel."""
624
if self.config.get_user_option('viz-show-diffs') != 'True':
627
if not revision: # default to selected row
628
revision = self.treeview.get_revision()
629
if (not revision) or (revision == NULL_REVISION):
632
if not parents: # default to selected row's parents
633
parents = self.treeview.get_parents()
634
if len(parents) == 0:
637
parent_id = parents[0]
639
rev_tree = self.branch.repository.revision_tree(revision.revision_id)
640
parent_tree = self.branch.repository.revision_tree(parent_id)
642
self.diff.set_diff(rev_tree, parent_tree)