1# -*- coding: utf-8 -*-
2
3# Copyright (C) 2006 Osmo Salomaa
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18"""Functions to calculate line lengths and to show them in widgets."""
19
20import aeidon
21import gaupol
22
23from gi.repository import Gtk
24from gi.repository import Pango
25
26
27class _Ruler:
28
29    """Measurer of line lengths in various units."""
30
31    def __init__(self):
32        """Initialize a :class:`_Ruler` instance."""
33        self._em_length = None
34        self._label = Gtk.Label()
35        self._update_em_length()
36
37    def get_char_length(self, text, strip=False, floor=False):
38        """Return length of `text` measured in characters."""
39        text = (aeidon.RE_ANY_TAG.sub("", text) if strip else text)
40        return len(text.replace("\n", " "))
41
42    def get_em_length(self, text, strip=False, floor=False):
43        """Return length of `text` measured in ems."""
44        text = (aeidon.RE_ANY_TAG.sub("", text) if strip else text)
45        self._label.set_text(text.replace("\n", " "))
46        width = self._label.get_preferred_width()[1]
47        length = width / self._em_length
48        return (int(length) if floor else length)
49
50    def _update_em_length(self):
51        """Update the length of em based on font rendering."""
52        text = "abcdefghijklmnopqrstuvwxyz"
53        self._label.set_text(text)
54        self._label.show()
55        width = self._label.get_preferred_width()[1]
56        # About 0.55 em per a-z average character.
57        # https://bugzilla.gnome.org/show_bug.cgi?id=763589
58        self._em_length = width / (0.55 * len(text))
59
60
61_ruler = _Ruler()
62
63def _on_text_view_draw(text_view, cairoc):
64    """Calculate and show line lengths in text view margin."""
65    text_buffer = text_view.get_buffer()
66    start, end = text_buffer.get_bounds()
67    text = text_buffer.get_text(start, end, False)
68    if not text: return
69    lengths = get_lengths(text)
70    layout = Pango.Layout(text_view.get_pango_context())
71    # XXX: Lines overlap if we don't set a spacing!?
72    layout.set_spacing(2*Pango.SCALE)
73    layout.set_markup("\n".join(str(x) for x in lengths), -1)
74    layout.set_alignment(Pango.Alignment.RIGHT)
75    width = layout.get_pixel_size()[0]
76    text_view.set_border_window_size(Gtk.TextWindowType.RIGHT, width+6)
77    x, y = text_view.window_to_buffer_coords(Gtk.TextWindowType.RIGHT, 2, 4)
78    x += text_view.get_border_width()
79    with aeidon.util.silent(AttributeError):
80        # Top margin available since GTK+ 3.18.
81        y += text_view.props.top_margin
82    style = text_view.get_style_context()
83    Gtk.render_layout(style, cairoc, x, y, layout)
84
85def connect_text_view(text_view):
86    """Connect `text_view` to show line lengths in its margin."""
87    context = text_view.get_pango_context()
88    layout = Pango.Layout(context)
89    layout.set_text("8", -1)
90    width = layout.get_pixel_size()[0]
91    text_view.set_border_window_size(Gtk.TextWindowType.RIGHT, width+6)
92    handler_id = text_view.connect_after("draw", _on_text_view_draw)
93    text_view.gaupol_ruler_handler_id = handler_id
94    return handler_id
95
96def disconnect_text_view(text_view):
97    """Disconnect `text_view` from showing line lengths in its margin."""
98    text_view.set_border_window_size(Gtk.TextWindowType.RIGHT, 0)
99    if not hasattr(text_view, "gaupol_ruler_handler_id"): return
100    handler_id = text_view.gaupol_ruler_handler_id
101    del text_view.gaupol_ruler_handler_id
102    return text_view.disconnect(handler_id)
103
104def get_length_function(unit):
105    """Return a function that returns text length in `unit`."""
106    if unit == gaupol.length_units.CHAR:
107        return _ruler.get_char_length
108    if unit == gaupol.length_units.EM:
109        return _ruler.get_em_length
110    raise ValueError("Invalid length unit: {}"
111                     .format(repr(unit)))
112
113def get_lengths(text):
114    """Return a sequence of floored line lengths without tags."""
115    fun = get_length_function(gaupol.conf.editor.length_unit)
116    return [fun(line, strip=True, floor=True) for line in text.split("\n")]
117