diffmap.py 7.21 KB
Newer Older
1 2 3
# Copyright (C) 2002-2009 Stephen Kennedy <stevek@gnome.org>
# Copyright (C) 2009-2013 Kai Willadsen <kai.willadsen@gmail.com>
#
4 5 6 7 8 9 10 11 12 13 14 15
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or (at
# your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

17 18
import collections

19 20
import cairo

21 22 23
from gi.repository import GObject
from gi.repository import Gdk
from gi.repository import Gtk
24

25
from meld.misc import get_common_theme
26
from meld.settings import meldsettings
27

28

29
class DiffMap(Gtk.DrawingArea):
30 31 32 33

    __gtype_name__ = "DiffMap"

    def __init__(self):
34 35
        GObject.GObject.__init__(self)
        self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
36 37 38 39 40 41 42
        self._scrolladj = None
        self._difffunc = lambda: None
        self._handlers = []
        self._y_offset = 0
        self._h_offset = 0
        self._scroll_y = 0
        self._scroll_height = 0
43
        self._setup = False
44
        self._width = 10
45 46
        meldsettings.connect('changed', self.on_setting_changed)
        self.on_setting_changed(meldsettings, 'style-scheme')
47

48
    def setup(self, scrollbar, change_chunk_fn):
49 50 51 52
        for (o, h) in self._handlers:
            o.disconnect(h)

        self._scrolladj = scrollbar.get_adjustment()
53
        self.on_scrollbar_style_updated(scrollbar)
54
        self.on_scrollbar_size_allocate(scrollbar, scrollbar.get_allocation())
55
        scrollbar.ensure_style()
56 57
        scroll_style_hid = scrollbar.connect("style-updated",
                                             self.on_scrollbar_style_updated)
58 59
        scroll_size_hid = scrollbar.connect("size-allocate",
                                            self.on_scrollbar_size_allocate)
60
        adj_change_hid = self._scrolladj.connect("changed",
Kai Willadsen's avatar
Kai Willadsen committed
61
                                                 lambda w: self.queue_draw())
62 63
        adj_val_hid = self._scrolladj.connect("value-changed",
                                              lambda w: self.queue_draw())
64
        self._handlers = [(scrollbar, scroll_style_hid),
65 66 67
                          (scrollbar, scroll_size_hid),
                          (self._scrolladj, adj_change_hid),
                          (self._scrolladj, adj_val_hid)]
68
        self._difffunc = change_chunk_fn
69
        self._setup = True
70
        self._cached_map = None
71 72
        self.queue_draw()

73 74 75
    def on_diffs_changed(self, *args):
        self._cached_map = None

76 77 78
    def on_setting_changed(self, meldsettings, key):
        if key == 'style-scheme':
            self.fill_colors, self.line_colors = get_common_theme()
79

80
    def on_scrollbar_style_updated(self, scrollbar):
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
        value = GObject.Value(int)
        scrollbar.style_get_property("stepper-size", value)
        stepper_size = value.get_int()
        scrollbar.style_get_property("stepper-spacing", value)
        stepper_spacing = value.get_int()

        bool_value = GObject.Value(bool)
        scrollbar.style_get_property("has-backward-stepper", bool_value)
        has_backward = bool_value.get_boolean()
        scrollbar.style_get_property("has-secondary-forward-stepper", bool_value)
        has_secondary_forward = bool_value.get_boolean()
        scrollbar.style_get_property("has-secondary-backward-stepper", bool_value)
        has_secondary_backward = bool_value.get_boolean()
        scrollbar.style_get_property("has-forward-stepper", bool_value)
        has_foreward = bool_value.get_boolean()
        steppers = [has_backward, has_secondary_forward, has_secondary_backward, has_foreward]
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111

        offset = stepper_size * steppers[0:2].count(True)
        shorter = stepper_size * steppers.count(True)
        if steppers[0] or steppers[1]:
            offset += stepper_spacing
            shorter += stepper_spacing
        if steppers[2] or steppers[3]:
            shorter += stepper_spacing
        self._y_offset = offset
        self._h_offset = shorter
        self.queue_draw()

    def on_scrollbar_size_allocate(self, scrollbar, allocation):
        self._scroll_y = allocation.y
        self._scroll_height = allocation.height
112
        self._width = max(allocation.width, 10)
113
        self._cached_map = None
114
        self.queue_resize()
115

116
    def do_draw(self, context):
117 118
        if not self._setup:
            return
119
        height = self._scroll_height - self._h_offset - 1
120 121 122
        y_start = self._scroll_y - self.get_allocation().y - self._y_offset + 1
        width = self.get_allocated_width()
        xpad = 2.5
123
        x0 = xpad
124
        x1 = width - 2 * xpad
125

126 127 128 129 130
        # Hack to work around a cairo bug when calling create_similar
        # https://bugs.freedesktop.org/show_bug.cgi?id=60519
        if not (width and height):
            return

131 132
        context.translate(0, y_start)
        context.set_line_width(1)
133
        context.rectangle(x0 - 3, -1, x1 + 6, height + 1)
134 135
        context.clip()

136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
        if self._cached_map is None:
            surface = cairo.Surface.create_similar(
                context.get_target(), cairo.CONTENT_COLOR_ALPHA,
                width, height)
            cache_ctx = cairo.Context(surface)
            cache_ctx.set_line_width(1)

            tagged_diffs = collections.defaultdict(list)
            for c, y0, y1 in self._difffunc():
                tagged_diffs[c].append((y0, y1))

            for tag, diffs in tagged_diffs.items():
                cache_ctx.set_source_rgba(*self.fill_colors[tag])
                for y0, y1 in diffs:
                    y0, y1 = round(y0 * height) - 0.5, round(y1 * height) - 0.5
                    cache_ctx.rectangle(x0, y0, x1, y1 - y0)
                cache_ctx.fill_preserve()
                cache_ctx.set_source_rgba(*self.line_colors[tag])
                cache_ctx.stroke()
            self._cached_map = surface

        context.set_source_surface(self._cached_map, 0., 0.)
        context.paint()
159

160 161 162
        page_color = (0., 0., 0., 0.1)
        page_outline_color = (0.0, 0.0, 0.0, 0.3)
        adj = self._scrolladj
163 164
        s = round(height * (adj.get_value() / adj.get_upper())) - 0.5
        e = round(height * (adj.get_page_size() / adj.get_upper()))
165 166 167 168 169 170
        context.set_source_rgba(*page_color)
        context.rectangle(x0 - 2, s, x1 + 4, e)
        context.fill_preserve()
        context.set_source_rgba(*page_outline_color)
        context.stroke()

171 172
    def do_button_press_event(self, event):
        if event.button == 1:
173
            y_start = self.get_allocation().y - self._scroll_y - self._y_offset
174 175 176 177
            total_height = self._scroll_height - self._h_offset
            fraction = (event.y + y_start) / total_height

            adj = self._scrolladj
178 179 180
            val = fraction * adj.get_upper() - adj.get_page_size() / 2
            upper = adj.get_upper() - adj.get_page_size()
            adj.set_value(max(min(upper, val), adj.get_lower()))
181 182 183
            return True
        return False

184
    def do_get_preferred_width(self):
185
        return self._width, self._width