1# -*- coding: utf-8 -*-
2
3# Copyright (C) 2005 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"""Miscellaneous functions and decorators."""
19
20import aeidon
21import gaupol
22import inspect
23import sys
24import traceback
25import webbrowser
26
27from gi.repository import Gdk
28from gi.repository import GLib
29from gi.repository import Gtk
30
31
32def char_to_px(nchar, font=None):
33    """Convert characters to pixels."""
34    if nchar < 0: return nchar
35    label = Gtk.Label(label="etaoin shrdlu")
36    gaupol.style.use_font(label, font)
37    label.show()
38    width = label.get_preferred_width()[1]
39    return int(round(nchar * width/len(label.props.label)))
40
41def delay_add(delay, function, *args, **kwargs):
42    """Call `function` with `args` and `kwargs` once after `delay` (ms)."""
43    def call_function(*args, **kwargs):
44        function(*args, **kwargs)
45        return False # to not be called again.
46    return GLib.timeout_add(delay, call_function, *args, **kwargs)
47
48def document_to_text_field(doc):
49    """Return :attr:`gaupol.fields` item corresponding to `doc`."""
50    if doc == aeidon.documents.MAIN:
51        return gaupol.fields.MAIN_TEXT
52    if doc == aeidon.documents.TRAN:
53        return gaupol.fields.TRAN_TEXT
54    raise ValueError("Invalid document: {}"
55                     .format(repr(doc)))
56
57def flash_dialog(dialog):
58    """Run `dialog`, destroy it and return response."""
59    response = dialog.run()
60    dialog.destroy()
61    return response
62
63def get_content_size(widget, font=None):
64    """Return the width and height desired by `widget`."""
65    if isinstance(widget, Gtk.TextView):
66        return get_text_view_size(widget, font)
67    if isinstance(widget, Gtk.TreeView):
68        return get_tree_view_size(widget, font)
69    raise ValueError("Unsupported container type: {}"
70                     .format(repr(type(widget))))
71
72def get_font():
73    """Return custom font or blank string."""
74    return (gaupol.conf.editor.custom_font if
75            gaupol.conf.editor.use_custom_font and
76            gaupol.conf.editor.custom_font else "")
77
78def get_gst_version():
79    """Return :mod:`Gst` version number as string or ``None``."""
80    try:
81        from gi.repository import Gst
82        return ".".join(map(str, Gst.version()))
83    except Exception:
84        return None
85
86def get_icon_image(name, fallback, size):
87    """Return icon image from `name` or `fallback` in theme."""
88    theme = Gtk.IconTheme.get_default()
89    if theme.has_icon(name):
90        return Gtk.Image(icon_name=name, icon_size=size)
91    return Gtk.Image(icon_name=fallback, icon_size=size)
92
93def get_preview_command():
94    """Return command to use for lauching video player."""
95    if gaupol.conf.preview.use_custom_command:
96        return gaupol.conf.preview.custom_command
97    if gaupol.conf.preview.force_utf_8:
98        return gaupol.conf.preview.player.command_utf_8
99    return gaupol.conf.preview.player.command
100
101def get_text_view_size(text_view, font=None):
102    """Return the width and height desired by `text_view`."""
103    text_buffer = text_view.get_buffer()
104    start, end = text_buffer.get_bounds()
105    text = text_buffer.get_text(start, end, False)
106    label = Gtk.Label(label=text)
107    gaupol.style.use_font(label, font)
108    label.show()
109    return (label.get_preferred_width()[1]
110            + text_view.get_left_margin()
111            + text_view.get_right_margin(),
112            label.get_preferred_height()[1])
113
114def get_tree_view_size(tree_view, font=None):
115    """Return the width and height desired by `tree_view`."""
116    scroller = tree_view.get_parent()
117    policy = scroller.get_policy()
118    scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)
119    width = scroller.get_preferred_width()[1]
120    height = scroller.get_preferred_height()[1]
121    scroller.set_policy(*policy)
122    return width, height
123
124@aeidon.deco.once
125def get_zebra_color(tree_view):
126    """Return background color to use for tree view zebra-stripes."""
127    # XXX: Zebra stripes would be faster and cleaner done with CSS
128    # selectors :nth-child(odd) and :nth-child(even), but they don't
129    # seem to work, might even be deliberately broken.
130    # https://bugzilla.gnome.org/show_bug.cgi?id=709617#c1
131    style = tree_view.get_style_context()
132    fg = style.get_color(Gtk.StateFlags.NORMAL)
133    bg = style.get_background_color(Gtk.StateFlags.NORMAL)
134    color = Gdk.RGBA()
135    color.red   = 0.92 * bg.red   + 0.08 * fg.red
136    color.green = 0.92 * bg.green + 0.08 * fg.green
137    color.blue  = 0.92 * bg.blue  + 0.08 * fg.blue
138    return(color)
139
140@aeidon.deco.once
141def gst_available():
142    """Return ``True`` if :mod:`Gst` and needed plugins are available."""
143    try:
144        from gi.repository import Gst
145    except Exception:
146        return False
147    if not Gst.ElementFactory.find("playbin"):
148        print("GStreamer found, but playbin missing.",
149              "Try installing gst-plugins-base.",
150              file=sys.stderr)
151        return False
152    if not Gst.ElementFactory.find("textoverlay"):
153        print("GStreamer found, but textoverlay missing.",
154              "Try installing gst-plugins-base.",
155              file=sys.stderr)
156        return False
157    if not Gst.ElementFactory.find("timeoverlay"):
158        print("GStreamer found, but timeoverlay missing.",
159              "Try installing gst-plugins-base.",
160              file=sys.stderr)
161        return False
162    if not Gst.ElementFactory.find("gtksink"):
163        print("GStreamer found, but gtksink missing.",
164              "Try installing gst-plugins-good.",
165              file=sys.stderr)
166        return False
167    return True
168
169@aeidon.deco.once
170def gtkspell_available():
171    """Return ``True`` if :mod:`GtkSpell` module is available."""
172    try:
173        from gi.repository import GtkSpell
174        return True
175    except Exception:
176        return False
177
178def hex_to_rgba(string):
179    """Return a :class:`Gdk.RGBA` for hexadecimal `string`."""
180    rgba = Gdk.RGBA()
181    success = rgba.parse(string)
182    if not success:
183        raise ValueError("Parsing string {} failed".format(repr(string)))
184    return rgba
185
186def idle_add(function, *args, **kwargs):
187    """Call `function` with `args` and `kwargs` when idle."""
188    def call_function(*args, **kwargs):
189        function(*args, **kwargs)
190        return False # to not be called again.
191    return GLib.idle_add(call_function, *args, **kwargs)
192
193def install_module(name, obj):
194    """
195    Install `obj`'s module into the :mod:`gaupol` namespace.
196
197    Typical call is of form::
198
199        gaupol.util.install_module("foo", lambda: None)
200    """
201    gaupol.__dict__[name] = inspect.getmodule(obj)
202
203def iterate_main():
204    """Iterate the GTK+ main loop while events are pending."""
205    while Gtk.events_pending():
206        Gtk.main_iteration()
207
208def lines_to_px(nlines, font=None):
209    """Convert lines to pixels."""
210    if nlines < 0: return nlines
211    text = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
212    label = Gtk.Label(label=text)
213    gaupol.style.use_font(label, font)
214    label.show()
215    height = label.get_preferred_height()[1]
216    return int(round(nlines * height))
217
218def new_hbox(spacing):
219    """Return a new horizontal :class:`Gtk.Box`."""
220    return Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL,
221                   spacing=spacing)
222
223def new_vbox(spacing):
224    """Return a new vertical :class:`Gtk.Box`."""
225    return Gtk.Box(orientation=Gtk.Orientation.VERTICAL,
226                   spacing=spacing)
227
228def pack_start(box, widget, padding=0):
229    """Pack widget to box without fill or expand."""
230    box.pack_start(widget,
231                   expand=False,
232                   fill=False,
233                   padding=padding)
234
235def pack_start_expand(box, widget, padding=0):
236    """Pack widget to box with fill and expand."""
237    box.pack_start(widget,
238                   expand=True,
239                   fill=True,
240                   padding=padding)
241
242def pack_start_fill(box, widget, padding=0):
243    """Pack widget to box with fill, but no expand."""
244    box.pack_start(widget,
245                   expand=False,
246                   fill=True,
247                   padding=padding)
248
249def prepare_text_view(text_view):
250    """Set spell-check, line-length margin and font properties."""
251    if (gaupol.util.gtkspell_available() and
252        gaupol.conf.spell_check.inline):
253        from gi.repository import GtkSpell
254        language = gaupol.conf.spell_check.language
255        with aeidon.util.silent(Exception):
256            checker = GtkSpell.Checker()
257            checker.set_language(language)
258            def on_language_changed(checker, lang, *args):
259                gaupol.conf.spell_check.language = lang
260            checker.connect("language-changed", on_language_changed)
261            checker.attach(text_view)
262    connect = gaupol.conf.editor.connect
263    def update_margin(section, value, text_view):
264        if gaupol.conf.editor.show_lengths_edit:
265            return gaupol.ruler.connect_text_view(text_view)
266        return gaupol.ruler.disconnect_text_view(text_view)
267    connect("notify::show_lengths_edit", update_margin, text_view)
268    update_margin(None, None, text_view)
269    def update_font(section, value, text_view):
270        text_view.reset_style()
271    gaupol.style.use_font(text_view, "custom")
272    connect("notify::use_custom_font", update_font, text_view)
273    connect("notify::custom_font", update_font, text_view)
274    update_font(None, None, text_view)
275    def update_spacing(section, value, text_view):
276        if gaupol.conf.editor.show_lengths_cell:
277            return text_view.set_pixels_above_lines(2)
278        return text_view.set_pixels_above_lines(0)
279    connect("notify::show_lengths_cell", update_spacing, text_view)
280    update_spacing(None, None, text_view)
281
282def raise_default(expression):
283    """Raise :exc:`gaupol.Default` if `expression` evaluates to ``True``."""
284    if expression:
285        raise gaupol.Default
286
287def rgba_to_hex(color):
288    """Return hexadecimal string for :class:`Gdk.RGBA` `color`."""
289    return "#{:02x}{:02x}{:02x}".format(int(color.red   * 255),
290                                        int(color.green * 255),
291                                        int(color.blue  * 255))
292
293def run_dialog(dialog):
294    """Run `dialog` and return response."""
295    return dialog.run()
296
297def scale_to_content(widget, min_nchar=0,  max_nchar=32768,
298                     min_nlines=0, max_nlines=32768, font=None):
299    """Set `widget's` size by content, but limited by `min` and `max`."""
300    width, height = get_content_size(widget, font)
301    width  = max(width, char_to_px(min_nchar, font))
302    width  = min(width, char_to_px(max_nchar, font))
303    height = max(height, lines_to_px(min_nlines, font))
304    height = min(height, lines_to_px(max_nlines, font))
305    parent = widget.get_parent()
306    if isinstance(parent, Gtk.ScrolledWindow):
307        # Vaguely account for possible scrollbars.
308        return parent.set_size_request(width+24, height+24)
309    widget.set_size_request(width, height)
310
311def scale_to_size(widget, nchar, nlines, font=None):
312    """Set `widget`'s size to `nchar` and `nlines`."""
313    width  = char_to_px(nchar, font)
314    height = lines_to_px(nlines, font)
315    parent = widget.get_parent()
316    if isinstance(parent, Gtk.ScrolledWindow):
317        # Vaguely account for possible scrollbars.
318        return parent.set_size_request(width+24, height+24)
319    widget.set_size_request(width, height)
320
321def separate_combo(store, itr, data=None):
322    """Separator function for combo box models."""
323    return store.get_value(itr, 0) == gaupol.COMBO_SEPARATOR
324
325def set_cursor_busy(window):
326    """Set mouse pointer busy when above window."""
327    cursor = window.get_window().get_cursor()
328    if (cursor is not None and cursor.get_cursor_type() ==
329        Gdk.CursorType.WATCH): return
330    cursor = Gdk.Cursor.new_for_display(
331        Gdk.Display.get_default(), Gdk.CursorType.WATCH)
332    window.get_window().set_cursor(cursor)
333    iterate_main()
334
335def set_cursor_normal(window):
336    """Set mouse pointer normal when above window."""
337    cursor = window.get_window().get_cursor()
338    if (cursor is not None and cursor.get_cursor_type() ==
339        Gdk.CursorType.LEFT_PTR): return
340    cursor = Gdk.Cursor.new_for_display(
341        Gdk.Display.get_default(), Gdk.CursorType.LEFT_PTR)
342    window.get_window().set_cursor(cursor)
343    iterate_main()
344
345def show_exception(exctype, value, tb):
346    """A :class:`gaupol.DebugDialog` :attr`sys.excepthook`."""
347    traceback.print_exception(exctype, value, tb)
348    if not isinstance(value, Exception): return
349    try: # to avoid recursion.
350        dialog = gaupol.DebugDialog()
351        dialog.set_text(exctype, value, tb)
352        response = dialog.run()
353        dialog.destroy()
354        if response == Gtk.ResponseType.NO:
355            raise SystemExit(1)
356    except Exception:
357        traceback.print_exc()
358
359def show_uri(uri):
360    """Open `uri` in default application."""
361    try:
362        return Gtk.show_uri(None, uri, Gdk.CURRENT_TIME)
363    except Exception:
364        # Gtk.show_uri fails on Windows and some misconfigured installations.
365        # GError: No application is registered as handling this file
366        # Gtk.show_uri: Operation not supported
367        if uri.startswith(("http://", "https://")):
368            return webbrowser.open(uri)
369        raise # Exception
370
371def text_field_to_document(field):
372    """Return :attr:`aeidon.documents` item corresponding to `field`."""
373    if field == gaupol.fields.MAIN_TEXT:
374        return aeidon.documents.MAIN
375    if field == gaupol.fields.TRAN_TEXT:
376        return aeidon.documents.TRAN
377    raise ValueError("Invalid field: {}"
378                     .format(repr(field)))
379
380def tree_path_to_row(path):
381    """
382    Convert `path` to a list row integer.
383
384    `path` can be either a :class:`Gtk.Treepath` instance or a string
385    representation of it (as commonly used by various callbacks).
386    """
387    if path is None: return None
388    if isinstance(path, Gtk.TreePath):
389        return path.get_indices()[0]
390    if isinstance(path, str):
391        return int(path)
392    raise TypeError("Bad type {} for path {}"
393                    .format(repr(type(path)), repr(path)))
394
395def tree_row_to_path(row):
396    """Convert list row integer to a :class:`Gtk.TreePath`."""
397    if row is None: return None
398    return Gtk.TreePath.new_from_string(str(row))
399