1
 
# -*- coding: UTF-8 -*-
 
4
3
This module contains the code to manage the branch information window,
 
5
4
which contains both the revision graph and details panes.
 
8
 
__copyright__ = "Copyright © 2005 Canonical Ltd."
 
 
7
__copyright__ = "Copyright (c) 2005 Canonical Ltd."
 
9
8
__author__    = "Scott James Remnant <scott@ubuntu.com>"
 
16
 
from bzrlib.osutils import format_date
 
18
 
from graph import distances, graph, same_branch
 
19
 
from graphcell import CellRendererGraph
 
22
 
class BranchWindow(gtk.Window):
 
 
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):
 
25
28
    This object represents and manages a single window containing information
 
26
29
    for a particular branch.
 
29
 
    def __init__(self, app=None):
 
30
 
        gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL)
 
 
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)
 
31
42
        self.set_border_width(0)
 
32
 
        self.set_title("bzrk")
 
36
 
        # Use three-quarters of the screen by default
 
37
 
        screen = self.get_screen()
 
38
 
        monitor = screen.get_monitor_geometry(0)
 
39
 
        width = int(monitor.width * 0.75)
 
40
 
        height = int(monitor.height * 0.75)
 
 
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)
 
41
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')
 
44
 
        icon = self.render_icon(gtk.STOCK_INDEX, gtk.ICON_SIZE_BUTTON)
 
 
71
        icon = self.render_icon_pixbuf(Gtk.STOCK_INDEX, Gtk.IconSize.BUTTON)
 
45
72
        self.set_icon(icon)
 
47
 
        self.accel_group = gtk.AccelGroup()
 
 
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()
 
48
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)
 
52
114
    def construct(self):
 
53
115
        """Construct the window contents."""
 
54
 
        vbox = gtk.VBox(spacing=0)
 
 
116
        vbox = Gtk.VBox(spacing=0)
 
57
 
        vbox.pack_start(self.construct_navigation(), expand=False, fill=True)
 
 
119
        # order is important here
 
 
120
        paned = self.construct_paned()
 
 
121
        nav = self.construct_navigation()
 
 
122
        menubar = self.construct_menubar()
 
60
 
        paned.pack1(self.construct_top(), resize=True, shrink=False)
 
61
 
        paned.pack2(self.construct_bottom(), resize=False, shrink=True)
 
63
 
        vbox.pack_start(paned, expand=True, fill=True)
 
 
124
        vbox.pack_start(menubar, False, True, 0)
 
 
125
        vbox.pack_start(nav, False, True, 0)
 
 
126
        vbox.pack_start(paned, True, True, 0)
 
64
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.Paned.new(Gtk.Orientation.HORIZONTAL)
 
 
139
            self.paned = Gtk.Paned.new(Gtk.Orientation.VERTICAL)
 
 
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)
 
68
317
    def construct_top(self):
 
69
318
        """Construct the top-half of the window."""
 
70
 
        scrollwin = gtk.ScrolledWindow()
 
71
 
        scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
 
72
 
        scrollwin.set_shadow_type(gtk.SHADOW_IN)
 
75
 
        self.treeview = gtk.TreeView()
 
76
 
        self.treeview.set_rules_hint(True)
 
77
 
        self.treeview.set_search_column(4)
 
78
 
        self.treeview.connect("cursor-changed", self._treeview_cursor_cb)
 
79
 
        self.treeview.connect("row-activated", self._treeview_row_activated_cb)
 
80
 
        scrollwin.add(self.treeview)
 
 
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)
 
81
332
        self.treeview.show()
 
83
 
        cell = CellRendererGraph()
 
84
 
        column = gtk.TreeViewColumn()
 
85
 
        column.set_resizable(True)
 
86
 
        column.pack_start(cell, expand=False)
 
87
 
        column.add_attribute(cell, "node", 1)
 
88
 
        column.add_attribute(cell, "in-lines", 2)
 
89
 
        column.add_attribute(cell, "out-lines", 3)
 
90
 
        self.treeview.append_column(column)
 
92
 
        cell = gtk.CellRendererText()
 
93
 
        cell.set_property("width-chars", 40)
 
94
 
        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 
95
 
        column = gtk.TreeViewColumn("Message")
 
96
 
        column.set_resizable(True)
 
97
 
        column.pack_start(cell, expand=True)
 
98
 
        column.add_attribute(cell, "text", 4)
 
