1# Copyright 2012 Christoph Reiter
2#
3# This program is free software; you can redistribute it and/or modify
4# it under the terms of the GNU General Public License as published by
5# the Free Software Foundation; either version 2 of the License, or
6# (at your option) any later version.
7
8import os
9import sys
10import warnings
11import logging
12
13from senf import environ, argv, fsn2text
14
15from quodlibet.const import MinVersions
16from quodlibet import config
17from quodlibet.util import is_osx, is_windows, i18n
18from quodlibet.util.dprint import print_e, PrintHandler
19from quodlibet.util.urllib import install_urllib2_ca_file
20
21from ._main import get_base_dir, is_release, get_image_dir, get_cache_dir
22
23
24_cli_initialized = False
25_initialized = False
26
27
28def _init_gtk_debug(no_excepthook):
29    from quodlibet.errorreport import enable_errorhook
30
31    enable_errorhook(not no_excepthook)
32
33
34def is_init():
35    """Returns if init() was called"""
36
37    global _initialized
38
39    return _initialized
40
41
42def init(no_translations=False, no_excepthook=False, config_file=None):
43    """This needs to be called before any API can be used.
44    Might raise in case of an error.
45
46    Pass no_translations=True to disable translations (used by tests)
47    """
48
49    global _initialized
50
51    if _initialized:
52        return
53
54    init_cli(no_translations=no_translations, config_file=config_file)
55    _init_gtk()
56    _init_gtk_debug(no_excepthook=no_excepthook)
57    _init_gst()
58    _init_dbus()
59
60    _initialized = True
61
62
63def _init_gettext(no_translations=False):
64    """Call before using gettext helpers"""
65
66    if no_translations:
67        language = u"C"
68    else:
69        language = config.gettext("settings", "language")
70        if not language:
71            language = None
72
73    i18n.init(language)
74
75    # Use the locale dir in ../build/share/locale if there is one
76    localedir = os.path.join(
77        os.path.dirname(get_base_dir()), "build", "share", "locale")
78    if not os.path.isdir(localedir):
79        localedir = None
80
81    i18n.register_translation("quodlibet", localedir)
82    debug_text = environ.get("QUODLIBET_TEST_TRANS")
83    if debug_text is not None:
84        i18n.set_debug_text(fsn2text(debug_text))
85
86
87def _init_python():
88    MinVersions.PYTHON3.check(sys.version_info)
89
90    if is_osx():
91        # We build our own openssl on OSX and need to make sure that
92        # our own ca file is used in all cases as the non-system openssl
93        # doesn't use the system certs
94        install_urllib2_ca_file()
95
96    if is_windows():
97        # Not really needed on Windows as pygi-aio seems to work fine, but
98        # wine doesn't have certs which we use for testing.
99        install_urllib2_ca_file()
100
101    if is_windows() and os.sep != "\\":
102        # In the MSYS2 console MSYSTEM is set, which breaks os.sep/os.path.sep
103        # If you hit this do a "setup.py clean -all" to get rid of the
104        # bytecode cache then start things with "MSYSTEM= ..."
105        raise AssertionError("MSYSTEM is set (%r)" % environ.get("MSYSTEM"))
106
107    logging.getLogger().addHandler(PrintHandler())
108
109
110def _init_formats():
111    from quodlibet.formats import init
112    init()
113
114
115def init_cli(no_translations=False, config_file=None):
116    """This needs to be called before any API can be used.
117    Might raise in case of an error.
118
119    Like init() but for code not using Gtk etc.
120    """
121
122    global _cli_initialized
123
124    if _cli_initialized:
125        return
126
127    _init_python()
128    config.init_defaults()
129    if config_file is not None:
130        config.init(config_file)
131    _init_gettext(no_translations)
132    _init_formats()
133    _init_g()
134
135    _cli_initialized = True
136
137
138def _init_dbus():
139    """Setup dbus mainloop integration. Call before using dbus"""
140
141    # To make GDBus fail early and we don't have to wait for a timeout
142    if is_osx() or is_windows():
143        os.environ["DBUS_SYSTEM_BUS_ADDRESS"] = "something-invalid"
144        os.environ["DBUS_SESSION_BUS_ADDRESS"] = "something-invalid"
145
146    try:
147        from dbus.mainloop.glib import DBusGMainLoop, threads_init
148    except ImportError:
149        try:
150            import dbus.glib
151            dbus.glib
152        except ImportError:
153            return
154    else:
155        threads_init()
156        DBusGMainLoop(set_as_default=True)
157
158
159def _fix_gst_leaks():
160    """gst_element_add_pad and gst_bin_add are wrongly annotated and lead
161    to PyGObject refing the passed element.
162
163    Work around by adding a wrapper that unrefs afterwards.
164    Can be called multiple times.
165
166    https://bugzilla.gnome.org/show_bug.cgi?id=741390
167    https://bugzilla.gnome.org/show_bug.cgi?id=702960
168    """
169
170    from gi.repository import Gst
171
172    assert Gst.is_initialized()
173
174    def do_wrap(func):
175        def wrap(self, obj):
176            result = func(self, obj)
177            obj.unref()
178            return result
179        return wrap
180
181    parent = Gst.Bin()
182    elm = Gst.Bin()
183    parent.add(elm)
184    if elm.__grefcount__ == 3:
185        elm.unref()
186        Gst.Bin.add = do_wrap(Gst.Bin.add)
187
188    pad = Gst.Pad.new("foo", Gst.PadDirection.SRC)
189    parent.add_pad(pad)
190    if pad.__grefcount__ == 3:
191        pad.unref()
192        Gst.Element.add_pad = do_wrap(Gst.Element.add_pad)
193
194
195def _init_g():
196    """Call before using GdkPixbuf/GLib/Gio/GObject"""
197
198    import gi
199
200    gi.require_version("GLib", "2.0")
201    gi.require_version("Gio", "2.0")
202    gi.require_version("GObject", "2.0")
203    gi.require_version("GdkPixbuf", "2.0")
204
205    # Newer glib is noisy regarding deprecated signals/properties
206    # even with stable releases.
207    if is_release():
208        warnings.filterwarnings(
209            'ignore', '.* It will be removed in a future version.',
210            Warning)
211
212    # blacklist some modules, simply loading can cause segfaults
213    sys.modules["glib"] = None
214    sys.modules["gobject"] = None
215
216
217def _init_gtk():
218    """Call before using Gtk/Gdk"""
219
220    import gi
221
222    if config.getboolean("settings", "pangocairo_force_fontconfig") and \
223            "PANGOCAIRO_BACKEND" not in environ:
224        environ["PANGOCAIRO_BACKEND"] = "fontconfig"
225
226    # disable for consistency and trigger events seem a bit flaky here
227    if config.getboolean("settings", "scrollbar_always_visible"):
228        environ["GTK_OVERLAY_SCROLLING"] = "0"
229
230    try:
231        # not sure if this is available under Windows
232        gi.require_version("GdkX11", "3.0")
233        from gi.repository import GdkX11
234        GdkX11
235    except (ValueError, ImportError):
236        pass
237
238    gi.require_version("Gtk", "3.0")
239    gi.require_version("Gdk", "3.0")
240    gi.require_version("Pango", "1.0")
241    gi.require_version('Soup', '2.4')
242    gi.require_version('PangoCairo', "1.0")
243
244    from gi.repository import Gtk
245    from quodlibet.qltk import ThemeOverrider, gtk_version
246
247    # PyGObject doesn't fail anymore when init fails, so do it ourself
248    initialized, argv[:] = Gtk.init_check(argv)
249    if not initialized:
250        raise SystemExit("Gtk.init failed")
251
252    # include our own icon theme directory
253    theme = Gtk.IconTheme.get_default()
254    theme_search_path = get_image_dir()
255    assert os.path.exists(theme_search_path)
256    theme.append_search_path(theme_search_path)
257
258    # Force menu/button image related settings. We might show too many atm
259    # but this makes sure we don't miss cases where we forgot to force them
260    # per widget.
261    # https://bugzilla.gnome.org/show_bug.cgi?id=708676
262    warnings.filterwarnings('ignore', '.*g_value_get_int.*', Warning)
263
264    # some day... but not now
265    warnings.filterwarnings(
266        'ignore', '.*Stock items are deprecated.*', Warning)
267    warnings.filterwarnings(
268        'ignore', '.*:use-stock.*', Warning)
269    warnings.filterwarnings(
270        'ignore', r'.*The property GtkAlignment:[^\s]+ is deprecated.*',
271        Warning)
272
273    settings = Gtk.Settings.get_default()
274    with warnings.catch_warnings():
275        warnings.simplefilter("ignore")
276        settings.set_property("gtk-button-images", True)
277        settings.set_property("gtk-menu-images", True)
278    if hasattr(settings.props, "gtk_primary_button_warps_slider"):
279        # https://bugzilla.gnome.org/show_bug.cgi?id=737843
280        settings.set_property("gtk-primary-button-warps-slider", True)
281
282    # Make sure PyGObject includes support for foreign cairo structs
283    try:
284        gi.require_foreign("cairo")
285    except ImportError:
286        print_e("PyGObject is missing cairo support")
287        exit(1)
288
289    css_override = ThemeOverrider()
290
291    if sys.platform == "darwin":
292        # fix duplicated shadows for popups with Gtk+3.14
293        style_provider = Gtk.CssProvider()
294        style_provider.load_from_data(b"""
295            GtkWindow {
296                box-shadow: none;
297            }
298            .tooltip {
299                border-radius: 0;
300                padding: 0;
301            }
302            .tooltip.background {
303                background-clip: border-box;
304            }
305            """)
306        css_override.register_provider("", style_provider)
307
308    if gtk_version[:2] >= (3, 20):
309        # https://bugzilla.gnome.org/show_bug.cgi?id=761435
310        style_provider = Gtk.CssProvider()
311        style_provider.load_from_data(b"""
312            spinbutton, button {
313                min-height: 22px;
314            }
315
316            .view button {
317                min-height: 24px;
318            }
319
320            entry {
321                min-height: 28px;
322            }
323
324            entry.cell {
325                min-height: 0;
326            }
327        """)
328        css_override.register_provider("Adwaita", style_provider)
329        css_override.register_provider("HighContrast", style_provider)
330
331        # https://github.com/quodlibet/quodlibet/issues/2541
332        style_provider = Gtk.CssProvider()
333        style_provider.load_from_data(b"""
334            treeview.view.separator {
335                min-height: 2px;
336                color: @borders;
337            }
338        """)
339        css_override.register_provider("Ambiance", style_provider)
340        css_override.register_provider("Radiance", style_provider)
341        # https://github.com/quodlibet/quodlibet/issues/2677
342        css_override.register_provider("Clearlooks-Phenix", style_provider)
343        # https://github.com/quodlibet/quodlibet/issues/2997
344        css_override.register_provider("Breeze", style_provider)
345
346    if gtk_version[:2] >= (3, 18):
347        # Hack to get some grab handle like thing for panes
348        style_provider = Gtk.CssProvider()
349        style_provider.load_from_data(b"""
350            GtkPaned.vertical, paned.vertical >separator {
351                -gtk-icon-source: -gtk-icontheme("view-more-symbolic");
352                -gtk-icon-transform: rotate(90deg) scaleX(0.1) scaleY(3);
353            }
354
355            GtkPaned.horizontal, paned.horizontal >separator {
356                -gtk-icon-source: -gtk-icontheme("view-more-symbolic");
357                -gtk-icon-transform: rotate(0deg) scaleX(0.1) scaleY(3);
358            }
359        """)
360        css_override.register_provider("", style_provider)
361
362    # https://bugzilla.gnome.org/show_bug.cgi?id=708676
363    warnings.filterwarnings('ignore', '.*g_value_get_int.*', Warning)
364
365    # blacklist some modules, simply loading can cause segfaults
366    sys.modules["gtk"] = None
367    sys.modules["gpod"] = None
368    sys.modules["gnome"] = None
369
370    from quodlibet.qltk import pygobject_version, gtk_version
371
372    MinVersions.GTK.check(gtk_version)
373    MinVersions.PYGOBJECT.check(pygobject_version)
374
375
376def _init_gst():
377    """Call once before importing GStreamer"""
378
379    arch_key = "64" if sys.maxsize > 2**32 else "32"
380    registry_name = "gst-registry-%s-%s.bin" % (sys.platform, arch_key)
381    environ["GST_REGISTRY"] = os.path.join(get_cache_dir(), registry_name)
382
383    assert "gi.repository.Gst" not in sys.modules
384
385    import gi
386
387    # We don't want python-gst, it changes API..
388    assert "gi.overrides.Gst" not in sys.modules
389    sys.modules["gi.overrides.Gst"] = None
390
391    # blacklist some modules, simply loading can cause segfaults
392    sys.modules["gst"] = None
393
394    # We don't depend on Gst overrides, so make sure it's initialized.
395    try:
396        gi.require_version("Gst", "1.0")
397        from gi.repository import Gst
398    except (ValueError, ImportError):
399        return
400
401    if Gst.is_initialized():
402        return
403
404    from gi.repository import GLib
405
406    try:
407        ok, argv[:] = Gst.init_check(argv)
408    except GLib.GError:
409        print_e("Failed to initialize GStreamer")
410        # Uninited Gst segfaults: make sure no one can use it
411        sys.modules["gi.repository.Gst"] = None
412    else:
413        # monkey patching ahead
414        _fix_gst_leaks()
415