1# Copyright (C) 2014 Marco Brito <bcaza@null.net>
2#
3# This program is free software: you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation, either version 2 of the License, or (at
6# your option) any later version.
7#
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11# General Public License for more details.
12#
13# You should have received a copy of the GNU General Public License
14# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
16from gi.repository import Gdk
17from gi.repository import GObject
18from gi.repository import Gtk
19
20
21class DiffGrid(Gtk.Grid):
22    __gtype_name__ = "DiffGrid"
23
24    def __init__(self):
25        super().__init__()
26        self._in_drag = False
27        self._drag_pos = -1
28        self._drag_handle = None
29        self._handle1 = HandleWindow()
30        self._handle2 = HandleWindow()
31
32    def do_realize(self):
33        Gtk.Grid.do_realize(self)
34        self._handle1.realize(self)
35        self._handle2.realize(self)
36
37    def do_unrealize(self):
38        self._handle1.unrealize()
39        self._handle2.unrealize()
40        Gtk.Grid.do_unrealize(self)
41
42    def do_map(self):
43        Gtk.Grid.do_map(self)
44        drag = self.get_child_at(2, 0)
45        if drag and drag.get_visible():
46            self._handle1.set_visible(True)
47
48        drag = self.get_child_at(4, 0)
49        if drag and drag.get_visible():
50            self._handle2.set_visible(True)
51
52    def do_unmap(self):
53        self._handle1.set_visible(False)
54        self._handle2.set_visible(False)
55        Gtk.Grid.do_unmap(self)
56
57    def _handle_set_prelight(self, window, flag):
58        if hasattr(window, "handle"):
59            window.handle.set_prelight(flag)
60            return True
61        return False
62
63    def do_enter_notify_event(self, event):
64        return self._handle_set_prelight(event.window, True)
65
66    def do_leave_notify_event(self, event):
67        if not self._in_drag:
68            return self._handle_set_prelight(event.window, False)
69        return False
70
71    def do_button_press_event(self, event):
72        if event.button & Gdk.BUTTON_PRIMARY:
73            self._drag_pos = event.x
74            self._in_drag = True
75            return True
76        return False
77
78    def do_button_release_event(self, event):
79        if event.button & Gdk.BUTTON_PRIMARY:
80            self._in_drag = False
81            return True
82        return False
83
84    def do_motion_notify_event(self, event):
85        if event.state & Gdk.ModifierType.BUTTON1_MASK:
86            if hasattr(event.window, "handle"):
87                x, y = event.window.get_position()
88                pos = round(x + event.x - self._drag_pos)
89                event.window.handle.set_position(pos)
90                self._drag_handle = event.window.handle
91                self.queue_resize_no_redraw()
92                return True
93        return False
94
95    def _calculate_positions(self, xmin, xmax, wlink1, wlink2,
96                             wpane1, wpane2, wpane3):
97        wremain = max(0, xmax - xmin - wlink1 - wlink2)
98        pos1 = self._handle1.get_position(wremain, xmin)
99        pos2 = self._handle2.get_position(wremain, xmin + wlink1)
100
101        if not self._drag_handle:
102            npanes = 0
103            if wpane1 > 0:
104                npanes += 1
105            if wpane2 > 0:
106                npanes += 1
107            if wpane3 > 0:
108                npanes += 1
109            wpane = float(wremain) / max(1, npanes)
110            if wpane1 > 0:
111                wpane1 = wpane
112            if wpane2 > 0:
113                wpane2 = wpane
114            if wpane3 > 0:
115                wpane3 = wpane
116
117        xminlink1 = xmin + wpane1
118        xmaxlink2 = xmax - wpane3 - wlink2
119        wlinkpane = wlink1 + wpane2
120
121        if wpane1 == 0:
122            pos1 = xminlink1
123        if wpane3 == 0:
124            pos2 = xmaxlink2
125        if wpane2 == 0:
126            if wpane3 == 0:
127                pos1 = pos2 - wlink2
128            else:
129                pos2 = pos1 + wlink1
130
131        if self._drag_handle == self._handle2:
132            xminlink2 = xminlink1 + wlinkpane
133            pos2 = min(max(xminlink2, pos2), xmaxlink2)
134            xmaxlink1 = pos2 - wlinkpane
135            pos1 = min(max(xminlink1, pos1), xmaxlink1)
136        else:
137            xmaxlink1 = xmaxlink2 - wlinkpane
138            pos1 = min(max(xminlink1, pos1), xmaxlink1)
139            xminlink2 = pos1 + wlinkpane
140            pos2 = min(max(xminlink2, pos2), xmaxlink2)
141
142        self._handle1.set_position(pos1)
143        self._handle2.set_position(pos2)
144        return int(round(pos1)), int(round(pos2))
145
146    def do_size_allocate(self, allocation):
147        # We should be chaining up here to:
148        #     Gtk.Grid.do_size_allocate(self, allocation)
149        # However, when we do this, we hit issues with doing multiple
150        # allocations in a single allocation cycle (see bgo#779883).
151
152        self.set_allocation(allocation)
153        wcols, hrows = self._get_min_sizes()
154        yrows = [allocation.y,
155                 allocation.y + hrows[0],
156                 # Roughly equivalent to hard-coding row 1 to expand=True
157                 allocation.y + (allocation.height - hrows[2] - hrows[3]),
158                 allocation.y + (allocation.height - hrows[3]),
159                 allocation.y + allocation.height]
160
161        wmap1, wpane1, wlink1, wpane2, wlink2, wpane3, wmap2 = wcols
162        xmin = allocation.x + wmap1
163        xmax = allocation.x + allocation.width - wmap2
164        pos1, pos2 = self._calculate_positions(xmin, xmax,
165                                               wlink1, wlink2,
166                                               wpane1, wpane2, wpane3)
167        wpane1 = pos1 - (allocation.x + wmap1)
168        wpane2 = pos2 - (pos1 + wlink1)
169        wpane3 = xmax - (pos2 + wlink2)
170        wcols = (
171            allocation.x, wmap1, wpane1, wlink1, wpane2, wlink2, wpane3, wmap2)
172        columns = [sum(wcols[:i + 1]) for i in range(len(wcols))]
173
174        def child_allocate(child):
175            if not child.get_visible():
176                return
177            left, top, width, height = self.child_get(
178                child, 'left-attach', 'top-attach', 'width', 'height')
179            # This is a copy, and we have to do this because there's no Python
180            # access to Gtk.Allocation.
181            child_alloc = self.get_allocation()
182            child_alloc.x = columns[left]
183            child_alloc.y = yrows[top]
184            child_alloc.width = columns[left + width] - columns[left]
185            child_alloc.height = yrows[top + height] - yrows[top]
186
187            if self.get_direction() == Gtk.TextDirection.RTL:
188                child_alloc.x = (
189                    allocation.x + allocation.width -
190                    (child_alloc.x - allocation.x) - child_alloc.width)
191
192            child.size_allocate(child_alloc)
193
194        for child in self.get_children():
195            child_allocate(child)
196
197        if self.get_realized():
198            mapped = self.get_mapped()
199            ydrag = yrows[0]
200            hdrag = yrows[1] - yrows[0]
201            self._handle1.set_visible(mapped and wlink1 > 0)
202            self._handle1.move_resize(pos1, ydrag, wlink1, hdrag)
203            self._handle2.set_visible(mapped and wlink2 > 0)
204            self._handle2.move_resize(pos2, ydrag, wlink2, hdrag)
205
206    def _get_min_sizes(self):
207        hrows = [0] * 4
208        wcols = [0] * 7
209        for row in range(0, 4):
210            for col in range(0, 7):
211                child = self.get_child_at(col, row)
212                if child and child.get_visible():
213                    msize, nsize = child.get_preferred_size()
214                    # Ignore spanning columns in width calculations; we should
215                    # do this properly, but it's difficult.
216                    spanning = GObject.Value(int)
217                    self.child_get_property(child, 'width', spanning)
218                    spanning = spanning.get_int()
219                    # We ignore natural size when calculating required
220                    # width, but use it when doing required height. The
221                    # logic here is that height-for-width means that
222                    # minimum width requisitions mean more-than-minimum
223                    # heights. This is all extremely dodgy, but works
224                    # for now.
225                    if spanning == 1:
226                        wcols[col] = max(wcols[col], msize.width)
227                    hrows[row] = max(hrows[row], msize.height, nsize.height)
228        return wcols, hrows
229
230    def do_draw(self, context):
231        Gtk.Grid.do_draw(self, context)
232        self._handle1.draw(context)
233        self._handle2.draw(context)
234
235
236class HandleWindow():
237    def __init__(self):
238        self._widget = None
239        self._window = None
240        self._area_x = -1
241        self._area_y = -1
242        self._area_width = 1
243        self._area_height = 1
244        self._prelit = False
245        self._pos = 0.0
246        self._transform = (0, 0)
247
248    def get_position(self, width, xtrans):
249        self._transform = (width, xtrans)
250        return float(self._pos * width) + xtrans
251
252    def set_position(self, pos):
253        width, xtrans = self._transform
254        self._pos = float(pos - xtrans) / width
255
256    def realize(self, widget):
257        attr = Gdk.WindowAttr()
258        attr.window_type = Gdk.WindowType.CHILD
259        attr.x = self._area_x
260        attr.y = self._area_y
261        attr.width = self._area_width
262        attr.height = self._area_height
263        attr.wclass = Gdk.WindowWindowClass.INPUT_OUTPUT
264        attr.event_mask = (widget.get_events() |
265                           Gdk.EventMask.BUTTON_PRESS_MASK |
266                           Gdk.EventMask.BUTTON_RELEASE_MASK |
267                           Gdk.EventMask.ENTER_NOTIFY_MASK |
268                           Gdk.EventMask.LEAVE_NOTIFY_MASK |
269                           Gdk.EventMask.POINTER_MOTION_MASK)
270        attr.cursor = Gdk.Cursor.new_for_display(widget.get_display(),
271                                                 Gdk.CursorType.
272                                                 SB_H_DOUBLE_ARROW)
273        attr_mask = (Gdk.WindowAttributesType.X |
274                     Gdk.WindowAttributesType.Y |
275                     Gdk.WindowAttributesType.CURSOR)
276
277        parent = widget.get_parent_window()
278        self._window = Gdk.Window(parent, attr, attr_mask)
279        self._window.handle = self
280        self._widget = widget
281        self._widget.register_window(self._window)
282
283    def unrealize(self):
284        self._widget.unregister_window(self._window)
285
286    def set_visible(self, visible):
287        if visible:
288            self._window.show()
289        else:
290            self._window.hide()
291
292    def move_resize(self, x, y, width, height):
293        self._window.move_resize(x, y, width, height)
294        self._area_x = x
295        self._area_y = y
296        self._area_width = width
297        self._area_height = height
298
299    def set_prelight(self, flag):
300        self._prelit = flag
301        self._widget.queue_draw_area(self._area_x, self._area_y,
302                                     self._area_width, self._area_height)
303
304    def draw(self, cairocontext):
305        alloc = self._widget.get_allocation()
306        padding = 5
307        x = self._area_x - alloc.x + padding
308        y = self._area_y - alloc.y + padding
309        width = max(0, self._area_width - 2 * padding)
310        height = max(0, self._area_height - 2 * padding)
311
312        if width == 0 or height == 0:
313            return
314
315        stylecontext = self._widget.get_style_context()
316        state = stylecontext.get_state()
317        if self._widget.is_focus():
318            state |= Gtk.StateFlags.SELECTED
319        if self._prelit:
320            state |= Gtk.StateFlags.PRELIGHT
321
322        if Gtk.cairo_should_draw_window(cairocontext, self._window):
323            stylecontext.save()
324            stylecontext.set_state(state)
325            stylecontext.add_class(Gtk.STYLE_CLASS_PANE_SEPARATOR)
326            color = stylecontext.get_background_color(state)
327            if color.alpha > 0.0:
328                Gtk.render_handle(stylecontext, cairocontext,
329                                  x, y, width, height)
330            else:
331                xcenter = x + width / 2.0
332                Gtk.render_line(stylecontext, cairocontext,
333                                xcenter, y, xcenter, y + height)
334            stylecontext.restore()
335