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