99
 
        self.treeview.append_column(column)
 
101
 
        cell = gtk.CellRendererText()
 
102
 
        cell.set_property("width-chars", 40)
 
103
 
        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 
104
 
        column = gtk.TreeViewColumn("Committer")
 
105
 
        column.set_resizable(True)
 
106
 
        column.pack_start(cell, expand=True)
 
107
 
        column.add_attribute(cell, "text", 5)
 
108
 
        self.treeview.append_column(column)
 
110
 
        cell = gtk.CellRendererText()
 
111
 
        cell.set_property("ellipsize", pango.ELLIPSIZE_END)
 
112
 
        column = gtk.TreeViewColumn("Date")
 
113
 
        column.set_resizable(True)
 
114
 
        column.pack_start(cell, expand=True)
 
115
 
        column.add_attribute(cell, "text", 6)
 
116
 
        self.treeview.append_column(column)
 
 
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')
 
120
350
    def construct_navigation(self):
 
121
351
        """Construct the navigation buttons."""
 
123
 
        frame.set_shadow_type(gtk.SHADOW_OUT)
 
126
 
        hbox = gtk.HBox(spacing=12)
 
130
 
        self.back_button = gtk.Button(stock=gtk.STOCK_GO_BACK)
 
131
 
        self.back_button.set_relief(gtk.RELIEF_NONE)
 
