gaupol/ruler.py

Source code for module gaupol.ruler from file gaupol/ruler.py.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# -*- coding: utf-8 -*-

# Copyright (C) 2006 Osmo Salomaa
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Functions to calculate line lengths and to show them in widgets."""

import aeidon
import gaupol

from gi.repository import Gtk
from gi.repository import Pango


class _Ruler:

    """
    Measurer of line lengths in various units.

    The "sans" font (whatever it happens to be) is used for size calculations
    in em units, as subtitles are commonly shown in a some sans serif font.
    This is an approximation, but should be far more accurate than using
    characters, at least in Latin and other variable width character scripts.
    """

    def __init__(self):
        """Initialize a :class:`_Ruler` instance."""
        self._em_length = None
        self._layout = None
        self._length_unit = None
        self._update_em_length()
        self._update_length_unit()
        gaupol.conf.connect_notify("editor", "length_unit", self)

    def get_char_length(self, text, strip=False, floor=False):
        """Return length of `text` measured in characters."""
        text = (aeidon.RE_ANY_TAG.sub("", text) if strip else text)
        return len(text.replace("\n", " "))

    def get_char_lengths(self, text, strip=False, floor=False):
        """Return line lengths of `text` measured in characters."""
        text = (aeidon.RE_ANY_TAG.sub("", text) if strip else text)
        return tuple(len(x) for x in text.split("\n"))

    def get_em_length(self, text, strip=False, floor=False):
        """Return length of `text` measured in ems."""
        text = (aeidon.RE_ANY_TAG.sub("", text) if strip else text)
        text = text.replace("\n", " ")
        self._layout.set_text(text, -1)
        length = self._layout.get_size()[0] / self._em_length
        return (int(length) if floor else length)

    def get_em_lengths(self, text, strip=False, floor=False):
        """Return line lengths of `text` measured in ems."""
        text = (aeidon.RE_ANY_TAG.sub("", text) if strip else text)
        lengths = []
        for line in text.split("\n"):
            self._layout.set_text(line, -1)
            length = self._layout.get_size()[0] / self._em_length
            lengths.append(int(length) if floor else length)
        return tuple(lengths)

    def get_lengths(self, text, strip=False, floor=False):
        """Return line lengths of `text` measured in default units."""
        if self._length_unit == gaupol.length_units.CHAR:
            return self.get_char_lengths(text, strip, floor)
        if self._length_unit == gaupol.length_units.EM:
            return self.get_em_lengths(text, strip, floor)
        raise ValueError("Invalid length unit: {}"
                         .format(repr(self._length_unit)))

    def _on_conf_editor_notify_length_unit(self, *args):
        """Update the length function used."""
        self._update_length_unit()

    def _update_em_length(self):
        """Update the length of em based on font description size."""
        self._layout = Gtk.Label().get_layout().copy()
        font_desc = self._layout.get_context().get_font_description()
        font_desc.merge(Pango.FontDescription("sans"), True)
        self._layout.set_font_description(font_desc)
        self._em_length = font_desc.get_size()

    def _update_length_unit(self):
        """Update the length function used."""
        self._length_unit = gaupol.conf.editor.length_unit


_ruler = _Ruler()

def _on_text_view_draw(text_view, cairoc):
    """Calculate and show line lengths in text view margin."""
    text_buffer = text_view.get_buffer()
    start, end = text_buffer.get_bounds()
    text = text_buffer.get_text(start, end, False)
    if not text: return
    lengths = get_lengths(text)
    layout = Pango.Layout(text_view.get_pango_context())
    layout.set_markup("\n".join(str(x) for x in lengths), -1)
    layout.set_alignment(Pango.Alignment.RIGHT)
    width = layout.get_pixel_size()[0]
    text_view.set_border_window_size(Gtk.TextWindowType.RIGHT, width+6)
    x, y = text_view.window_to_buffer_coords(Gtk.TextWindowType.RIGHT, 2, 4)
    x += text_view.get_border_width()
    style = text_view.get_style_context()
    Gtk.render_layout(style, cairoc, x, y, layout)

def connect_text_view(text_view):
    """Connect `text_view` to show line lengths in its margin."""
    context = text_view.get_pango_context()
    layout = Pango.Layout(context)
    layout.set_text("8", -1)
    width = layout.get_pixel_size()[0]
    text_view.set_border_window_size(Gtk.TextWindowType.RIGHT, width+6)
    handler_id = text_view.connect_after("draw", _on_text_view_draw)
    text_view.gaupol_ruler_handler_id = handler_id
    return handler_id

def disconnect_text_view(text_view):
    """Disconnect `text_view` from showing line lengths in its margin."""
    text_view.set_border_window_size(Gtk.TextWindowType.RIGHT, 0)
    if not hasattr(text_view, "gaupol_ruler_handler_id"): return
    handler_id = text_view.gaupol_ruler_handler_id
    del text_view.gaupol_ruler_handler_id
    return text_view.disconnect(handler_id)

def get_length_function(unit):
    """Return a function that returns text length in `unit`."""
    if unit == gaupol.length_units.CHAR:
        return _ruler.get_char_length
    if unit == gaupol.length_units.EM:
        return _ruler.get_em_length
    raise ValueError("Invalid length unit: {}"
                     .format(repr(unit)))

def get_lengths(text):
    """Return a sequence of floored line lengths without tags."""
    return _ruler.get_lengths(text, strip=True, floor=True)