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