132
 
        self.back_button.add_accelerator("clicked", self.accel_group, ord('['),
 
134
 
        self.back_button.connect("clicked", self._back_clicked_cb)
 
135
 
        hbox.pack_start(self.back_button, expand=False, fill=True)
 
136
 
        self.back_button.show()
 
138
 
        self.fwd_button = gtk.Button(stock=gtk.STOCK_GO_FORWARD)
 
139
 
        self.fwd_button.set_relief(gtk.RELIEF_NONE)
 
140
 
        self.fwd_button.add_accelerator("clicked", self.accel_group, ord(']'),
 
142
 
        self.fwd_button.connect("clicked", self._fwd_clicked_cb)
 
143
 
        hbox.pack_start(self.fwd_button, expand=False, fill=True)
 
144
 
        self.fwd_button.show()
 
 
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()
 
148
371
    def construct_bottom(self):
 
149
372
        """Construct the bottom half of the window."""
 
150
 
        scrollwin = gtk.ScrolledWindow()
 
151
 
        scrollwin.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
 
152
 
        scrollwin.set_shadow_type(gtk.SHADOW_NONE)
 
 
373
        if self.config.get_user_option('viz-wide-diffs') == 'True':
 
 
374
            self.diff_paned = Gtk.Paned.new(Gtk.Orientation.VERTICAL)
 
 
376
            self.diff_paned = Gtk.Paned.new(Gtk.Orientation.HORIZONTAL)
 
153
377
        (width, height) = self.get_size()
 
154
 
        scrollwin.set_size_request(width, int(height / 2.5))
 
157
 
        vbox = gtk.VBox(False, spacing=6)
 
158
 
        vbox.set_border_width(6)
 
159
 
        scrollwin.add_with_viewport(vbox)
 
162
 
        table = gtk.Table(rows=4, columns=2)
 
163
 
        table.set_row_spacings(6)
 
164
 
        table.set_col_spacings(6)
 
165
 
        vbox.pack_start(table, expand=False, fill=True)
 
168
 
        align = gtk.Alignment(0.0, 0.5)
 
170
 
        label.set_markup("<b>Revision:</b>")
 
172
 
        table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
 
176
 
        align = gtk.Alignment(0.0, 0.5)
 
177
 
        self.revid_label = gtk.Label()
 
178
 
        self.revid_label.set_selectable(True)
 
179
 
        align.add(self.revid_label)
 
180
 
        table.attach(align, 1, 2, 0, 1, gtk.EXPAND | gtk.FILL, gtk.FILL)
 
181
 
        self.revid_label.show()
 
184
 
        align = gtk.Alignment(0.0, 0.5)
 
186
 
        label.set_markup("<b>Committer:</b>")
 
188
 
        table.attach(align, 0, 1, 1, 2, gtk.FILL, gtk.FILL)
 
192
 
        align = gtk.Alignment(0.0, 0.5)
 
193
 
        self.committer_label = gtk.Label()
 
194
 
        self.committer_label.set_selectable(True)
 
195
 
        align.add(self.committer_label)
 
196
 
        table.attach(align, 1, 2, 1, 2, gtk.EXPAND | gtk.FILL, gtk.FILL)
 
197
 
        self.committer_label.show()
 
200
 
        align = gtk.Alignment(0.0, 0.5)
 
202
 
        label.set_markup("<b>Branch nick:</b>")
 
204
 
        table.attach(align, 0, 1, 2, 3, gtk.FILL, gtk.FILL)
 
208
 
        align = gtk.Alignment(0.0, 0.5)
 
209
 
        self.branchnick_label = gtk.Label()
 
210
 
        self.branchnick_label.set_selectable(True)
 
211
 
        align.add(self.branchnick_label)
 
212
 
        table.attach(align, 1, 2, 2, 3, gtk.EXPAND | gtk.FILL, gtk.FILL)
 
213
 
        self.branchnick_label.show()
 
216
 
        align = gtk.Alignment(0.0, 0.5)
 
218
 
        label.set_markup("<b>Timestamp:</b>")
 
220
 
        table.attach(align, 0, 1, 3, 4, gtk.FILL, gtk.FILL)
 
224
 
        align = gtk.Alignment(0.0, 0.5)
 
225
 
        self.timestamp_label = gtk.Label()
 
226
 
        self.timestamp_label.set_selectable(True)
 
227
 
        align.add(self.timestamp_label)
 
228
 
        table.attach(align, 1, 2, 3, 4, gtk.EXPAND | gtk.FILL, gtk.FILL)
 
229
 
        self.timestamp_label.show()
 
232
 
        self.parents_table = gtk.Table(rows=1, columns=2)
 
233
 
        self.parents_table.set_row_spacings(3)
 
234
 
        self.parents_table.set_col_spacings(6)
 
235
 
        self.parents_table.show()
 
236
 
        vbox.pack_start(self.parents_table, expand=False, fill=True)
 
237
 
        self.parents_widgets = []
 
240
 
        label.set_markup("<b>Parents:</b>")
 
241
 
        align = gtk.Alignment(0.0, 0.5)
 
243
 
        self.parents_table.attach(align, 0, 1, 0, 1, gtk.FILL, gtk.FILL)
 
247
 
        self.message_buffer = gtk.TextBuffer()
 
248
 
        textview = gtk.TextView(self.message_buffer)
 
249
 
        textview.set_editable(False)
 
250
 
        textview.set_wrap_mode(gtk.WRAP_WORD)
 
251
 
        textview.modify_font(pango.FontDescription("Monospace"))
 
252
 
        vbox.pack_start(textview, expand=True, fill=True)
 
257
 
    def set_branch(self, branch, start, maxnum):
 
258
 
        """Set the branch and start position for this window.
 
260
 
        Creates a new TreeModel and populates it with information about
 
261
 
        the new branch before updating the window title and model of the
 
266
 
        # [ revision, node, last_lines, lines, message, committer, timestamp ]
 
267
 
        self.model = gtk.ListStore(gobject.TYPE_PYOBJECT,
 
268
 
                                   gobject.TYPE_PYOBJECT,
 
269
 
                                   gobject.TYPE_PYOBJECT,
 
270
 
                                   gobject.TYPE_PYOBJECT,
 
276
 
        (self.revisions, colours, self.children, self.parent_ids,
 
277
 
         merge_sorted) = distances(branch, start)
 
278
 
        for (index, (revision, node, lines)) in enumerate(graph(
 
279
 
                self.revisions, colours, merge_sorted)):
 
280
 
            # FIXME: at this point we should be able to show the graph order
 
281
 
            # and lines with no message or commit data - and then incrementally
 
282
 
            # fill the timestamp, committer etc data as desired.
 
283
 
            message = revision.message.split("\n")[0]
 
284
 
            if revision.committer is not None:
 
285
 
                timestamp = format_date(revision.timestamp, revision.timezone)
 
288
 
            self.model.append([revision, node, last_lines, lines,
 
289
 
                               message, revision.committer, timestamp])
 
290
 
            self.index[revision] = index
 
292
 
            if maxnum is not None and index > maxnum:
 
295
 
        self.set_title(branch.nick + " - bzrk")
 
296
 
        self.treeview.set_model(self.model)
 
298
 
    def _treeview_cursor_cb(self, *args):
 
299
 
        """Callback for when the treeview cursor changes."""
 
300
 
        (path, col) = self.treeview.get_cursor()
 
301
 
        revision = self.model[path][0]
 
303
 
        self.back_button.set_sensitive(len(self.parent_ids[revision]) > 0)
 
304
 
        self.fwd_button.set_sensitive(len(self.children[revision]) > 0)
 
306
 
        if revision.committer is not None:
 
308
 
            committer = revision.committer
 
309
 
            timestamp = format_date(revision.timestamp, revision.timezone)
 
310
 
            message = revision.message
 
312
 
                branchnick = revision.properties['branch-nick']
 
322
 
        self.revid_label.set_text(revision.revision_id)
 
323
 
        self.branchnick_label.set_text(branchnick)
 
325
 
        self.committer_label.set_text(committer)
 
326
 
        self.timestamp_label.set_text(timestamp)
 
327
 
        self.message_buffer.set_text(message)
 
329
 
        for widget in self.parents_widgets:
 
330
 
            self.parents_table.remove(widget)
 
332
 
        self.parents_widgets = []
 
333
 
        self.parents_table.resize(max(len(self.parent_ids[revision]), 1), 2)
 
335
 
        for idx, parent_id in enumerate(self.parent_ids[revision]):
 
336
 
            align = gtk.Alignment(0.0, 0.0)
 
337
 
            self.parents_widgets.append(align)
 
338
 
            self.parents_table.attach(align, 1, 2, idx, idx + 1,
 
339
 
                                      gtk.EXPAND | gtk.FILL, gtk.FILL)
 
342
 
            hbox = gtk.HBox(False, spacing=6)
 
347
 
            image.set_from_stock(
 
348
 
                gtk.STOCK_FIND, gtk.ICON_SIZE_SMALL_TOOLBAR)
 
351
 
            button = gtk.Button()
 
353
 
            button.set_sensitive(self.app is not None)
 
354
 
            button.connect("clicked", self._show_clicked_cb,
 
355
 
                           revision.revision_id, parent_id)
 
356
 
            hbox.pack_start(button, expand=False, fill=True)
 
359
 
            button = gtk.Button(parent_id)
 
360
 
            button.set_use_underline(False)
 
361
 
            button.connect("clicked", self._go_clicked_cb, parent_id)
 
362
 
            hbox.pack_start(button, expand=False, fill=True)
 
 
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
        if revision is not None:
 
 
482
            self.show_diff(revision.revision_id, parent_id)
 
 
484
            self.show_diff(NULL_REVISION)
 
 
485
        self.treeview.grab_focus()
 
366
487
    def _back_clicked_cb(self, *args):
 
367
488
        """Callback for when the back button is clicked."""
 
368
 
        (path, col) = self.treeview.get_cursor()
 
369
 
        revision = self.model[path][0]
 
370
 
        if not len(self.parent_ids[revision]):
 
373
 
        for parent_id in self.parent_ids[revision]:
 
374
 
            parent = self.revisions[parent_id]
 
375
 
            if same_branch(revision, parent):
 
376
 
                self.treeview.set_cursor(self.index[parent])
 
379
 
            next = self.revisions[self.parent_ids[revision][0]]
 
380
 
            self.treeview.set_cursor(self.index[next])
 
381
 
        self.treeview.grab_focus()
 
383
491
    def _fwd_clicked_cb(self, *args):
 
384
492
        """Callback for when the forward button is clicked."""
 
385
 
        (path, col) = self.treeview.get_cursor()
 
386
 
        revision = self.model[path][0]
 
387
 
        if not len(self.children[revision]):
 
390
 
        for child in self.children[revision]:
 
391
 
            if same_branch(child, revision):
 
392
 
                self.treeview.set_cursor(self.index[child])
 
395
 
            prev = list(self.children[revision])[0]
 
396
 
            self.treeview.set_cursor(self.index[prev])
 
397
 
        self.treeview.grab_focus()
 
399
 
    def _go_clicked_cb(self, widget, revid):
 
 
493
        self.treeview.forward()
 
 
495
    def _go_clicked_cb(self, w, p):
 
400
496
        """Callback for when the go button for a parent is clicked."""
 
401
 
        self.treeview.set_cursor(self.index[self.revisions[revid]])
 
402
 
        self.treeview.grab_focus()
 
 
497
        if self.revisionview.get_revision() is not None:
 
 
498
            self.treeview.set_revision(self.revisionview.get_revision())
 
404
 
    def _show_clicked_cb(self, widget, revid, parentid):
 
 
500
    def _show_clicked_cb(self, revid, parentid):
 
405
501
        """Callback for when the show button for a parent is clicked."""
 
406
 
        if self.app is not None:
 
407
 
            self.app.show_diff(self.branch, revid, parentid)
 
408
 
        self.treeview.grab_focus()
 
410
 
    def _treeview_row_activated_cb(self, widget, path, col):
 
411
 
        # TODO: more than one parent
 
412
 
        """Callback for when a treeview row gets activated."""
 
413
 
        revision = self.model[path][0]
 
414
 
        if len(self.parent_ids[revision]) == 0:
 
415
 
            # Ignore revisions without parent
 
417
 
        parent_id = self.parent_ids[revision][0]
 
418
 
        if self.app is not None:
 
419
 
            self.app.show_diff(self.branch, revision.revision_id, parent_id)
 
420
 
        self.treeview.grab_focus()
 
 
502
        self.show_diff(revid, parentid)
 
 
503
        self.treeview.grab_focus()
 
 
505
    def _set_revision_cb(self, w, revision_id):
 
 
506
        self.treeview.set_revision_id(revision_id)
 
 
508
    def _brokenlines_toggled_cb(self, button):
 
 
509
        self.compact_view = button.get_active()
 
 
511
        if self.compact_view:
 
 
516
        self.config.set_user_option('viz-compact-view', option)
 
 
517
        self.treeview.set_property('compact', self.compact_view)
 
 
518
        self.treeview.refresh()
 
 
520
    def _branch_index_cb(self, w):
 
 
521
        from bzrlib.plugins.search import index as _mod_index
 
 
522
        _mod_index.index_url(self.branch.base)
 
 
524
    def _branch_search_cb(self, w):
 
 
525
        from bzrlib.plugins.search import (
 
 
527
            errors as search_errors,
 
 
529
        from bzrlib.plugins.gtk.search import SearchDialog
 
 
532
            index = _mod_index.open_index_url(self.branch.base)
 
 
533
        except search_errors.NoSearchIndex:
 
 
534
            dialog = Gtk.MessageDialog(self, type=Gtk.MessageType.QUESTION, 
 
 
535
                buttons=Gtk.ButtonsType.OK_CANCEL, 
 
 
536
                message_format="This branch has not been indexed yet. "
 
 
538
            if dialog.run() == Gtk.ResponseType.OK:
 
 
540
                index = _mod_index.index_url(self.branch.base)
 
 
545
        dialog = SearchDialog(index)
 
 
547
        if dialog.run() == Gtk.ResponseType.OK:
 
 
548
            revid = dialog.get_revision()
 
 
549
            if revid is not None:
 
 
550
                self.set_revision(revid)
 
 
554
    def _about_dialog_cb(self, w):
 
 
555
        from bzrlib.plugins.gtk.about import AboutDialog
 
 
558
    def _col_visibility_changed(self, col, property):
 
 
559
        self.config.set_user_option(property + '-column-visible', col.get_active())
 
 
560
        self.treeview.set_property(property + '-column-visible', col.get_active())
 
 
562
    def _toolbar_visibility_changed(self, col):
 
 
567
        self.config.set_user_option('viz-toolbar-visible', col.get_active())
 
 
569
    def _vertical_layout(self, col):
 
 
570
        """Toggle the layout vertical/horizontal"""
 
 
571
        self.config.set_user_option('viz-vertical', str(col.get_active()))
 
 
574
        self.vbox.remove(old)
 
 
575
        self.vbox.pack_start(
 
 
576
            self.construct_paned(), True, True, 0)
 
 
577
        self._make_diff_paned_nonzero_size()
 
 
578
        self._make_diff_nonzero_size()
 
 
580
        self.treeview.emit('revision-selected')
 
 
582
    def _make_diff_paned_nonzero_size(self):
 
 
583
        """make sure the diff/revision pane isn't zero-width or zero-height"""
 
 
584
        alloc = self.diff_paned.get_allocation()
 
 
585
        if (alloc.width < 10) or (alloc.height < 10):
 
 
586
            width, height = self.get_size()
 
 
587
            self.diff_paned.set_size_request(width/3, int(height / 2.5))
 
 
589
    def _make_diff_nonzero_size(self):
 
 
590
        """make sure the diff isn't zero-width or zero-height"""
 
 
591
        alloc = self.diff.get_allocation()
 
 
592
        if (alloc.width < 10) or (alloc.height < 10):
 
 
593
            width, height = self.get_size()
 
 
594
            self.revisionview.set_size_request(width/3, int(height / 2.5))
 
 
596
    def _diff_visibility_changed(self, col):
 
 
597
        """Hide or show the diff panel."""
 
 
600
            self._make_diff_nonzero_size()
 
 
603
        self.config.set_user_option('viz-show-diffs', str(col.get_active()))
 
 
604
        self.update_diff_panel()
 
 
606
    def _diff_placement_changed(self, col):
 
 
607
        """Toggle the diff panel's position."""
 
 
608
        self.config.set_user_option('viz-wide-diffs', str(col.get_active()))
 
 
610
        old = self.paned.get_child2()
 
 
611
        self.paned.remove(old)
 
 
612
        self.paned.pack2(self.construct_bottom(), resize=True, shrink=False)
 
 
613
        self._make_diff_nonzero_size()
 
 
615
        self.treeview.emit('revision-selected')
 
 
617
    def _diff_wrap_changed(self, widget):
 
 
618
        """Toggle word wrap in the diff widget."""
 
 
619
        self.config.set_user_option('viz-wrap-diffs', widget.get_active())
 
 
620
        self.diff._on_wraplines_toggled(widget)
 
 
622
    def _refresh_clicked(self, w):
 
 
623
        self.treeview.refresh()
 
 
625
    def _update_tags(self):
 
 
628
        if self.branch.supports_tags():
 
 
629
            tags = self.branch.tags.get_tag_dict().items()
 
 
630
            tags.sort(reverse=True)
 
 
631
            for tag, revid in tags:
 
 
632
                tag_image = Gtk.Image()
 
 
633
                tag_image.set_from_file(icon_path('tag-16.png'))
 
 
634
                tag_item = Gtk.ImageMenuItem.new_with_mnemonic(
 
 
635
                    tag.replace('_', '__'))
 
 
636
                tag_item.set_image(tag_image)
 
 
637
                tag_item.connect('activate', self._tag_selected_cb, revid)
 
 
638
                tag_item.set_sensitive(self.treeview.has_revision_id(revid))
 
 
640
            self.go_menu_tags.set_submenu(menu)
 
 
642
            self.go_menu_tags.set_sensitive(len(tags) != 0)
 
 
644
            self.go_menu_tags.set_sensitive(False)
 
 
646
        self.go_menu_tags.show_all()
 
 
648
    def _load_size(self, name):
 
 
649
        """Read and parse 'name' from self.config.
 
 
650
        The value is a string, formatted as WIDTHxHEIGHT
 
 
651
        Returns None, or (width, height)
 
 
653
        size = self.config.get_user_option(name)
 
 
655
            width, height = [int(num) for num in size.split('x')]
 
 
656
            # avoid writing config every time we start
 
 
660
    def show_diff(self, revid, parentid=NULL_REVISION):
 
 
661
        """Open a new window to show a diff between the given revisions."""
 
 
662
        from bzrlib.plugins.gtk.diff import DiffWindow
 
 
663
        window = DiffWindow(parent=self)
 
 
665
        rev_tree    = self.branch.repository.revision_tree(revid)
 
 
666
        parent_tree = self.branch.repository.revision_tree(parentid)
 
 
668
        description = revid + " - " + self.branch._get_nick(local=True)
 
 
669
        window.set_diff(description, rev_tree, parent_tree)
 
 
672
    def update_diff_panel(self, revision=None, parents=None):
 
 
673
        """Show the current revision in the diff panel."""
 
 
674
        if self.config.get_user_option('viz-show-diffs') != 'True':
 
 
677
        if not revision: # default to selected row
 
 
678
            revision = self.treeview.get_revision()
 
 
679
        if revision == NULL_REVISION:
 
 
682
        if not parents: # default to selected row's parents
 
 
683
            parents  = self.treeview.get_parents()
 
 
684
        if len(parents) == 0:
 
 
685
            parent_id = NULL_REVISION
 
 
687
            parent_id = parents[0]
 
 
689
        rev_tree    = self.branch.repository.revision_tree(revision.revision_id)
 
 
690
        parent_tree = self.branch.repository.revision_tree(parent_id)
 
 
692
        self.diff.set_diff(rev_tree, parent_tree)
 
 
693
        if self.config.get_user_option('viz-wrap-diffs') == 'True':
 
 
694
            self.diff._on_wraplines_toggled(wrap=True)