1# Copyright (C) 2013-2014 Kai Willadsen <kai.willadsen@gmail.com> 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 16import math 17 18from gi.repository import Gdk 19from gi.repository import Gtk 20from gi.repository import GtkSource 21from gi.repository import Pango 22 23from meld.conf import _ 24from meld.const import MODE_DELETE, MODE_INSERT, MODE_REPLACE 25from meld.misc import get_common_theme 26from meld.settings import meldsettings 27from meld.ui.gtkcompat import get_style 28 29# Fixed size of the renderer. Ideally this would be font-dependent and 30# would adjust to other textview attributes, but that's both quite difficult 31# and not necessarily desirable. 32LINE_HEIGHT = 16 33 34GTK_RENDERER_STATE_MAPPING = { 35 GtkSource.GutterRendererState.NORMAL: Gtk.StateFlags.NORMAL, 36 GtkSource.GutterRendererState.CURSOR: Gtk.StateFlags.FOCUSED, 37 GtkSource.GutterRendererState.PRELIT: Gtk.StateFlags.PRELIGHT, 38 GtkSource.GutterRendererState.SELECTED: Gtk.StateFlags.SELECTED, 39} 40 41ALIGN_MODE_FIRST = GtkSource.GutterRendererAlignmentMode.FIRST 42 43 44def load(icon_name): 45 icon_theme = Gtk.IconTheme.get_default() 46 return icon_theme.load_icon(icon_name, LINE_HEIGHT, 0) 47 48 49def get_background_rgba(renderer): 50 '''Get and cache the expected background for the renderer widget 51 52 Current versions of GTK+ don't paint the background of text view 53 gutters with the actual expected widget background, which causes 54 them to look wrong when put next to any other widgets. This hack 55 just gets the background from the renderer's view, and then caches 56 it for performance, and on the basis that all renderers will be 57 assigned to similarly-styled views. This is fragile, but the 58 alternative is really significantly slower. 59 ''' 60 global _background_rgba 61 if _background_rgba is None: 62 if renderer.props.view: 63 stylecontext = renderer.props.view.get_style_context() 64 background_set, _background_rgba = ( 65 stylecontext.lookup_color('theme_bg_color')) 66 return _background_rgba 67 68 69_background_rgba = None 70 71 72def renderer_to_gtk_state(state): 73 gtk_state = Gtk.StateFlags(0) 74 for renderer_flag, gtk_flag in GTK_RENDERER_STATE_MAPPING.items(): 75 if renderer_flag & state: 76 gtk_state |= gtk_flag 77 return gtk_state 78 79 80class MeldGutterRenderer: 81 82 def set_renderer_defaults(self): 83 self.set_alignment_mode(GtkSource.GutterRendererAlignmentMode.FIRST) 84 self.set_padding(3, 0) 85 self.set_alignment(0.5, 0.5) 86 87 def on_setting_changed(self, meldsettings, key): 88 if key == 'style-scheme': 89 self.fill_colors, self.line_colors = get_common_theme() 90 alpha = self.fill_colors['current-chunk-highlight'].alpha 91 self.chunk_highlights = { 92 state: Gdk.RGBA(*[alpha + c * (1.0 - alpha) for c in colour]) 93 for state, colour in self.fill_colors.items() 94 } 95 96 def draw_chunks( 97 self, context, background_area, cell_area, start, end, state): 98 99 chunk = self._chunk 100 if not chunk: 101 return 102 103 line = start.get_line() 104 is_first_line = line == chunk[1] 105 is_last_line = line == chunk[2] - 1 106 if not (is_first_line or is_last_line): 107 # Only paint for the first and last lines of a chunk 108 return 109 110 x = background_area.x - 1 111 y = background_area.y 112 width = background_area.width + 2 113 height = 1 if chunk[1] == chunk[2] else background_area.height 114 115 context.set_line_width(1.0) 116 Gdk.cairo_set_source_rgba(context, self.line_colors[chunk[0]]) 117 if is_first_line: 118 context.move_to(x, y + 0.5) 119 context.rel_line_to(width, 0) 120 if is_last_line: 121 context.move_to(x, y - 0.5 + height) 122 context.rel_line_to(width, 0) 123 context.stroke() 124 125 def query_chunks(self, start, end, state): 126 line = start.get_line() 127 chunk_index = self.linediffer.locate_chunk(self.from_pane, line)[0] 128 in_chunk = chunk_index is not None 129 130 chunk = None 131 if in_chunk: 132 chunk = self.linediffer.get_chunk( 133 chunk_index, self.from_pane, self.to_pane) 134 135 if chunk is not None: 136 if chunk[1] == chunk[2]: 137 background_rgba = get_background_rgba(self) 138 elif self.props.view.current_chunk_check(chunk): 139 background_rgba = self.chunk_highlights[chunk[0]] 140 else: 141 background_rgba = self.fill_colors[chunk[0]] 142 else: 143 # TODO: Remove when fixed in upstream GTK+ 144 background_rgba = get_background_rgba(self) 145 self._chunk = chunk 146 self.set_background(background_rgba) 147 return in_chunk 148 149 150class GutterRendererChunkAction( 151 GtkSource.GutterRendererPixbuf, MeldGutterRenderer): 152 __gtype_name__ = "GutterRendererChunkAction" 153 154 ACTION_MAP = { 155 'LTR': { 156 MODE_REPLACE: load("meld-change-apply-right"), 157 MODE_DELETE: load("meld-change-delete"), 158 MODE_INSERT: load("meld-change-copy"), 159 }, 160 'RTL': { 161 MODE_REPLACE: load("meld-change-apply-left"), 162 MODE_DELETE: load("meld-change-delete"), 163 MODE_INSERT: load("meld-change-copy"), 164 } 165 } 166 167 def __init__(self, from_pane, to_pane, views, filediff, linediffer): 168 super().__init__() 169 self.set_renderer_defaults() 170 self.from_pane = from_pane 171 self.to_pane = to_pane 172 # FIXME: Views are needed only for editable checking; connect to this 173 # in Filediff instead? 174 self.views = views 175 # FIXME: Don't pass in the linediffer; pass a generator like elsewhere 176 self.linediffer = linediffer 177 self.mode = MODE_REPLACE 178 self.set_size(LINE_HEIGHT) 179 direction = 'LTR' if from_pane < to_pane else 'RTL' 180 if self.views[0].get_direction() == Gtk.TextDirection.RTL: 181 direction = 'LTR' if direction == 'RTL' else 'RTL' 182 183 self.is_action = False 184 self.action_map = self.ACTION_MAP[direction] 185 self.filediff = filediff 186 self.filediff.connect("action-mode-changed", 187 self.on_container_mode_changed) 188 189 meldsettings.connect('changed', self.on_setting_changed) 190 self.on_setting_changed(meldsettings, 'style-scheme') 191 192 def do_activate(self, start, area, event): 193 line = start.get_line() 194 chunk_index = self.linediffer.locate_chunk(self.from_pane, line)[0] 195 if chunk_index is None: 196 return 197 198 chunk = self.linediffer.get_chunk( 199 chunk_index, self.from_pane, self.to_pane) 200 if chunk[1] != line: 201 return 202 203 action = self._classify_change_actions(chunk) 204 if action == MODE_DELETE: 205 self.filediff.delete_chunk(self.from_pane, chunk) 206 elif action == MODE_INSERT: 207 copy_menu = self._make_copy_menu(chunk) 208 # TODO: Need a custom GtkMenuPositionFunc to position this next to 209 # the clicked gutter, not where the cursor is 210 copy_menu.popup(None, None, None, None, 0, event.time) 211 elif action == MODE_REPLACE: 212 self.filediff.replace_chunk(self.from_pane, self.to_pane, chunk) 213 214 def _make_copy_menu(self, chunk): 215 copy_menu = Gtk.Menu() 216 copy_up = Gtk.MenuItem.new_with_mnemonic(_("Copy _up")) 217 copy_down = Gtk.MenuItem.new_with_mnemonic(_("Copy _down")) 218 copy_menu.append(copy_up) 219 copy_menu.append(copy_down) 220 copy_menu.show_all() 221 222 # FIXME: This is horrible 223 widget = self.filediff.widget 224 copy_menu.attach_to_widget(widget, None) 225 226 def copy_chunk(widget, chunk, copy_up): 227 self.filediff.copy_chunk(self.from_pane, self.to_pane, chunk, 228 copy_up) 229 230 copy_up.connect('activate', copy_chunk, chunk, True) 231 copy_down.connect('activate', copy_chunk, chunk, False) 232 return copy_menu 233 234 def do_begin(self, *args): 235 self.views_editable = [v.get_editable() for v in self.views] 236 237 def do_draw(self, context, background_area, cell_area, start, end, state): 238 GtkSource.GutterRendererPixbuf.do_draw( 239 self, context, background_area, cell_area, start, end, state) 240 if self.is_action: 241 # TODO: Fix padding and min-height in CSS and use 242 # draw_style_common 243 style_context = get_style(None, "button.flat.image-button") 244 style_context.set_state(renderer_to_gtk_state(state)) 245 246 x = background_area.x + 1 247 y = background_area.y + 1 248 width = background_area.width - 2 249 height = background_area.height - 2 250 251 Gtk.render_background(style_context, context, x, y, width, height) 252 Gtk.render_frame(style_context, context, x, y, width, height) 253 254 pixbuf = self.props.pixbuf 255 pix_width, pix_height = pixbuf.props.width, pixbuf.props.height 256 257 xalign, yalign = self.get_alignment() 258 align_mode = self.get_alignment_mode() 259 if align_mode == GtkSource.GutterRendererAlignmentMode.CELL: 260 icon_x = x + (width - pix_width) // 2 261 icon_y = y + (height - pix_height) // 2 262 else: 263 line_iter = start if align_mode == ALIGN_MODE_FIRST else end 264 textview = self.get_view() 265 loc = textview.get_iter_location(line_iter) 266 line_x, line_y = textview.buffer_to_window_coords( 267 self.get_window_type(), loc.x, loc.y) 268 icon_x = cell_area.x + (cell_area.width - pix_width) * xalign 269 icon_y = line_y + (loc.height - pix_height) * yalign 270 271 Gtk.render_icon(style_context, context, pixbuf, icon_x, icon_y) 272 273 self.draw_chunks( 274 context, background_area, cell_area, start, end, state) 275 276 def do_query_activatable(self, start, area, event): 277 line = start.get_line() 278 chunk_index = self.linediffer.locate_chunk(self.from_pane, line)[0] 279 if chunk_index is not None: 280 # FIXME: This is all chunks, not just those shared with to_pane 281 chunk = self.linediffer.get_chunk(chunk_index, self.from_pane) 282 if chunk[1] == line: 283 return True 284 return False 285 286 def do_query_data(self, start, end, state): 287 self.query_chunks(start, end, state) 288 line = start.get_line() 289 290 if self._chunk and self._chunk[1] == line: 291 action = self._classify_change_actions(self._chunk) 292 pixbuf = self.action_map.get(action) 293 else: 294 pixbuf = None 295 self.is_action = bool(pixbuf) 296 self.props.pixbuf = pixbuf 297 298 def on_container_mode_changed(self, container, mode): 299 self.mode = mode 300 self.queue_draw() 301 302 def _classify_change_actions(self, change): 303 """Classify possible actions for the given change 304 305 Returns the action that can be performed given the content and 306 context of the change. 307 """ 308 editable, other_editable = self.views_editable 309 310 if not editable and not other_editable: 311 return None 312 313 # Reclassify conflict changes, since we treat them the same as a 314 # normal two-way change as far as actions are concerned 315 change_type = change[0] 316 if change_type == "conflict": 317 if change[1] == change[2]: 318 change_type = "insert" 319 elif change[3] == change[4]: 320 change_type = "delete" 321 else: 322 change_type = "replace" 323 324 if change_type == 'insert': 325 return None 326 327 action = self.mode 328 if action == MODE_DELETE and not editable: 329 action = None 330 elif action == MODE_INSERT and change_type == 'delete': 331 action = MODE_REPLACE 332 if not other_editable: 333 action = MODE_DELETE 334 return action 335 336 337# GutterRendererChunkLines is an adaptation of GtkSourceGutterRendererLines 338# Copyright (C) 2010 - Jesse van den Kieboom 339# 340# Python reimplementation is Copyright (C) 2015 Kai Willadsen 341 342 343class GutterRendererChunkLines( 344 GtkSource.GutterRendererText, MeldGutterRenderer): 345 __gtype_name__ = "GutterRendererChunkLines" 346 347 def __init__(self, from_pane, to_pane, linediffer): 348 super().__init__() 349 self.set_renderer_defaults() 350 self.from_pane = from_pane 351 self.to_pane = to_pane 352 # FIXME: Don't pass in the linediffer; pass a generator like elsewhere 353 self.linediffer = linediffer 354 355 self.num_line_digits = 0 356 self.changed_handler_id = None 357 358 meldsettings.connect('changed', self.on_setting_changed) 359 self.on_setting_changed(meldsettings, 'style-scheme') 360 361 def do_change_buffer(self, old_buffer): 362 if old_buffer: 363 old_buffer.disconnect(self.changed_handler_id) 364 365 view = self.get_view() 366 if view: 367 buf = view.get_buffer() 368 if buf: 369 self.changed_handler_id = buf.connect( 370 "changed", self.recalculate_size) 371 self.recalculate_size(buf) 372 373 def _measure_markup(self, markup): 374 layout = self.get_view().create_pango_layout() 375 layout.set_markup(markup) 376 w, h = layout.get_size() 377 return w / Pango.SCALE, h / Pango.SCALE 378 379 def recalculate_size(self, buf): 380 381 # Always calculate display size for at least two-digit line counts 382 num_lines = max(buf.get_line_count(), 99) 383 num_digits = int(math.ceil(math.log(num_lines, 10))) 384 385 if num_digits == self.num_line_digits: 386 return 387 388 self.num_line_digits = num_digits 389 markup = "<b>%d</b>" % num_lines 390 width, height = self._measure_markup(markup) 391 self.set_size(width) 392 393 def do_draw(self, context, background_area, cell_area, start, end, state): 394 GtkSource.GutterRendererText.do_draw( 395 self, context, background_area, cell_area, start, end, state) 396 self.draw_chunks( 397 context, background_area, cell_area, start, end, state) 398 399 def do_query_data(self, start, end, state): 400 self.query_chunks(start, end, state) 401 line = start.get_line() + 1 402 current_line = state & GtkSource.GutterRendererState.CURSOR 403 markup = "<b>%d</b>" % line if current_line else str(line) 404 self.set_markup(markup, -1) 405