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