1# Copyright (C) 2009 Vincent Legoll <vincent.legoll@gmail.com> 2# Copyright (C) 2010-2011, 2013-2015 Kai Willadsen <kai.willadsen@gmail.com> 3# 4# This program is free software: you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation, either version 2 of the License, or (at 7# your option) any later version. 8# 9# This program is distributed in the hope that it will be useful, but 10# WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12# General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License 15# along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17import logging 18from enum import Enum 19 20from gi.repository import Gdk 21from gi.repository import Gio 22from gi.repository import GLib 23from gi.repository import GObject 24from gi.repository import Gtk 25from gi.repository import GtkSource 26 27from meld.meldbuffer import MeldBuffer 28from meld.misc import colour_lookup_with_fallback, get_common_theme 29from meld.settings import bind_settings, meldsettings, settings 30 31 32log = logging.getLogger(__name__) 33 34 35def get_custom_encoding_candidates(): 36 custom_candidates = [] 37 try: 38 for charset in settings.get_value('detect-encodings'): 39 encoding = GtkSource.Encoding.get_from_charset(charset) 40 if not encoding: 41 log.warning('Invalid charset "%s" skipped', charset) 42 continue 43 custom_candidates.append(encoding) 44 if custom_candidates: 45 custom_candidates.extend( 46 GtkSource.Encoding.get_default_candidates()) 47 except AttributeError: 48 # get_default_candidates() is only available in GtkSourceView 3.18 49 # and we'd rather use their defaults than our old detect list. 50 pass 51 return custom_candidates 52 53 54class LanguageManager: 55 56 manager = GtkSource.LanguageManager() 57 58 @classmethod 59 def get_language_from_file(cls, gfile): 60 try: 61 info = gfile.query_info( 62 Gio.FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, 0, None) 63 except (GLib.GError, AttributeError): 64 return None 65 content_type = info.get_content_type() 66 return cls.manager.guess_language(gfile.get_basename(), content_type) 67 68 @classmethod 69 def get_language_from_mime_type(cls, mime_type): 70 content_type = Gio.content_type_from_mime_type(mime_type) 71 return cls.manager.guess_language(None, content_type) 72 73 74class TextviewLineAnimationType(Enum): 75 fill = 'fill' 76 stroke = 'stroke' 77 78 79class TextviewLineAnimation: 80 __slots__ = ("start_mark", "end_mark", "start_rgba", "end_rgba", 81 "start_time", "duration", "anim_type") 82 83 def __init__(self, mark0, mark1, rgba0, rgba1, duration, anim_type): 84 self.start_mark = mark0 85 self.end_mark = mark1 86 self.start_rgba = rgba0 87 self.end_rgba = rgba1 88 self.start_time = GLib.get_monotonic_time() 89 self.duration = duration 90 self.anim_type = anim_type 91 92 93class MeldSourceView(GtkSource.View): 94 95 __gtype_name__ = "MeldSourceView" 96 97 __gsettings_bindings__ = ( 98 ('highlight-current-line', 'highlight-current-line-local'), 99 ('indent-width', 'tab-width'), 100 ('insert-spaces-instead-of-tabs', 'insert-spaces-instead-of-tabs'), 101 ('draw-spaces', 'draw-spaces'), 102 ('wrap-mode', 'wrap-mode'), 103 ('show-line-numbers', 'show-line-numbers'), 104 ) 105 106 # Named so as not to conflict with the GtkSourceView property 107 highlight_current_line_local = GObject.Property(type=bool, default=False) 108 109 def get_show_line_numbers(self): 110 return self._show_line_numbers 111 112 def set_show_line_numbers(self, show): 113 if show == self._show_line_numbers: 114 return 115 116 if self.line_renderer: 117 self.line_renderer.set_visible(show) 118 119 self._show_line_numbers = bool(show) 120 self.notify("show-line-numbers") 121 122 show_line_numbers = GObject.Property( 123 type=bool, default=False, getter=get_show_line_numbers, 124 setter=set_show_line_numbers) 125 126 replaced_entries = ( 127 # We replace the default GtkSourceView undo mechanism 128 (Gdk.KEY_z, Gdk.ModifierType.CONTROL_MASK), 129 (Gdk.KEY_z, Gdk.ModifierType.CONTROL_MASK | 130 Gdk.ModifierType.SHIFT_MASK), 131 132 # We replace the default line movement behaviour of Alt+Up/Down 133 (Gdk.KEY_Up, Gdk.ModifierType.MOD1_MASK), 134 (Gdk.KEY_KP_Up, Gdk.ModifierType.MOD1_MASK), 135 (Gdk.KEY_KP_Up, Gdk.ModifierType.MOD1_MASK | 136 Gdk.ModifierType.SHIFT_MASK), 137 (Gdk.KEY_Down, Gdk.ModifierType.MOD1_MASK), 138 (Gdk.KEY_KP_Down, Gdk.ModifierType.MOD1_MASK), 139 (Gdk.KEY_KP_Down, Gdk.ModifierType.MOD1_MASK | 140 Gdk.ModifierType.SHIFT_MASK), 141 # ...and Alt+Left/Right 142 (Gdk.KEY_Left, Gdk.ModifierType.MOD1_MASK), 143 (Gdk.KEY_KP_Left, Gdk.ModifierType.MOD1_MASK), 144 (Gdk.KEY_Right, Gdk.ModifierType.MOD1_MASK), 145 (Gdk.KEY_KP_Right, Gdk.ModifierType.MOD1_MASK), 146 ) 147 148 def __init__(self, *args, **kwargs): 149 super().__init__(*args, **kwargs) 150 151 self.drag_dest_add_uri_targets() 152 153 binding_set = Gtk.binding_set_find('GtkSourceView') 154 for key, modifiers in self.replaced_entries: 155 Gtk.binding_entry_remove(binding_set, key, modifiers) 156 self.anim_source_id = None 157 self.animating_chunks = [] 158 self.syncpoints = [] 159 self._show_line_numbers = None 160 161 buf = MeldBuffer() 162 inline_tag = GtkSource.Tag.new("inline") 163 inline_tag.props.draw_spaces = True 164 buf.get_tag_table().add(inline_tag) 165 buf.create_tag("dimmed") 166 self.set_buffer(buf) 167 168 meldsettings.connect('changed', self.on_setting_changed) 169 170 def do_paste_clipboard(self, *args): 171 # This is an awful hack to replace another awful hack. The idea 172 # here is to sanitise the clipboard contents so that it doesn't 173 # contain GtkTextTags, by requesting and setting plain text. 174 175 def text_received_cb(clipboard, text, *user_data): 176 # Manual encoding is required here, or the length will be 177 # incorrect, and the API requires a UTF-8 bytestring. 178 utf8_text = text.encode('utf-8') 179 clipboard.set_text(text, len(utf8_text)) 180 self.get_buffer().paste_clipboard( 181 clipboard, None, self.get_editable()) 182 183 clipboard = self.get_clipboard(Gdk.SELECTION_CLIPBOARD) 184 clipboard.request_text(text_received_cb) 185 186 def get_y_for_line_num(self, line): 187 buf = self.get_buffer() 188 it = buf.get_iter_at_line(line) 189 y, h = self.get_line_yrange(it) 190 if line >= buf.get_line_count(): 191 return y + h 192 return y 193 194 def get_line_num_for_y(self, y): 195 return self.get_line_at_y(y)[0].get_line() 196 197 def add_fading_highlight( 198 self, mark0, mark1, colour_name, duration, 199 anim_type=TextviewLineAnimationType.fill, starting_alpha=1.0): 200 201 if not self.get_realized(): 202 return 203 204 rgba0 = self.fill_colors[colour_name].copy() 205 rgba1 = self.fill_colors[colour_name].copy() 206 rgba0.alpha = starting_alpha 207 rgba1.alpha = 0.0 208 anim = TextviewLineAnimation( 209 mark0, mark1, rgba0, rgba1, duration, anim_type) 210 self.animating_chunks.append(anim) 211 212 def on_setting_changed(self, settings, key): 213 if key == 'font': 214 self.override_font(meldsettings.font) 215 elif key == 'style-scheme': 216 self.highlight_color = colour_lookup_with_fallback( 217 "meld:current-line-highlight", "background") 218 self.syncpoint_color = colour_lookup_with_fallback( 219 "meld:syncpoint-outline", "foreground") 220 self.fill_colors, self.line_colors = get_common_theme() 221 222 tag = self.get_buffer().get_tag_table().lookup("inline") 223 tag.props.background_rgba = colour_lookup_with_fallback( 224 "meld:inline", "background") 225 tag = self.get_buffer().get_tag_table().lookup("dimmed") 226 tag.props.foreground_rgba = colour_lookup_with_fallback( 227 "meld:dimmed", "foreground") 228 229 def do_realize(self): 230 bind_settings(self) 231 self.on_setting_changed(meldsettings, 'font') 232 self.on_setting_changed(meldsettings, 'style-scheme') 233 return GtkSource.View.do_realize(self) 234 235 def do_draw_layer(self, layer, context): 236 if layer != Gtk.TextViewLayer.BELOW_TEXT: 237 return GtkSource.View.do_draw_layer(self, layer, context) 238 239 context.save() 240 context.set_line_width(1.0) 241 242 _, clip = Gdk.cairo_get_clip_rectangle(context) 243 bounds = ( 244 self.get_line_num_for_y(clip.y), 245 self.get_line_num_for_y(clip.y + clip.height), 246 ) 247 248 x = clip.x - 0.5 249 width = clip.width + 1 250 251 # Paint chunk backgrounds and outlines 252 for change in self.chunk_iter(bounds): 253 ypos0 = self.get_y_for_line_num(change[1]) 254 ypos1 = self.get_y_for_line_num(change[2]) 255 height = max(0, ypos1 - ypos0 - 1) 256 257 context.rectangle(x, ypos0 + 0.5, width, height) 258 if change[1] != change[2]: 259 context.set_source_rgba(*self.fill_colors[change[0]]) 260 context.fill_preserve() 261 if self.current_chunk_check(change): 262 highlight = self.fill_colors['current-chunk-highlight'] 263 context.set_source_rgba(*highlight) 264 context.fill_preserve() 265 266 context.set_source_rgba(*self.line_colors[change[0]]) 267 context.stroke() 268 269 textbuffer = self.get_buffer() 270 271 # Paint current line highlight 272 if self.props.highlight_current_line_local and self.is_focus(): 273 it = textbuffer.get_iter_at_mark(textbuffer.get_insert()) 274 ypos, line_height = self.get_line_yrange(it) 275 context.rectangle(x, ypos, width, line_height) 276 context.set_source_rgba(*self.highlight_color) 277 context.fill() 278 279 # Draw syncpoint indicator lines 280 for syncpoint in self.syncpoints: 281 if syncpoint is None: 282 continue 283 syncline = textbuffer.get_iter_at_mark(syncpoint).get_line() 284 if bounds[0] <= syncline <= bounds[1]: 285 ypos = self.get_y_for_line_num(syncline) 286 context.rectangle(x, ypos - 0.5, width, 1) 287 context.set_source_rgba(*self.syncpoint_color) 288 context.stroke() 289 290 # Overdraw all animated chunks, and update animation states 291 new_anim_chunks = [] 292 for c in self.animating_chunks: 293 current_time = GLib.get_monotonic_time() 294 percent = min( 295 1.0, (current_time - c.start_time) / float(c.duration)) 296 rgba_pairs = zip(c.start_rgba, c.end_rgba) 297 rgba = [s + (e - s) * percent for s, e in rgba_pairs] 298 299 it = textbuffer.get_iter_at_mark(c.start_mark) 300 ystart, _ = self.get_line_yrange(it) 301 it = textbuffer.get_iter_at_mark(c.end_mark) 302 yend, _ = self.get_line_yrange(it) 303 if ystart == yend: 304 ystart -= 1 305 306 context.set_source_rgba(*rgba) 307 context.rectangle(x, ystart, width, yend - ystart) 308 if c.anim_type == TextviewLineAnimationType.stroke: 309 context.stroke() 310 else: 311 context.fill() 312 313 if current_time <= c.start_time + c.duration: 314 new_anim_chunks.append(c) 315 else: 316 textbuffer.delete_mark(c.start_mark) 317 textbuffer.delete_mark(c.end_mark) 318 self.animating_chunks = new_anim_chunks 319 320 if self.animating_chunks and self.anim_source_id is None: 321 def anim_cb(): 322 self.queue_draw() 323 return True 324 # Using timeout_add interferes with recalculation of inline 325 # highlighting; this mechanism could be improved. 326 self.anim_source_id = GLib.idle_add(anim_cb) 327 elif not self.animating_chunks and self.anim_source_id: 328 GLib.source_remove(self.anim_source_id) 329 self.anim_source_id = None 330 331 context.restore() 332 333 return GtkSource.View.do_draw_layer(self, layer, context) 334 335 336class CommitMessageSourceView(GtkSource.View): 337 338 __gtype_name__ = "CommitMessageSourceView" 339 340 __gsettings_bindings__ = ( 341 ('indent-width', 'tab-width'), 342 ('insert-spaces-instead-of-tabs', 'insert-spaces-instead-of-tabs'), 343 ('draw-spaces', 'draw-spaces'), 344 ) 345