1# Copyright 2005 Joe Wreschnig, Michael Urman 2# 2012 Christoph Reiter 3# 2016-17 Nick Boultbee 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 2 of the License, or 8# (at your option) any later version. 9 10import os 11import signal 12import socket 13from urllib.parse import urlparse 14 15import gi 16gi.require_version("Gtk", "3.0") 17 18from gi.repository import Gtk 19from gi.repository import Gdk 20from gi.repository import GLib, GObject, PangoCairo 21from senf import fsn2bytes, bytes2fsn, uri2fsn 22 23from quodlibet.util import print_d, print_w, is_windows, is_osx 24 25 26def show_uri(label, uri): 27 """Shows a uri. The uri can be anything handled by GIO or a quodlibet 28 specific one. 29 30 Currently handled quodlibet uris: 31 - quodlibet:///prefs/plugins/<plugin id> 32 33 Args: 34 label (str) 35 uri (str) the uri to show 36 Returns: 37 True on success, False on error 38 """ 39 40 parsed = urlparse(uri) 41 if parsed.scheme == "quodlibet": 42 if parsed.netloc != "": 43 print_w("Unknown QuodLibet URL format (%s)" % uri) 44 return False 45 else: 46 return __show_quodlibet_uri(parsed) 47 elif parsed.scheme == "file" and (is_windows() or is_osx()): 48 # Gio on non-Linux can't handle file URIs for some reason, 49 # fall back to our own implementation for now 50 from quodlibet.qltk.showfiles import show_files 51 52 try: 53 filepath = uri2fsn(uri) 54 except ValueError: 55 return False 56 else: 57 return show_files(filepath, []) 58 else: 59 # Gtk.show_uri_on_window exists since 3.22 60 try: 61 if hasattr(Gtk, "show_uri_on_window"): 62 from quodlibet.qltk import get_top_parent 63 return Gtk.show_uri_on_window(get_top_parent(label), uri, 0) 64 else: 65 return Gtk.show_uri(None, uri, 0) 66 except GLib.Error: 67 return False 68 69 70def __show_quodlibet_uri(uri): 71 if uri.path.startswith("/prefs/plugins/"): 72 from .pluginwin import PluginWindow 73 print_d("Showing plugin prefs resulting from URI (%s)" % (uri, )) 74 return PluginWindow().move_to(uri.path[len("/prefs/plugins/"):]) 75 else: 76 return False 77 78 79def get_fg_highlight_color(widget): 80 """Returns a color useable for highlighting things on top of the standard 81 background color. 82 83 Args: 84 widget (Gtk.Widget) 85 Returns: 86 Gdk.RGBA 87 """ 88 89 context = widget.get_style_context() 90 if hasattr(Gtk.StateFlags, "LINK"): 91 # gtk+ >=3.12 92 context.save() 93 context.set_state(Gtk.StateFlags.LINK) 94 color = context.get_color(context.get_state()) 95 context.restore() 96 else: 97 value = GObject.Value() 98 value.init(Gdk.Color) 99 value.set_boxed(None) 100 context.get_style_property("link-color", value) 101 color = Gdk.RGBA() 102 old_color = value.get_boxed() 103 if old_color is not None: 104 color.parse(old_color.to_string()) 105 return color 106 107 108def get_primary_accel_mod(): 109 """Returns the primary Gdk.ModifierType modifier. 110 111 cmd on osx, ctrl everywhere else. 112 """ 113 114 return Gtk.accelerator_parse("<Primary>")[1] 115 116 117def redraw_all_toplevels(): 118 """A hack to trigger redraws for all windows and widgets.""" 119 120 for widget in Gtk.Window.list_toplevels(): 121 if not widget.get_realized(): 122 continue 123 if widget.is_active(): 124 widget.queue_draw() 125 continue 126 sensitive = widget.get_sensitive() 127 widget.set_sensitive(not sensitive) 128 widget.set_sensitive(sensitive) 129 130 131def selection_set_songs(selection_data, songs): 132 """Stores filenames of the passed songs in a Gtk.SelectionData""" 133 134 filenames = [] 135 for filename in (song["~filename"] for song in songs): 136 filenames.append(fsn2bytes(filename, "utf-8")) 137 type_ = Gdk.atom_intern("text/x-quodlibet-songs", True) 138 selection_data.set(type_, 8, b"\x00".join(filenames)) 139 140 141def selection_get_filenames(selection_data): 142 """Extracts the filenames of songs set with selection_set_songs() 143 from a Gtk.SelectionData. 144 """ 145 146 data_type = selection_data.get_data_type() 147 assert data_type.name() == "text/x-quodlibet-songs" 148 149 items = selection_data.get_data().split(b"\x00") 150 return [bytes2fsn(i, "utf-8") for i in items] 151 152 153def get_top_parent(widget): 154 """Return the ultimate parent of a widget; the assumption that code 155 using this makes is that it will be a Gtk.Window, i.e. the widget 156 is fully packed when this is called.""" 157 158 parent = widget and widget.get_toplevel() 159 if parent and parent.is_toplevel(): 160 return parent 161 else: 162 return None 163 164 165def get_menu_item_top_parent(widget): 166 """Returns the toplevel for a menu item or None if the menu 167 and none of its parents isn't attached to a widget 168 """ 169 170 while isinstance(widget, Gtk.MenuItem): 171 menu = widget.get_parent() 172 if not menu: 173 return 174 widget = menu.get_attach_widget() 175 return get_top_parent(widget) 176 177 178def find_widgets(widget, type_): 179 """Given a widget, find all children that are a subclass of type_ 180 (including itself) 181 182 Args: 183 widget (Gtk.Widget) 184 type_ (type) 185 Returns: 186 List[Gtk.Widget] 187 """ 188 189 found = [] 190 191 if isinstance(widget, type_): 192 found.append(widget) 193 194 if isinstance(widget, Gtk.Container): 195 for child in widget.get_children(): 196 found.extend(find_widgets(child, type_)) 197 198 return found 199 200 201def menu_popup(menu, shell, item, func, *args): 202 """Wrapper to fix API break: 203 https://git.gnome.org/browse/gtk+/commit/?id=8463d0ee62b4b22fa 204 """ 205 206 if func is not None: 207 def wrap_pos_func(menu, *args): 208 return func(menu, args[-1]) 209 else: 210 wrap_pos_func = None 211 212 return menu.popup(shell, item, wrap_pos_func, *args) 213 214 215def _popup_menu_at_widget(menu, widget, button, time, under): 216 217 def pos_func(menu, data, widget=widget): 218 screen = widget.get_screen() 219 ref = get_top_parent(widget) 220 menu.set_screen(screen) 221 x, y = widget.translate_coordinates(ref, 0, 0) 222 dx, dy = ref.get_window().get_origin()[1:] 223 wa = widget.get_allocation() 224 225 # fit menu to screen, aligned per text direction 226 screen_width = screen.get_width() 227 screen_height = screen.get_height() 228 menu.realize() 229 ma = menu.get_allocation() 230 231 menu_y_under = y + dy + wa.height 232 menu_y_above = y + dy - ma.height 233 if under: 234 menu_y = menu_y_under 235 if menu_y + ma.height > screen_height and menu_y_above > 0: 236 menu_y = menu_y_above 237 else: 238 menu_y = menu_y_above 239 if menu_y < 0 and menu_y_under + ma.height < screen_height: 240 menu_y = menu_y_under 241 242 if Gtk.Widget.get_default_direction() == Gtk.TextDirection.LTR: 243 menu_x = min(x + dx, screen_width - ma.width) 244 else: 245 menu_x = max(0, x + dx - ma.width + wa.width) 246 247 return (menu_x, menu_y, True) # x, y, move_within_screen 248 menu_popup(menu, None, None, pos_func, None, button, time) 249 250 251def _ensure_menu_attached(menu, widget): 252 assert widget is not None 253 254 # Workaround the menu inheriting the wrong colors with the Ubuntu 12.04 255 # default themes. Attaching to the parent kinda works... submenus still 256 # have the wrong color. 257 if isinstance(widget, Gtk.Button): 258 widget = widget.get_parent() or widget 259 260 attached_widget = menu.get_attach_widget() 261 if attached_widget is widget: 262 return 263 if attached_widget is not None: 264 menu.detach() 265 menu.attach_to_widget(widget, None) 266 267 268def popup_menu_under_widget(menu, widget, button, time): 269 _ensure_menu_attached(menu, widget) 270 _popup_menu_at_widget(menu, widget, button, time, True) 271 272 273def popup_menu_above_widget(menu, widget, button, time): 274 _ensure_menu_attached(menu, widget) 275 _popup_menu_at_widget(menu, widget, button, time, False) 276 277 278def popup_menu_at_widget(menu, widget, button, time): 279 _ensure_menu_attached(menu, widget) 280 menu_popup(menu, None, None, None, None, button, time) 281 282 283def add_fake_accel(widget, accel): 284 """Accelerators are only for window menus and global keyboard shortcuts. 285 286 Since we want to use them in context menus as well, to indicate which 287 key events the parent widget knows about, we use a global fake 288 accelgroup without any actions.. 289 """ 290 291 if not hasattr(add_fake_accel, "_group"): 292 add_fake_accel._group = Gtk.AccelGroup() 293 group = add_fake_accel._group 294 295 key, val = Gtk.accelerator_parse(accel) 296 assert key is not None 297 assert val is not None 298 widget.add_accelerator( 299 'activate', group, key, val, Gtk.AccelFlags.VISIBLE) 300 301 302def is_accel(event, *accels): 303 """Checks if the given keypress Gdk.Event matches 304 any of accelerator strings. 305 306 example: is_accel(event, "<shift><ctrl>z") 307 308 Args: 309 *accels: one ore more `str` 310 Returns: 311 bool 312 Raises: 313 ValueError: in case any of the accels could not be parsed 314 """ 315 316 assert accels 317 318 if event.type != Gdk.EventType.KEY_PRESS: 319 return False 320 321 # ctrl+shift+x gives us ctrl+shift+X and accelerator_parse returns 322 # lowercase values for matching, so lowercase it if possible 323 keyval = event.keyval 324 if not keyval & ~0xFF: 325 keyval = ord(chr(keyval).lower()) 326 327 default_mod = Gtk.accelerator_get_default_mod_mask() 328 keymap = Gdk.Keymap.get_default() 329 330 for accel in accels: 331 accel_keyval, accel_mod = Gtk.accelerator_parse(accel) 332 if accel_keyval == 0 and accel_mod == 0: 333 raise ValueError("Invalid accel: %s" % accel) 334 335 # If the accel contains non default modifiers matching will 336 # never work and since no one should use them, complain 337 non_default = accel_mod & ~default_mod 338 if non_default: 339 print_w("Accelerator '%s' contains a non default modifier '%s'." % 340 (accel, Gtk.accelerator_name(0, non_default) or "")) 341 342 # event.state contains the real mod mask + the virtual one, while 343 # we usually pass only virtual one as text. This adds the real one 344 # so they match in the end. 345 accel_mod = keymap.map_virtual_modifiers(accel_mod)[1] 346 347 # Remove everything except default modifiers and compare 348 if (accel_keyval, accel_mod) == (keyval, event.state & default_mod): 349 return True 350 351 return False 352 353 354def add_css(widget, css): 355 """Add css for the widget, overriding the theme. 356 357 Can raise GLib.GError in case the css is invalid 358 """ 359 360 if not isinstance(css, bytes): 361 css = css.encode("utf-8") 362 363 provider = Gtk.CssProvider() 364 provider.load_from_data(css) 365 context = widget.get_style_context() 366 context.add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 367 368 369def remove_padding(widget): 370 """Removes padding on supplied widget""" 371 return add_css(widget, " * { padding: 0px; } ") 372 373 374def is_instance_of_gtype_name(instance, name): 375 """Returns False if the gtype can't be found""" 376 377 try: 378 gtype = GObject.type_from_name(name) 379 except Exception: 380 return False 381 else: 382 pytype = gtype.pytype 383 if pytype is None: 384 return False 385 return isinstance(instance, pytype) 386 387 388def is_wayland(): 389 display = Gdk.Display.get_default() 390 if display is None: 391 return False 392 return is_instance_of_gtype_name(display, "GdkWaylandDisplay") 393 394 395def get_backend_name(): 396 """The GDK backend name""" 397 398 display = Gdk.Display.get_default() 399 if display is not None: 400 name = display.__gtype__.name 401 if name.startswith("Gdk"): 402 name = name[3:] 403 if name.endswith("Display"): 404 name = name[:-7] 405 return name 406 return u"Unknown" 407 408 409def get_font_backend_name() -> str: 410 """The PangoCairo font backend name""" 411 412 font_map = PangoCairo.FontMap.get_default() 413 name = font_map.__gtype__.name.lower() 414 name = name.split("pangocairo")[-1].split("fontmap")[0] 415 if name == "fc": 416 name = "fontconfig" 417 return name 418 419 420gtk_version = (Gtk.get_major_version(), Gtk.get_minor_version(), 421 Gtk.get_micro_version()) 422 423pygobject_version = gi.version_info 424 425 426def io_add_watch(fd, prio, condition, func, *args, **kwargs): 427 try: 428 # The new gir bindings don't fail with an invalid fd, 429 # and we can't do the same with the static ones (return a valid 430 # source ID..) so fail with newer pygobject as well. 431 if isinstance(fd, int) and fd < 0: 432 raise ValueError("invalid fd") 433 elif hasattr(fd, "fileno") and fd.fileno() < 0: 434 raise ValueError("invalid fd") 435 return GLib.io_add_watch(fd, prio, condition, func, *args, **kwargs) 436 except TypeError: 437 # older pygi 438 kwargs["priority"] = prio 439 return GLib.io_add_watch(fd, condition, func, *args, **kwargs) 440 441 442def add_signal_watch(signal_action, _sockets=[]): 443 """Catches signals which should exit the program and calls `signal_action` 444 after the main loop has started, even if the signal occurred before the 445 main loop has started. 446 """ 447 448 # See https://bugzilla.gnome.org/show_bug.cgi?id=622084 for details 449 450 sig_names = ["SIGINT", "SIGTERM", "SIGHUP"] 451 if os.name == "nt": 452 sig_names = ["SIGINT", "SIGTERM"] 453 454 signals = {} 455 for name in sig_names: 456 id_ = getattr(signal, name, None) 457 if id_ is None: 458 continue 459 signals[id_] = name 460 461 for signum, name in signals.items(): 462 # Before the mainloop starts we catch signals in python 463 # directly and idle_add the app.quit 464 def idle_handler(signum, frame): 465 print_d("Python signal handler activated: %s" % signals[signum]) 466 GLib.idle_add(signal_action, priority=GLib.PRIORITY_HIGH) 467 468 print_d("Register Python signal handler: %r" % name) 469 signal.signal(signum, idle_handler) 470 471 read_socket, write_socket = socket.socketpair() 472 for sock in [read_socket, write_socket]: 473 sock.setblocking(False) 474 # prevent it from being GCed and leak it 475 _sockets.append(sock) 476 477 def signal_notify(source, condition): 478 if condition & GLib.IOCondition.IN: 479 try: 480 return bool(read_socket.recv(1)) 481 except EnvironmentError: 482 return False 483 else: 484 return False 485 486 if os.name == "nt": 487 channel = GLib.IOChannel.win32_new_socket(read_socket.fileno()) 488 else: 489 channel = GLib.IOChannel.unix_new(read_socket.fileno()) 490 io_add_watch(channel, GLib.PRIORITY_HIGH, 491 (GLib.IOCondition.IN | GLib.IOCondition.HUP | 492 GLib.IOCondition.NVAL | GLib.IOCondition.ERR), 493 signal_notify) 494 495 signal.set_wakeup_fd(write_socket.fileno()) 496 497 498def enqueue(songs): 499 songs = [s for s in songs if s.can_add] 500 if songs: 501 from quodlibet import app 502 app.window.playlist.enqueue(songs) 503 504 505class ThemeOverrider(object): 506 """Allows registering global Gtk.StyleProviders for a specific theme. 507 They get activated when the theme gets active and removed when the theme 508 changes to something else. 509 """ 510 511 def __init__(self): 512 self._providers = {} 513 self._active_providers = [] 514 settings = Gtk.Settings.get_default() 515 settings.connect("notify::gtk-theme-name", self._on_theme_name_notify) 516 self._update_providers() 517 518 def register_provider(self, theme_name, provider): 519 """ 520 Args: 521 theme_name (str): A gtk+ theme name e.g. "Adwaita" or empty to 522 apply to all themes 523 provider (Gtk.StyleProvider) 524 """ 525 526 self._providers.setdefault(theme_name, []).append(provider) 527 self._update_providers() 528 529 def _update_providers(self): 530 settings = Gtk.Settings.get_default() 531 532 theme_name = settings.get_property("gtk-theme-name") 533 wanted_providers = \ 534 self._providers.get(theme_name, []) + self._providers.get("", []) 535 536 for provider in list(self._active_providers): 537 if provider not in wanted_providers: 538 Gtk.StyleContext.remove_provider_for_screen( 539 Gdk.Screen.get_default(), provider) 540 self._active_providers.remove(provider) 541 542 for provider in wanted_providers: 543 if provider not in self._active_providers: 544 Gtk.StyleContext.add_provider_for_screen( 545 Gdk.Screen.get_default(), 546 provider, 547 Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION 548 ) 549 self._active_providers.append(provider) 550 551 def _on_theme_name_notify(self, settings, gparam): 552 self._update_providers() 553 554 555from .msg import Message, ErrorMessage, WarningMessage 556from .x import Align, Button, ToggleButton, Notebook, SeparatorMenuItem, \ 557 WebImage, MenuItem, Frame, EntryCompletion 558from .icons import Icons 559from .window import Window, UniqueWindow, Dialog 560from .paned import ConfigRPaned, ConfigRHPaned 561 562Message, ErrorMessage, WarningMessage 563Align, Button, ToggleButton, Notebook, SeparatorMenuItem, \ 564 WebImage, MenuItem, Frame, EntryCompletion 565Icons 566Window, UniqueWindow, Dialog 567ConfigRPaned, ConfigRHPaned 568