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 Gtk
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.IconSize.BUTTON)
73
Gtk.AccelMap.add_entry("<viz>/Go/Next Revision", Gdk.KEY_Up, Gdk.ModifierType.MOD1_MASK)
74
Gtk.AccelMap.add_entry("<viz>/Go/Previous Revision", Gdk.KEY_Down, Gdk.ModifierType.MOD1_MASK)
75
Gtk.AccelMap.add_entry("<viz>/View/Refresh", Gdk.KEY_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
Gtk.Action.set_tool_item_type(Gtk.MenuToolButton)
83
self.prev_rev_action = Gtk.Action("prev-rev", "_Previous Revision", "Go to the previous revision", Gtk.STOCK_GO_DOWN)
84
self.prev_rev_action.set_accel_path("<viz>/Go/Previous Revision")
85
self.prev_rev_action.set_accel_group(self.accel_group)
86
self.prev_rev_action.connect("activate", self._back_clicked_cb)
87
self.prev_rev_action.connect_accelerator()
89
self.next_rev_action = Gtk.Action("next-rev", "_Next Revision", "Go to the next revision", Gtk.STOCK_GO_UP)
90
self.next_rev_action.set_accel_path("<viz>/Go/Next Revision")
91
self.next_rev_action.set_accel_group(self.accel_group)
92
self.next_rev_action.connect("activate", self._fwd_clicked_cb)
93
self.next_rev_action.connect_accelerator()
95
self.refresh_action = Gtk.Action("refresh", "_Refresh", "Refresh view", Gtk.STOCK_REFRESH)
96
self.refresh_action.set_accel_path("<viz>/View/Refresh")
97
self.refresh_action.set_accel_group(self.accel_group)
98
self.refresh_action.connect("activate", self._refresh_clicked)
99
self.refresh_action.connect_accelerator()
101
self.vbox = self.construct()
103
def _save_size_on_destroy(self, widget, config_name):
104
"""Creates a hook that saves the size of widget to config option
105
config_name when the window is destroyed/closed."""
107
width, height = widget.allocation.width, widget.allocation.height
108
value = '%sx%s' % (width, height)
109
self.config.set_user_option(config_name, value)
110
self.connect("destroy", save_size)
112
def set_revision(self, revid):
113
self.treeview.set_revision_id(revid)
116
"""Construct the window contents."""
117
vbox = Gtk.VBox(spacing=0)
120
# order is important here
121
paned = self.construct_paned()
122
nav = self.construct_navigation()
123
menubar = self.construct_menubar()
125
vbox.pack_start(menubar, False, True, True, 0)
126
vbox.pack_start(nav, False, True,True, 0)
127
vbox.pack_start(paned, True, True, True, 0)
128
vbox.set_focus_child(paned)
135
def construct_paned(self):
136
"""Construct the main HPaned/VPaned contents."""
137
if self.config.get_user_option('viz-vertical') == 'True':
138
self.paned = Gtk.HPaned()
140
self.paned = Gtk.VPaned()
142
self.paned.pack1(self.construct_top(), resize=False, shrink=True)
143
self.paned.pack2(self.construct_bottom(), resize=True, shrink=False)
148
def construct_menubar(self):
149
menubar = Gtk.MenuBar()
151
file_menu = Gtk.Menu()
152
file_menuitem = Gtk.MenuItem("_File")
153
file_menuitem.set_submenu(file_menu)
155
file_menu_close = Gtk.ImageMenuItem(Gtk.STOCK_CLOSE, self.accel_group)
156
file_menu_close.connect('activate', lambda x: self.destroy())
158
file_menu_quit = Gtk.ImageMenuItem(Gtk.STOCK_QUIT, self.accel_group)
159
file_menu_quit.connect('activate', lambda x: Gtk.main_quit())
161
if self._parent is not None:
162
file_menu.add(file_menu_close)
163
file_menu.add(file_menu_quit)
165
edit_menu = Gtk.Menu()
166
edit_menuitem = Gtk.MenuItem("_Edit")
167
edit_menuitem.set_submenu(edit_menu)
169
edit_menu_branchopts = Gtk.MenuItem("Branch Settings")
170
edit_menu_branchopts.connect('activate', lambda x: PreferencesWindow(self.branch.get_config()).show())
172
edit_menu_globopts = Gtk.MenuItem("Global Settings")
173
edit_menu_globopts.connect('activate', lambda x: PreferencesWindow().show())
175
edit_menu.add(edit_menu_branchopts)
176
edit_menu.add(edit_menu_globopts)
178
view_menu = Gtk.Menu()
179
view_menuitem = Gtk.MenuItem("_View")
180
view_menuitem.set_submenu(view_menu)
182
view_menu_refresh = self.refresh_action.create_menu_item()
183
view_menu_refresh.connect('activate', self._refresh_clicked)
185
view_menu.add(view_menu_refresh)
186
view_menu.add(Gtk.SeparatorMenuItem())
188
view_menu_toolbar = Gtk.CheckMenuItem("Show Toolbar")
189
view_menu_toolbar.set_active(True)
190
if self.config.get_user_option('viz-toolbar-visible') == 'False':
191
view_menu_toolbar.set_active(False)
193
view_menu_toolbar.connect('toggled', self._toolbar_visibility_changed)
195
view_menu_compact = Gtk.CheckMenuItem("Show Compact Graph")
196
view_menu_compact.set_active(self.compact_view)
197
view_menu_compact.connect('activate', self._brokenlines_toggled_cb)
199
view_menu_vertical = Gtk.CheckMenuItem("Side-by-side Layout")
200
view_menu_vertical.set_active(False)
201
if self.config.get_user_option('viz-vertical') == 'True':
202
view_menu_vertical.set_active(True)
203
view_menu_vertical.connect('toggled', self._vertical_layout)
205
view_menu_diffs = Gtk.CheckMenuItem("Show Diffs")
206
view_menu_diffs.set_active(False)
207
if self.config.get_user_option('viz-show-diffs') == 'True':
208
view_menu_diffs.set_active(True)
209
view_menu_diffs.connect('toggled', self._diff_visibility_changed)
211
view_menu_wide_diffs = Gtk.CheckMenuItem("Wide Diffs")
212
view_menu_wide_diffs.set_active(False)
213
if self.config.get_user_option('viz-wide-diffs') == 'True':
214
view_menu_wide_diffs.set_active(True)
215
view_menu_wide_diffs.connect('toggled', self._diff_placement_changed)
217
view_menu_wrap_diffs = Gtk.CheckMenuItem("Wrap _Long Lines in Diffs")
218
view_menu_wrap_diffs.set_active(False)
219
if self.config.get_user_option('viz-wrap-diffs') == 'True':
220
view_menu_wrap_diffs.set_active(True)
221
view_menu_wrap_diffs.connect('toggled', self._diff_wrap_changed)
223
view_menu.add(view_menu_toolbar)
224
view_menu.add(view_menu_compact)
225
view_menu.add(view_menu_vertical)
226
view_menu.add(Gtk.SeparatorMenuItem())
227
view_menu.add(view_menu_diffs)
228
view_menu.add(view_menu_wide_diffs)
229
view_menu.add(view_menu_wrap_diffs)
230
view_menu.add(Gtk.SeparatorMenuItem())
232
self.mnu_show_revno_column = Gtk.CheckMenuItem("Show Revision _Number Column")
233
self.mnu_show_date_column = Gtk.CheckMenuItem("Show _Date Column")
235
# Revision numbers are pointless if there are multiple branches
236
if len(self.start_revs) > 1:
237
self.mnu_show_revno_column.set_sensitive(False)
238
self.treeview.set_property('revno-column-visible', False)
240
for (col, name) in [(self.mnu_show_revno_column, "revno"),
241
(self.mnu_show_date_column, "date")]:
242
col.set_active(self.treeview.get_property(name + "-column-visible"))
243
col.connect('toggled', self._col_visibility_changed, name)
247
go_menu.set_accel_group(self.accel_group)
248
go_menuitem = Gtk.MenuItem("_Go")
249
go_menuitem.set_submenu(go_menu)
251
go_menu_next = self.next_rev_action.create_menu_item()
252
go_menu_prev = self.prev_rev_action.create_menu_item()
254
tag_image = Gtk.Image()
255
tag_image.set_from_file(icon_path("tag-16.png"))
256
self.go_menu_tags = Gtk.ImageMenuItem("_Tags")
257
self.go_menu_tags.set_image(tag_image)
258
self.treeview.connect('refreshed', lambda w: self._update_tags())
260
go_menu.add(go_menu_next)
261
go_menu.add(go_menu_prev)
262
go_menu.add(Gtk.SeparatorMenuItem())
263
go_menu.add(self.go_menu_tags)
265
self.revision_menu = RevisionMenu(self.branch.repository, [],
266
self.branch, parent=self)
267
revision_menuitem = Gtk.MenuItem("_Revision")
268
revision_menuitem.set_submenu(self.revision_menu)
270
branch_menu = Gtk.Menu()
271
branch_menuitem = Gtk.MenuItem("_Branch")
272
branch_menuitem.set_submenu(branch_menu)
274
branch_menu.add(Gtk.MenuItem("Pu_ll Revisions"))
275
branch_menu.add(Gtk.MenuItem("Pu_sh Revisions"))
278
from bzrlib.plugins import search
280
mutter("Didn't find search plugin")
282
branch_menu.add(Gtk.SeparatorMenuItem())
284
branch_index_menuitem = Gtk.MenuItem("_Index")
285
branch_index_menuitem.connect('activate', self._branch_index_cb)
286
branch_menu.add(branch_index_menuitem)
288
branch_search_menuitem = Gtk.MenuItem("_Search")
289
branch_search_menuitem.connect('activate', self._branch_search_cb)
290
branch_menu.add(branch_search_menuitem)
292
help_menu = Gtk.Menu()
293
help_menuitem = Gtk.MenuItem("_Help")
294
help_menuitem.set_submenu(help_menu)
296
help_about_menuitem = Gtk.ImageMenuItem(Gtk.STOCK_ABOUT,
298
help_about_menuitem.connect('activate', self._about_dialog_cb)
300
help_menu.add(help_about_menuitem)
302
menubar.add(file_menuitem)
303
menubar.add(edit_menuitem)
304
menubar.add(view_menuitem)
305
menubar.add(go_menuitem)
306
menubar.add(revision_menuitem)
307
menubar.add(branch_menuitem)
308
menubar.add(help_menuitem)
313
def construct_top(self):
314
"""Construct the top-half of the window."""
315
# FIXME: Make broken_line_length configurable
317
self.treeview = TreeView(self.branch, self.start_revs, self.maxnum,
320
for col in ["revno", "date"]:
321
option = self.config.get_user_option(col + '-column-visible')
322
if option is not None:
323
self.treeview.set_property(col + '-column-visible',
326
self.treeview.set_property(col + '-column-visible', False)
330
align = Gtk.Alignment.new(0.0, 0.0, 1.0, 1.0)
331
align.set_padding(5, 0, 0, 0)
332
align.add(self.treeview)
333
# user-configured size
334
size = self._load_size('viz-graph-size')
337
align.set_size_request(width, height)
339
(width, height) = self.get_size()
340
align.set_size_request(width, int(height / 2.5))
341
self._save_size_on_destroy(align, 'viz-graph-size')
346
def construct_navigation(self):
347
"""Construct the navigation buttons."""
348
self.toolbar = Gtk.Toolbar()
349
self.toolbar.set_style(Gtk.TOOLBAR_BOTH_HORIZ)
351
self.prev_button = self.prev_rev_action.create_tool_item()
352
self.toolbar.insert(self.prev_button, -1)
354
self.next_button = self.next_rev_action.create_tool_item()
355
self.toolbar.insert(self.next_button, -1)
357
self.toolbar.insert(Gtk.SeparatorToolItem(), -1)
359
refresh_button = Gtk.ToolButton(Gtk.STOCK_REFRESH)
360
refresh_button.connect('clicked', self._refresh_clicked)
361
self.toolbar.insert(refresh_button, -1)
363
self.toolbar.show_all()
367
def construct_bottom(self):
368
"""Construct the bottom half of the window."""
369
if self.config.get_user_option('viz-wide-diffs') == 'True':
370
self.diff_paned = Gtk.VPaned()
372
self.diff_paned = Gtk.HPaned()
373
(width, height) = self.get_size()
374
self.diff_paned.set_size_request(20, 20) # shrinkable
376
from bzrlib.plugins.gtk.revisionview import RevisionView
377
self.revisionview = RevisionView(branch=self.branch)
378
self.revisionview.set_size_request(width/3, int(height / 2.5))
379
# user-configured size
380
size = self._load_size('viz-revisionview-size')
383
self.revisionview.set_size_request(width, height)
384
self._save_size_on_destroy(self.revisionview, 'viz-revisionview-size')
385
self.revisionview.show()
386
self.revisionview.set_show_callback(self._show_clicked_cb)
387
self.revisionview.connect('notify::revision', self._go_clicked_cb)
388
self.treeview.connect('tag-added',
389
lambda w, t, r: self.revisionview.update_tags())
390
self.treeview.connect('revision-selected',
391
self._treeselection_changed_cb)
392
self.treeview.connect('revision-activated',
393
self._tree_revision_activated)
394
self.diff_paned.pack1(self.revisionview)
396
from bzrlib.plugins.gtk.diff import DiffWidget
397
self.diff = DiffWidget()
398
self.diff_paned.pack2(self.diff)
400
self.diff_paned.show_all()
401
if self.config.get_user_option('viz-show-diffs') != 'True':
404
return self.diff_paned
406
def _tag_selected_cb(self, menuitem, revid):
407
self.treeview.set_revision_id(revid)
409
def _treeselection_changed_cb(self, selection, *args):
410
"""callback for when the treeview changes."""
411
revision = self.treeview.get_revision()
412
parents = self.treeview.get_parents()
413
children = self.treeview.get_children()
415
if revision and revision.revision_id != NULL_REVISION:
416
self.revision_menu.set_revision_ids([revision.revision_id])
417
prev_menu = Gtk.Menu()
419
self.prev_rev_action.set_sensitive(True)
420
for parent_id in parents:
421
if parent_id and parent_id != NULL_REVISION:
422
parent = self.branch.repository.get_revision(parent_id)
424
str = ' (%s)' % parent.properties['branch-nick']
428
item = Gtk.MenuItem(parent.message.split("\n")[0] + str)
429
item.connect('activate', self._set_revision_cb, parent_id)
433
self.prev_rev_action.set_sensitive(False)
436
if getattr(self.prev_button, 'set_menu', None) is not None:
437
self.prev_button.set_menu(prev_menu)
439
next_menu = Gtk.Menu()
440
if len(children) > 0:
441
self.next_rev_action.set_sensitive(True)
442
for child_id in children:
443
child = self.branch.repository.get_revision(child_id)
445
str = ' (%s)' % child.properties['branch-nick']
449
item = Gtk.MenuItem(child.message.split("\n")[0] + str)
450
item.connect('activate', self._set_revision_cb, child_id)
454
self.next_rev_action.set_sensitive(False)
457
if getattr(self.next_button, 'set_menu', None) is not None:
458
self.next_button.set_menu(next_menu)
460
self.revisionview.set_revision(revision)
461
self.revisionview.set_children(children)
462
self.update_diff_panel(revision, parents)
464
def _tree_revision_activated(self, widget, path, col):
465
# TODO: more than one parent
466
"""Callback for when a treeview row gets activated."""
467
revision = self.treeview.get_revision()
468
parents = self.treeview.get_parents()
470
if len(parents) == 0:
471
parent_id = NULL_REVISION
473
parent_id = parents[0]
475
self.show_diff(revision.revision_id, parent_id)
476
self.treeview.grab_focus()
478
def _back_clicked_cb(self, *args):
479
"""Callback for when the back button is clicked."""
482
def _fwd_clicked_cb(self, *args):
483
"""Callback for when the forward button is clicked."""
484
self.treeview.forward()
486
def _go_clicked_cb(self, w, p):
487
"""Callback for when the go button for a parent is clicked."""
488
if self.revisionview.get_revision() is not None:
489
self.treeview.set_revision(self.revisionview.get_revision())
491
def _show_clicked_cb(self, revid, parentid):
492
"""Callback for when the show button for a parent is clicked."""
493
self.show_diff(revid, parentid)
494
self.treeview.grab_focus()
496
def _set_revision_cb(self, w, revision_id):
497
self.treeview.set_revision_id(revision_id)
499
def _brokenlines_toggled_cb(self, button):
500
self.compact_view = button.get_active()
502
if self.compact_view:
507
self.config.set_user_option('viz-compact-view', option)
508
self.treeview.set_property('compact', self.compact_view)
509
self.treeview.refresh()
511
def _branch_index_cb(self, w):
512
from bzrlib.plugins.search import index as _mod_index
513
_mod_index.index_url(self.branch.base)
515
def _branch_search_cb(self, w):
516
from bzrlib.plugins.search import (
518
errors as search_errors,
520
from bzrlib.plugins.gtk.search import SearchDialog
523
index = _mod_index.open_index_url(self.branch.base)
524
except search_errors.NoSearchIndex:
525
dialog = Gtk.MessageDialog(self, type=Gtk.MessageType.QUESTION,
526
buttons=Gtk.ButtonsType.OK_CANCEL,
527
message_format="This branch has not been indexed yet. "
529
if dialog.run() == Gtk.ResponseType.OK:
531
index = _mod_index.index_url(self.branch.base)
536
dialog = SearchDialog(index)
538
if dialog.run() == Gtk.ResponseType.OK:
539
self.set_revision(dialog.get_revision())
543
def _about_dialog_cb(self, w):
544
from bzrlib.plugins.gtk.about import AboutDialog
547
def _col_visibility_changed(self, col, property):
548
self.config.set_user_option(property + '-column-visible', col.get_active())
549
self.treeview.set_property(property + '-column-visible', col.get_active())
551
def _toolbar_visibility_changed(self, col):
556
self.config.set_user_option('viz-toolbar-visible', col.get_active())
558
def _vertical_layout(self, col):
559
"""Toggle the layout vertical/horizontal"""
560
self.config.set_user_option('viz-vertical', str(col.get_active()))
563
self.vbox.remove(old)
564
self.vbox.pack_start(
565
self.construct_paned(, True, True, 0), True, True, True, 0)
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)