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