mcg/mcg/widgets.py
coderkun 202bfb424c Use “gettext” instead of “locale” for L10N
As the documentation on the “locale” module states, the “gettext” module
should be used instead. Therefore adjust the localization calls to use
gettext instead.

Additionally fix one button label which did not use localization at all
and update the message catalogues.
2019-02-17 00:03:57 +01:00

2086 lines
76 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
import gi
gi.require_version('Gtk', '3.0')
try:
import keyring
use_keyring = True
except:
use_keyring = False
import gettext
import logging
import math
import sys
import threading
from gi.repository import Gtk, Gdk, GObject, GdkPixbuf, GLib, Gio
from mcg import client
from mcg.utils import SortOrder
from mcg.utils import Utils
from mcg.zeroconf import ZeroconfProvider
class ShortcutsDialog():
def __init__(self, builder, window):
# Widgets
self._window = builder.get_object('shortcuts-dialog')
self._window.set_transient_for(window.get())
def get(self):
return self._window
def present(self):
self._window.present()
class InfoDialog():
def __init__(self, builder):
self._logger = logging.getLogger(__name__)
# Widgets
self._info_dialog = builder.get_object('info-dialog')
self._resize_logo()
def get(self):
return self._info_dialog
def run(self):
self._info_dialog.run()
self._info_dialog.hide()
def _resize_logo(self):
try:
logo_pixbuf = self._info_dialog.get_logo()
self._info_dialog.set_logo(
logo_pixbuf.scale_simple(256, 256, GdkPixbuf.InterpType.HYPER)
)
except:
self._logger.warn("Failed to resize logo")
class WindowState(GObject.Object):
WIDTH = 'width'
HEIGHT = 'height'
IS_MAXIMIZED = 'is_maximized'
IS_FULLSCREENED = 'is_fullscreened'
width = GObject.Property(type=int, default=800)
height = GObject.Property(type=int, default=600)
is_maximized = GObject.Property(type=bool, default=False)
is_fullscreened = GObject.Property(type=bool, default=False)
def __init__(self):
GObject.Object.__init__(self)
class Window():
SETTING_HOST = 'host'
SETTING_PORT = 'port'
SETTING_CONNECTED = 'connected'
SETTING_IMAGE_DIR = 'image-dir'
SETTING_WINDOW_WIDTH = 'width'
SETTING_WINDOW_HEIGHT = 'height'
SETTING_WINDOW_MAXIMIZED = 'is-maximized'
SETTING_PANEL = 'panel'
SETTING_ITEM_SIZE = 'item-size'
SETTING_SORT_ORDER = 'sort-order'
SETTING_SORT_TYPE = 'sort-type'
STOCK_ICON_DEFAULT = 'image-x-generic-symbolic'
_PANEL_INDEX_SERVER = 0
_PANEL_INDEX_COVER = 1
_PANEL_INDEX_PLAYLIST = 2
_PANEL_INDEX_LIBRARY = 3
_CSS_SELECTION = 'selection'
_CUSTOM_STARTUP_COMPLETE = 'startup-complete'
def __init__(self, app, builder, title, settings):
self._appwindow = builder.get_object('appwindow')
self._appwindow.set_application(app)
self._appwindow.set_title(title)
self._settings = settings
self._panels = []
self._mcg = client.Client()
self._logger = logging.getLogger(__name__)
self._state = WindowState()
# Login screen
self._connection_panel = ConnectionPanel(builder)
# Panels
self._panels.append(ServerPanel(builder))
self._panels.append(CoverPanel(builder))
self._panels.append(PlaylistPanel(builder))
self._panels.append(LibraryPanel(builder))
# Widgets
# InfoBar
self._infobar = InfoBar(builder)
# Stack
self._content_stack = builder.get_object('contentstack')
self._stack = builder.get_object('panelstack')
# Header
self._header_bar = HeaderBar(builder)
# Toolbar stack
self._toolbar_stack = builder.get_object('toolbarstack')
# Properties
self._header_bar.set_sensitive(False, False)
self._connection_panel.set_host(self._settings.get_string(Window.SETTING_HOST))
self._connection_panel.set_port(self._settings.get_int(Window.SETTING_PORT))
if use_keyring:
self._connection_panel.set_password(keyring.get_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME))
self._connection_panel.set_image_dir(self._settings.get_string(Window.SETTING_IMAGE_DIR))
self._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(self._settings.get_int(Window.SETTING_ITEM_SIZE))
self._panels[Window._PANEL_INDEX_LIBRARY].set_item_size(self._settings.get_int(Window.SETTING_ITEM_SIZE))
self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_order(self._settings.get_enum(Window.SETTING_SORT_ORDER))
self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_type(self._settings.get_boolean(Window.SETTING_SORT_TYPE))
# Signals
self._header_bar.connect('stack-switched', self.on_header_bar_stack_switched)
self._header_bar.connect('toolbar-connect', self.on_header_bar_connect)
self._header_bar.connect('toolbar-playpause', self.on_header_bar_playpause)
self._header_bar.connect('toolbar-set-volume', self.on_header_bar_set_volume)
self._connection_panel.connect('connection-changed', self.on_connection_panel_connection_changed)
self._panels[Window._PANEL_INDEX_SERVER].connect('change-output-device', self.on_server_panel_output_device_changed)
self._panels[Window._PANEL_INDEX_COVER].connect('toggle-fullscreen', self.on_cover_panel_toggle_fullscreen)
self._panels[Window._PANEL_INDEX_COVER].connect('set-song', self.on_cover_panel_set_song)
self._panels[Window._PANEL_INDEX_PLAYLIST].connect('clear-playlist', self.on_playlist_panel_clear_playlist)
self._panels[Window._PANEL_INDEX_PLAYLIST].connect('remove', self.on_playlist_panel_remove)
self._panels[Window._PANEL_INDEX_PLAYLIST].connect('remove-multiple', self.on_playlist_panel_remove_multiple)
self._panels[Window._PANEL_INDEX_PLAYLIST].connect('play', self.on_playlist_panel_play)
self._panels[Window._PANEL_INDEX_LIBRARY].connect('update', self.on_library_panel_update)
self._panels[Window._PANEL_INDEX_LIBRARY].connect('play', self.on_library_panel_play)
self._panels[Window._PANEL_INDEX_LIBRARY].connect('queue', self.on_library_panel_queue)
self._panels[Window._PANEL_INDEX_LIBRARY].connect('queue-multiple', self.on_library_panel_queue_multiple)
self._panels[Window._PANEL_INDEX_LIBRARY].connect('item-size-changed', self.on_library_panel_item_size_changed)
self._panels[Window._PANEL_INDEX_LIBRARY].connect('sort-order-changed', self.on_library_panel_sort_order_changed)
self._panels[Window._PANEL_INDEX_LIBRARY].connect('sort-type-changed', self.on_library_panel_sort_type_changed)
self._mcg.connect_signal(client.Client.SIGNAL_CONNECTION, self.on_mcg_connect)
self._mcg.connect_signal(client.Client.SIGNAL_STATUS, self.on_mcg_status)
self._mcg.connect_signal(client.Client.SIGNAL_STATS, self.on_mcg_stats)
self._mcg.connect_signal(client.Client.SIGNAL_LOAD_OUTPUT_DEVICES, self.on_mcg_load_output_devices)
self._mcg.connect_signal(client.Client.SIGNAL_LOAD_PLAYLIST, self.on_mcg_load_playlist)
self._mcg.connect_signal(client.Client.SIGNAL_LOAD_ALBUMS, self.on_mcg_load_albums)
self._mcg.connect_signal(client.Client.SIGNAL_CUSTOM, self.on_mcg_custom)
self._mcg.connect_signal(client.Client.SIGNAL_ERROR, self.on_mcg_error)
self._settings.connect('changed::'+Window.SETTING_PANEL, self.on_settings_panel_changed)
self._settings.connect('changed::'+Window.SETTING_ITEM_SIZE, self.on_settings_item_size_changed)
self._settings.connect('changed::'+Window.SETTING_SORT_ORDER, self.on_settings_sort_order_changed)
self._settings.connect('changed::'+Window.SETTING_SORT_TYPE, self.on_settings_sort_type_changed)
self._settings.bind(Window.SETTING_WINDOW_WIDTH, self._state, WindowState.WIDTH, Gio.SettingsBindFlags.DEFAULT)
self._settings.bind(Window.SETTING_WINDOW_HEIGHT, self._state, WindowState.HEIGHT, Gio.SettingsBindFlags.DEFAULT)
self._settings.bind(Window.SETTING_WINDOW_MAXIMIZED, self._state, WindowState.IS_MAXIMIZED, Gio.SettingsBindFlags.DEFAULT)
handlers = {
'on_appwindow_size_allocate': self.on_resize,
'on_appwindow_window_state_event': self.on_state
}
handlers.update(self._header_bar.get_signal_handlers())
handlers.update(self._infobar.get_signal_handlers())
handlers.update(self._connection_panel.get_signal_handlers())
handlers.update(self._panels[Window._PANEL_INDEX_COVER].get_signal_handlers())
handlers.update(self._panels[Window._PANEL_INDEX_PLAYLIST].get_signal_handlers())
handlers.update(self._panels[Window._PANEL_INDEX_LIBRARY].get_signal_handlers())
builder.connect_signals(handlers)
# Actions
self._appwindow.set_default_size(self._state.width, self._state.height)
if self._state.get_property(WindowState.IS_MAXIMIZED):
self._appwindow.maximize()
self._appwindow.show_all()
self._content_stack.set_visible_child(self._connection_panel.get())
if self._settings.get_boolean(Window.SETTING_CONNECTED):
self._connect()
# Menu actions
self._connect_action = Gio.SimpleAction.new_stateful("connect", None, GLib.Variant.new_boolean(False))
self._connect_action.connect('change-state', self.on_menu_connect)
self._appwindow.add_action(self._connect_action)
self._play_action = Gio.SimpleAction.new_stateful("play", None, GLib.Variant.new_boolean(False))
self._play_action.set_enabled(False)
self._play_action.connect('change-state', self.on_menu_play)
self._appwindow.add_action(self._play_action)
self._clear_playlist_action = Gio.SimpleAction.new("clear-playlist", None)
self._clear_playlist_action.set_enabled(False)
self._clear_playlist_action.connect('activate', self.on_menu_clear_playlist)
self._appwindow.add_action(self._clear_playlist_action)
panel_variant = GLib.Variant.new_string("0")
self._panel_action = Gio.SimpleAction.new_stateful("panel", panel_variant.get_type(), panel_variant)
self._panel_action.set_enabled(False)
self._panel_action.connect('change-state', self.on_menu_panel)
self._appwindow.add_action(self._panel_action)
def get(self):
return self._appwindow
def present(self):
self._appwindow.present()
def on_resize(self, widget, event):
if not self._state.get_property(WindowState.IS_MAXIMIZED):
size = self._appwindow.get_size()
self._state.set_property(WindowState.WIDTH, size.width)
self._state.set_property(WindowState.HEIGHT, size.height)
def on_state(self, widget, state):
self._state.set_property(WindowState.IS_MAXIMIZED, (state.new_window_state & Gdk.WindowState.MAXIMIZED > 0))
self._fullscreen((state.new_window_state & Gdk.WindowState.FULLSCREEN > 0))
def on_menu_connect(self, action, value):
self._connect()
def on_menu_play(self, action, value):
self._mcg.playpause()
def on_menu_clear_playlist(self, action, value):
self._mcg.clear_playlist()
def on_menu_panel(self, action, value):
action.set_state(value)
self._stack.set_visible_child(self._panels[int(value.get_string())].get())
# HeaderBar callbacks
def on_header_bar_stack_switched(self, widget):
self._set_visible_toolbar()
self._save_visible_panel()
self._set_menu_visible_panel()
for panel in self._panels:
panel.set_selected(panel.get() == self._stack.get_visible_child())
GObject.idle_add(
self._stack.child_set_property,
self._stack.get_visible_child(),
'needs-attention',
False
)
def on_header_bar_connect(self, widget):
self._connect()
def on_header_bar_playpause(self, widget):
self._mcg.playpause()
self._mcg.get_status()
def on_header_bar_set_volume(self, widget, volume):
self._mcg.set_volume(volume)
# Panel callbacks
def on_connection_panel_connection_changed(self, widget, host, port, password, image_dir):
self._settings.set_string(Window.SETTING_HOST, host)
self._settings.set_int(Window.SETTING_PORT, port)
if use_keyring:
if password:
keyring.set_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME, password)
else:
if keyring.get_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME):
keyring.delete_password(ZeroconfProvider.KEYRING_SYSTEM, ZeroconfProvider.KEYRING_USERNAME)
self._settings.set_string(Window.SETTING_IMAGE_DIR, image_dir)
def on_playlist_panel_clear_playlist(self, widget):
self._mcg.clear_playlist()
def on_playlist_panel_remove(self, widget, album):
self._mcg.remove_album_from_playlist(album)
def on_playlist_panel_remove_multiple(self, widget, albums):
self._mcg.remove_albums_from_playlist(albums)
def on_playlist_panel_play(self, widget, album):
self._mcg.play_album_from_playlist(album)
def on_server_panel_output_device_changed(self, widget, device, enabled):
self._mcg.enable_output_device(device, enabled)
def on_cover_panel_toggle_fullscreen(self, widget):
if not self._state.get_property(WindowState.IS_FULLSCREENED):
self._appwindow.fullscreen()
else:
self._appwindow.unfullscreen()
def on_cover_panel_set_song(self, widget, pos, time):
self._mcg.seek(pos, time)
def on_library_panel_update(self, widget):
self._mcg.update()
def on_library_panel_play(self, widget, album):
self._mcg.play_album(album)
def on_library_panel_queue(self, widget, album):
self._mcg.queue_album(album)
def on_library_panel_queue_multiple(self, widget, albums):
self._mcg.queue_albums(albums)
def on_library_panel_item_size_changed(self, widget, size):
self._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(size)
self._settings.set_int(Window.SETTING_ITEM_SIZE, self._panels[Window._PANEL_INDEX_LIBRARY].get_item_size())
def on_library_panel_sort_order_changed(self, widget, sort_order):
self._settings.set_enum(Window.SETTING_SORT_ORDER, self._panels[Window._PANEL_INDEX_LIBRARY].get_sort_order())
def on_library_panel_sort_type_changed(self, widget, sort_type):
self._settings.set_boolean(Window.SETTING_SORT_TYPE, self._panels[Window._PANEL_INDEX_LIBRARY].get_sort_type())
# MCG callbacks
def on_mcg_connect(self, connected):
if connected:
GObject.idle_add(self._connect_connected)
self._mcg.load_playlist()
self._mcg.load_albums()
self._mcg.get_custom(Window._CUSTOM_STARTUP_COMPLETE)
self._mcg.get_status()
self._mcg.get_stats()
self._mcg.get_output_devices()
self._connect_action.set_state(GLib.Variant.new_boolean(True))
self._play_action.set_enabled(True)
self._clear_playlist_action.set_enabled(True)
self._panel_action.set_enabled(True)
else:
GObject.idle_add(self._connect_disconnected)
self._connect_action.set_state(GLib.Variant.new_boolean(False))
self._play_action.set_enabled(False)
self._clear_playlist_action.set_enabled(False)
self._panel_action.set_enabled(False)
def on_mcg_status(self, state, album, pos, time, volume, file, audio, bitrate, error):
# Album
GObject.idle_add(self._panels[Window._PANEL_INDEX_COVER].set_album, album)
if not album and self._state.get_property(WindowState.IS_FULLSCREENED):
self._fullscreen(False)
# State
if state == 'play':
GObject.idle_add(self._header_bar.set_play)
GObject.idle_add(self._panels[Window._PANEL_INDEX_COVER].set_play, pos, time)
self._play_action.set_state(GLib.Variant.new_boolean(True))
elif state == 'pause' or state == 'stop':
GObject.idle_add(self._header_bar.set_pause)
GObject.idle_add(self._panels[Window._PANEL_INDEX_COVER].set_pause)
self._play_action.set_state(GLib.Variant.new_boolean(False))
# Volume
GObject.idle_add(self._header_bar.set_volume, volume)
# Status
self._panels[Window._PANEL_INDEX_SERVER].set_status(file, audio, bitrate, error)
# Error
if error is None:
self._infobar.hide()
else:
self._show_error(error)
def on_mcg_stats(self, artists, albums, songs, dbplaytime, playtime, uptime):
self._panels[Window._PANEL_INDEX_SERVER].set_stats(artists, albums, songs, dbplaytime, playtime, uptime)
def on_mcg_load_output_devices(self, devices):
self._panels[Window._PANEL_INDEX_SERVER].set_output_devices(devices)
def on_mcg_load_playlist(self, playlist):
self._panels[self._PANEL_INDEX_PLAYLIST].set_playlist(self._connection_panel.get_host(), playlist)
def on_mcg_load_albums(self, albums):
self._panels[self._PANEL_INDEX_LIBRARY].set_albums(self._connection_panel.get_host(), albums)
def on_mcg_custom(self, name):
if name == Window._CUSTOM_STARTUP_COMPLETE:
for panel in self._panels:
GObject.idle_add(
self._stack.child_set_property,
panel.get(),
'needs-attention',
False
)
def on_mcg_error(self, error):
GObject.idle_add(self._show_error, str(error))
# Settings callbacks
def on_settings_panel_changed(self, settings, key):
panel_index = settings.get_int(key)
self._stack.set_visible_child(self._panels[panel_index].get())
def on_settings_item_size_changed(self, settings, key):
size = settings.get_int(key)
self._panels[Window._PANEL_INDEX_PLAYLIST].set_item_size(size)
self._panels[Window._PANEL_INDEX_LIBRARY].set_item_size(size)
def on_settings_sort_order_changed(self, settings, key):
sort_order = settings.get_enum(key)
self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_order(sort_order)
def on_settings_sort_type_changed(self, settings, key):
sort_type = settings.get_boolean(key)
self._panels[Window._PANEL_INDEX_LIBRARY].set_sort_type(sort_type)
# Private methods
def _connect(self):
self._connection_panel.get().set_sensitive(False)
self._header_bar.set_sensitive(False, True)
if self._mcg.is_connected():
self._mcg.disconnect()
self._settings.set_boolean(Window.SETTING_CONNECTED, False)
else:
host = self._connection_panel.get_host()
port = self._connection_panel.get_port()
password = self._connection_panel.get_password()
image_dir = self._connection_panel.get_image_dir()
self._mcg.connect(host, port, password, image_dir)
self._settings.set_boolean(Window.SETTING_CONNECTED, True)
def _connect_connected(self):
self._header_bar.connected()
self._header_bar.set_sensitive(True, False)
self._content_stack.set_visible_child(self._stack)
self._stack.set_visible_child(self._panels[self._settings.get_int(Window.SETTING_PANEL)].get())
def _connect_disconnected(self):
self._panels[Window._PANEL_INDEX_PLAYLIST].stop_threads();
self._panels[Window._PANEL_INDEX_LIBRARY].stop_threads();
self._header_bar.disconnected()
self._header_bar.set_sensitive(False, False)
self._save_visible_panel()
self._content_stack.set_visible_child(self._connection_panel.get())
self._connection_panel.get().set_sensitive(True)
def _fullscreen(self, fullscreened_new):
if fullscreened_new != self._state.get_property(WindowState.IS_FULLSCREENED):
self._state.set_property(WindowState.IS_FULLSCREENED, fullscreened_new)
if self._state.get_property(WindowState.IS_FULLSCREENED):
self._header_bar.get().hide()
self._panels[Window._PANEL_INDEX_COVER].set_fullscreen(True)
else:
self._header_bar.get().show()
self._panels[Window._PANEL_INDEX_COVER].set_fullscreen(False)
def _save_visible_panel(self):
panels = [panel.get() for panel in self._panels]
panel_index_selected = panels.index(self._stack.get_visible_child())
self._settings.set_int(Window.SETTING_PANEL, panel_index_selected)
def _set_menu_visible_panel(self):
panels = [panel.get() for panel in self._panels]
panel_index_selected = panels.index(self._stack.get_visible_child())
self._panel_action.set_state(GLib.Variant.new_string(str(panel_index_selected)))
def _set_visible_toolbar(self):
panels = [panel.get() for panel in self._panels]
panel_index_selected = panels.index(self._stack.get_visible_child())
toolbar = self._panels[panel_index_selected].get_toolbar()
self._toolbar_stack.set_visible_child(toolbar)
def _show_error(self, message):
self._infobar.show_error(message)
class HeaderBar(GObject.GObject):
__gsignals__ = {
'stack-switched': (GObject.SIGNAL_RUN_FIRST, None, ()),
'toolbar-connect': (GObject.SIGNAL_RUN_FIRST, None, ()),
'toolbar-playpause': (GObject.SIGNAL_RUN_FIRST, None, ()),
'toolbar-set-volume': (GObject.SIGNAL_RUN_FIRST, None, (int,))
}
def __init__(self, builder):
GObject.GObject.__init__(self)
self._changing_volume = False
self._setting_volume = False
# Widgets
self._header_bar = builder.get_object('headerbar')
self._title_stack = builder.get_object('headerbar-title-stack')
self._connection_label = builder.get_object('headerbar-connectionn-label')
self._stack_switcher = StackSwitcher(builder)
self._button_connect = builder.get_object('headerbar-connection')
self._button_playpause = builder.get_object('headerbar-playpause')
self._button_volume = builder.get_object('headerbar-volume')
# Signals
self._stack_switcher.connect('stack-switched', self.on_stack_switched)
self._button_handlers = {
'on_headerbar-connection_active_notify': self.on_connection_active_notify,
'on_headerbar-connection_state_set': self.on_connection_state_set,
'on_headerbar-playpause_toggled': self.on_playpause_toggled,
'on_headerbar-volume_value_changed': self.on_volume_changed,
'on_headerbar-volume_button_press_event': self.on_volume_press,
'on_headerbar-volume_button_release_event': self.on_volume_release
}
def get(self):
return self._header_bar
def get_signal_handlers(self):
return self._button_handlers
def set_sensitive(self, sensitive, connecting):
self._button_playpause.set_sensitive(sensitive)
self._button_volume.set_sensitive(sensitive)
self._stack_switcher.get().set_sensitive(sensitive)
self._button_connect.set_sensitive(not connecting)
def on_connection_active_notify(self, widget, status):
self.emit('toolbar-connect')
def on_connection_state_set(self, widget, state):
return True
def on_playpause_toggled(self, widget):
self.emit('toolbar-playpause')
def on_stack_switched(self, widget):
self.emit('stack-switched')
def on_volume_changed(self, widget, value):
if not self._setting_volume:
self.emit('toolbar-set-volume', int(value*100))
def on_volume_press(self, *args):
self.volume_set_active(None, None, True)
def on_volume_release(self, *args):
self.volume_set_active(None, None, False)
def volume_set_active(self, widget, event, active):
self._changing_volume = active
def connected(self):
self._button_connect.handler_block_by_func(
self.on_connection_active_notify
)
self._button_connect.set_active(True)
self._button_connect.set_state(True)
self._button_connect.handler_unblock_by_func(
self.on_connection_active_notify
)
self._title_stack.set_visible_child(self._stack_switcher.get())
def disconnected(self):
self._button_connect.handler_block_by_func(
self.on_connection_active_notify
)
self._button_connect.set_active(False)
self._button_connect.set_state(False)
self._button_connect.handler_unblock_by_func(
self.on_connection_active_notify
)
self._title_stack.set_visible_child(self._connection_label)
def set_play(self):
self._button_playpause.handler_block_by_func(
self.on_playpause_toggled
)
self._button_playpause.set_active(True)
self._button_playpause.handler_unblock_by_func(
self.on_playpause_toggled
)
def set_pause(self):
self._button_playpause.handler_block_by_func(
self.on_playpause_toggled
)
self._button_playpause.set_active(False)
self._button_playpause.handler_unblock_by_func(
self.on_playpause_toggled
)
def set_volume(self, volume):
if volume >= 0:
self._button_volume.set_visible(True)
if not self._changing_volume:
self._setting_volume = True
self._button_volume.set_value(volume / 100)
self._setting_volume = False
else:
self._button_volume.set_visible(False)
class InfoBar():
def __init__(self, builder):
# Widgets
self._revealer = builder.get_object('server-info-revealer')
self._bar = builder.get_object('server-info-bar')
self._message_label = builder.get_object('server-info-label')
def get_signal_handlers(self):
return {
'on_server-info-bar_close': self.on_close,
'on_server-info-bar_response': self.on_response
}
def on_close(self, *args):
self.hide()
def on_response(self, widget, response):
self.hide()
def hide(self):
self._revealer.set_reveal_child(False)
def show_error(self, message):
self._bar.set_message_type(Gtk.MessageType.ERROR)
self._message_label.set_text(message)
self._revealer.set_reveal_child(True)
class ConnectionPanel(GObject.GObject):
__gsignals__ = {
'connection-changed': (GObject.SIGNAL_RUN_FIRST, None, (str, int, str, str))
}
def __init__(self, builder):
GObject.GObject.__init__(self)
self._services = Gtk.ListStore(str, str, int)
self._profile = None
# Widgets
self._panel = builder.get_object('connection-panel')
# Zeroconf
self._zeroconf_list = builder.get_object('server-zeroconf-list')
self._zeroconf_list.set_model(self._services)
renderer = Gtk.CellRendererText()
column = Gtk.TreeViewColumn("Zeroconf", renderer, text=0)
self._zeroconf_list.append_column(column)
# Host
self._host_entry = builder.get_object('server-host')
# Port
self._port_spinner = builder.get_object('server-port')
# Passwort
self._password_entry = builder.get_object('server-password')
# Image directory
self._image_dir_entry = builder.get_object('server-image-dir')
# Zeroconf provider
self._zeroconf_provider = ZeroconfProvider()
self._zeroconf_provider.connect_signal(ZeroconfProvider.SIGNAL_SERVICE_NEW, self.on_new_service)
def get(self):
return self._panel
def get_signal_handlers(self):
return {
'on_server-zeroconf-list-selection_changed': self.on_service_selected,
'on_server-zeroconf-list_focus_out_event': self.on_zeroconf_list_outfocused,
'on_server-host_focus_out_event': self.on_host_entry_outfocused,
'on_server-port_value_changed': self.on_port_spinner_value_changed,
'on_server-password_focus_out_event': self.on_password_entry_outfocused,
'on_server-image-dir_focus_out_event': self.on_image_dir_entry_outfocused
}
def on_new_service(self, service):
name, host, port = service
self._services.append([name, host, port])
def on_service_selected(self, selection):
model, treeiter = selection.get_selected()
if treeiter != None:
service = model[treeiter]
self.set_host(service[1])
self.set_port(service[2])
def on_zeroconf_list_outfocused(self, widget, event):
self._zeroconf_list.get_selection().unselect_all()
def on_host_entry_outfocused(self, widget, event):
self._call_back()
def on_port_spinner_value_changed(self, widget):
self._call_back()
def on_password_entry_outfocused(self, widget, event):
self._call_back()
def on_image_dir_entry_outfocused(self, widget, event):
self._call_back()
def set_host(self, host):
self._host_entry.set_text(host)
def get_host(self):
return self._host_entry.get_text()
def set_port(self, port):
self._port_spinner.set_value(port)
def get_port(self):
return self._port_spinner.get_value_as_int()
def set_password(self, password):
if password is None:
password = ""
self._password_entry.set_text(password)
def get_password(self):
if self._password_entry.get_text() == "":
return None
else:
return self._password_entry.get_text()
def set_image_dir(self, image_dir):
self._image_dir_entry.set_text(image_dir)
def get_image_dir(self):
return self._image_dir_entry.get_text()
def _call_back(self):
self.emit('connection-changed', self.get_host(), self.get_port(), self.get_password(), self.get_image_dir())
class ServerPanel(GObject.GObject):
__gsignals__ = {
'change-output-device': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,bool,)),
}
def __init__(self, builder):
GObject.GObject.__init__(self)
self._none_label = ""
self._output_buttons = {}
self._is_selected = False
# Widgets
self._panel = builder.get_object('server-panel')
self._toolbar = builder.get_object('server-toolbar')
self._stack = builder.get_object('server-stack')
# Status widgets
self._status_file = builder.get_object('server-status-file')
self._status_audio = builder.get_object('server-status-audio')
self._status_bitrate = builder.get_object('server-status-bitrate')
self._status_error = builder.get_object('server-status-error')
self._none_label = self._status_file.get_label()
# Stats widgets
self._stats_artists = builder.get_object('server-stats-artists')
self._stats_albums = builder.get_object('server-stats-albums')
self._stats_songs = builder.get_object('server-stats-songs')
self._stats_dbplaytime = builder.get_object('server-stats-dbplaytime')
self._stats_playtime = builder.get_object('server-stats-playtime')
self._stats_uptime = builder.get_object('server-stats-uptime')
# Audio ouptut devices widgets
self._output_devices = builder.get_object('server-output-devices')
def get(self):
return self._panel
def set_selected(self, selected):
self._is_selected = selected
def get_toolbar(self):
return self._toolbar
def on_output_device_toggled(self, widget, device):
self.emit('change-output-device', device, widget.get_active())
def set_status(self, file, audio, bitrate, error):
if file:
file = GObject.markup_escape_text(file)
else:
file = self._none_label
self._status_file.set_markup(file)
# Audio information
if audio:
parts = audio.split(":")
if len(parts) == 3:
audio = "{}Hz, {}bit, {}channels".format(parts[0], parts[1], parts[2])
else:
audio = self._none_label
self._status_audio.set_markup(audio)
# Bitrate
if bitrate:
bitrate = bitrate + "kb/s"
else:
bitrate = self._none_label
self._status_bitrate.set_markup(bitrate)
# Error
if error:
error = GObject.markup_escape_text(error)
else:
error = self._none_label
self._status_error.set_markup(error)
def set_stats(self, artists, albums, songs, dbplaytime, playtime, uptime):
self._stats_artists.set_text(str(artists))
self._stats_albums.set_text(str(albums))
self._stats_songs.set_text(str(songs))
self._stats_dbplaytime.set_text(str(dbplaytime))
self._stats_playtime.set_text(str(playtime))
self._stats_uptime.set_text(str(uptime))
def set_output_devices(self, devices):
device_ids = []
# Add devices
for device in devices:
device_ids.append(device.get_id())
if device.get_id() in self._output_buttons.keys():
self._output_buttons[device.get_id()].freeze_notify()
self._output_buttons[device.get_id()].set_active(device.is_enabled())
self._output_buttons[device.get_id()].thaw_notify()
else:
button = Gtk.CheckButton(device.get_name())
if device.is_enabled():
button.set_active(True)
handler = button.connect('toggled', self.on_output_device_toggled, device)
self._output_devices.insert(button, -1)
self._output_buttons[device.get_id()] = button
self._output_devices.show_all()
# Remove devices
for id in self._output_buttons.keys():
if id not in device_ids:
self._output_devices.remove(self._output_buttons[id].get_parent())
class CoverPanel(GObject.GObject):
__gsignals__ = {
'toggle-fullscreen': (GObject.SIGNAL_RUN_FIRST, None, ()),
'set-song': (GObject.SIGNAL_RUN_FIRST, None, (int, int,))
}
def __init__(self, builder):
GObject.GObject.__init__(self)
self._current_album = None
self._cover_pixbuf = None
self._timer = None
self._properties = {}
self._icon_theme = Gtk.IconTheme.get_default()
self._fullscreened = False
self._is_selected = False
self._current_size = None
# Widgets
self._appwindow = builder.get_object('appwindow')
self._panel = builder.get_object('cover-panel')
self._toolbar = builder.get_object('cover-toolbar')
# Toolbar menu
self._toolbar_fullscreen_button = builder.get_object('cover-toolbar-fullscreen')
# Cover
self._cover_stack = builder.get_object('cover-stack')
self._cover_spinner = builder.get_object('cover-spinner')
self._cover_scroll = builder.get_object('cover-scroll')
self._cover_box = builder.get_object('cover-box')
self._cover_image = builder.get_object('cover-image')
self._cover_stack.set_visible_child(self._cover_scroll)
self._cover_pixbuf = self._get_default_image()
# Album Infos
self._info_revealer = builder.get_object('cover-info-revealer')
self._info_box = builder.get_object('cover-info-box')
self._album_title_label = builder.get_object('cover-album')
self._album_date_label = builder.get_object('cover-date')
self._album_artist_label = builder.get_object('cover-artist')
# Songs
self._songs_scale = builder.get_object('cover-songs')
# Initial actions
GObject.idle_add(self._enable_tracklist)
def get(self):
return self._panel
def set_selected(self, selected):
self._is_selected = selected
def get_toolbar(self):
return self._toolbar
def get_signal_handlers(self):
return {
'on_cover-toolbar-fullscreen_clicked': self.on_fullscreen_clicked,
'on_cover-box_button_press_event': self.on_cover_box_pressed,
'on_cover-scroll_size_allocate': self.on_cover_size_allocate,
'on_cover-songs_button_press_event': self.on_songs_start_change,
'on_cover-songs_button_release_event': self.on_songs_change
}
def on_fullscreen_clicked(self, widget):
self.emit('toggle-fullscreen')
def on_cover_box_pressed(self, widget, event):
if self._current_album and event.type == Gdk.EventType._2BUTTON_PRESS:
self.emit('toggle-fullscreen')
def on_cover_size_allocate(self, widget, allocation):
GObject.idle_add(self._resize_image)
def on_songs_start_change(self, widget, event):
if self._timer:
GObject.source_remove(self._timer)
self._timer = None
def on_songs_change(self, widget, event):
value = int(self._songs_scale.get_value())
time = self._current_album.get_length()
tracks = self._current_album.get_tracks()
pos = 0
for index in range(len(tracks)-1, -1, -1):
time = time - tracks[index].get_length()
pos = tracks[index].get_pos()
if time < value:
break
time = max(value - time - 1, 0)
self.emit('set-song', pos, time)
def set_album(self, album):
if album:
# Set labels
self._album_title_label.set_label(album.get_title())
self._album_date_label.set_label(', '.join(album.get_dates()))
self._album_artist_label.set_label(', '.join(album.get_albumartists()))
# Set tracks
self._set_tracks(album)
# Set current album
old_album = self._current_album
self._current_album = album
self._enable_tracklist()
self._toolbar_fullscreen_button.set_sensitive(self._current_album is not None)
# Load cover
threading.Thread(target=self._set_cover, args=(old_album, album,)).start()
def set_play(self, pos, time):
if self._timer is not None:
GObject.source_remove(self._timer)
self._timer = None
tracks = self._current_album.get_tracks()
for index in range(0, pos):
time = time + tracks[index].get_length()
self._songs_scale.set_value(time+1)
self._timer = GObject.timeout_add(1000, self._playing)
def set_pause(self):
if self._timer is not None:
GObject.source_remove(self._timer)
self._timer = None
def set_fullscreen(self, active):
if active:
self._info_revealer.set_reveal_child(False)
self._cover_box.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 1))
GObject.idle_add(self._resize_image)
# Hide curser
self._appwindow.get_window().set_cursor(
Gdk.Cursor.new_from_name(Gdk.Display.get_default(), "none")
)
self._fullscreened = True
else:
self._fullscreened = False
self._info_revealer.set_reveal_child(True)
self._cover_box.override_background_color(Gtk.StateFlags.NORMAL, Gdk.RGBA(0, 0, 0, 0))
GObject.idle_add(self._resize_image)
# Reset cursor
self._appwindow.get_window().set_cursor(
Gdk.Cursor.new_from_name(Gdk.Display.get_default(), "default")
)
def _set_cover(self, current_album, new_album):
self._cover_stack.set_visible_child(self._cover_spinner)
self._cover_spinner.start()
if not current_album or not new_album or current_album != new_album:
url = new_album.get_cover() if new_album else None
if url and url is not "":
# Load image and draw it
self._cover_pixbuf = Utils.load_cover(url)
else:
# Reset image
self._cover_pixbuf = self._get_default_image()
self._current_size = None
self._resize_image()
self._cover_stack.set_visible_child(self._cover_scroll)
self._cover_spinner.stop()
def _set_tracks(self, album):
self._songs_scale.clear_marks()
self._songs_scale.set_range(0, album.get_length())
length = 0
for track in album.get_tracks():
cur_length = length
if length > 0 and length < album.get_length():
cur_length = cur_length + 1
self._songs_scale.add_mark(
cur_length,
Gtk.PositionType.RIGHT,
GObject.markup_escape_text(
Utils.create_track_title(track)
)
)
length = length + track.get_length()
self._songs_scale.add_mark(
length,
Gtk.PositionType.RIGHT,
"{0[0]:02d}:{0[1]:02d} minutes".format(divmod(length, 60))
)
def _enable_tracklist(self):
if self._current_album:
# enable
self._info_revealer.set_reveal_child(True)
else:
# disable
self._info_revealer.set_reveal_child(False)
def _playing(self):
value = self._songs_scale.get_value() + 1
self._songs_scale.set_value(value)
return True
def _resize_image(self):
"""Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
"""
# Get size
size = self._cover_scroll.get_allocation()
# Abort if size is the same
if self._current_size and size.width == self._current_size.width and size.height == self._current_size.height:
return
self._current_size = size
# Get pixelbuffer
pixbuf = self._cover_pixbuf
# Check pixelbuffer
if pixbuf is None:
return
# Skalierungswert für Breite und Höhe ermitteln
ratioW = float(size.width) / float(pixbuf.get_width())
ratioH = float(size.height) / float(pixbuf.get_height())
# Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
ratio = min(ratioW, ratioH)
ratio = min(ratio, 1)
# Neue Breite und Höhe berechnen
width = int(math.floor(pixbuf.get_width()*ratio))
height = int(math.floor(pixbuf.get_height()*ratio))
if width <= 0 or height <= 0:
return
# Pixelpuffer auf Oberfläche zeichnen
self._cover_image.set_allocation(self._cover_scroll.get_allocation())
self._cover_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
self._cover_image.show()
def _get_default_image(self):
return self._icon_theme.load_icon(
Window.STOCK_ICON_DEFAULT,
512,
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
)
class PlaylistPanel(GObject.GObject):
__gsignals__ = {
'clear-playlist': (GObject.SIGNAL_RUN_FIRST, None, ()),
'remove': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
'remove-multiple': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
'play': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,))
}
def __init__(self, builder):
GObject.GObject.__init__(self)
self._host = None
self._item_size = 150
self._playlist = None
self._playlist_albums = None
self._playlist_lock = threading.Lock()
self._playlist_stop = threading.Event()
self._icon_theme = Gtk.IconTheme.get_default()
self._standalone_pixbuf = None
self._selected_albums = []
self._is_selected = False
# Widgets
self._appwindow = builder.get_object('appwindow')
self._panel = builder.get_object('playlist-panel')
self._toolbar = builder.get_object('playlist-toolbar')
self._headerbar = builder.get_object('headerbar')
self._headerbar_standalone = builder.get_object('headerbar-playlist-standalone')
self._panel_normal = builder.get_object('playlist-panel-normal')
self._panel_standalone = builder.get_object('playlist-panel-standalone')
self._actionbar_revealer = builder.get_object('playlist-actionbar-revealer')
# Select button
self._select_button = builder.get_object('playlist-toolbar-select')
# Clear button
self._playlist_clear_button = builder.get_object('playlist-toolbar-clear')
# Playlist Grid: Model
self._playlist_grid_model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
# Playlist Grid
self._playlist_grid = builder.get_object('playlist-iconview')
self._playlist_grid.set_model(self._playlist_grid_model)
self._playlist_grid.set_pixbuf_column(0)
self._playlist_grid.set_text_column(-1)
self._playlist_grid.set_tooltip_column(1)
# Action bar (normal)
actionbar = builder.get_object('playlist-actionbar')
cancel_button = Gtk.Button(gettext.gettext("cancel"))
cancel_button.connect('clicked', self.on_selection_cancel_clicked)
actionbar.pack_start(cancel_button)
remove_button = Gtk.Button('remove')
remove_button.connect('clicked', self.on_selection_remove_clicked)
actionbar.pack_end(remove_button)
# Standalone labels
self._standalone_title = builder.get_object('headerbar-playlist-standalone-title')
self._standalone_artist = builder.get_object('headerbar-playlist-standalone-artist')
# Standalone Image
self._standalone_stack = builder.get_object('playlist-standalone-stack')
self._standalone_spinner = builder.get_object('playlist-standalone-spinner')
self._standalone_scroll = builder.get_object('playlist-standalone-scroll')
self._standalone_image = builder.get_object('playlist-standalone-image')
# Action bar (standalone)
actionbar_standalone = builder.get_object('playlist-standalone-actionbar')
play_button = Gtk.Button(gettext.gettext("play"))
play_button.connect('clicked', self.on_standalone_play_clicked)
actionbar_standalone.pack_end(play_button)
remove_button = Gtk.Button(gettext.gettext("remove"))
remove_button.connect('clicked', self.on_standalone_remove_clicked)
actionbar_standalone.pack_end(remove_button)
def get(self):
return self._panel
def set_selected(self, selected):
self._is_selected = selected
def get_toolbar(self):
return self._toolbar
def get_signal_handlers(self):
return {
'on_playlist-toolbar-select_toggled': self.on_select_toggled,
'on_playlist-toolbar-clear_clicked': self.clear_clicked,
'on_playlist-iconview_item_activated': self.on_playlist_grid_clicked,
'on_playlist-iconview_selection_changed': self.on_playlist_grid_selection_changed,
'on_playlist-standalone-scroll_size_allocate': self.on_standalone_scroll_size_allocate,
'on_headerbar-playlist-standalone-close_clicked': self.on_standalone_close_clicked
}
def on_select_toggled(self, widget):
if widget.get_active():
self._actionbar_revealer.set_reveal_child(True)
self._playlist_grid.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
self._playlist_grid.get_style_context().add_class(Window._CSS_SELECTION)
else:
self._actionbar_revealer.set_reveal_child(False)
self._playlist_grid.set_selection_mode(Gtk.SelectionMode.SINGLE)
self._playlist_grid.get_style_context().remove_class(Window._CSS_SELECTION)
def clear_clicked(self, widget):
if widget is self._playlist_clear_button:
self.emit('clear-playlist')
def on_playlist_grid_clicked(self, widget, path):
# Get selected album
iter = self._playlist_grid_model.get_iter(path)
hash = self._playlist_grid_model.get_value(iter, 2)
album = self._playlist_albums[hash]
self._selected_albums = [album]
# Show standalone album
if widget.get_selection_mode() == Gtk.SelectionMode.SINGLE:
# Set labels
self._standalone_title.set_text(album.get_title())
self._standalone_artist.set_text(", ".join(album.get_albumartists()))
# Show panel
self._open_standalone()
# Load cover
threading.Thread(target=self._show_standalone_image, args=(album,)).start()
def on_playlist_grid_selection_changed(self, widget):
self._selected_albums = []
for path in widget.get_selected_items():
iter = self._playlist_grid_model.get_iter(path)
hash = self._playlist_grid_model.get_value(iter, 2)
self._selected_albums.append(self._playlist_albums[hash])
def on_selection_cancel_clicked(self, widget):
self._select_button.set_active(False)
def on_selection_remove_clicked(self, widget):
self.emit('remove-multiple', self._selected_albums)
self._select_button.set_active(False)
def on_standalone_scroll_size_allocate(self, widget, allocation):
self._resize_standalone_image()
def on_standalone_close_clicked(self, widget):
self._close_standalone()
def on_standalone_remove_clicked(self, widget):
self.emit('remove', self._selected_albums[0])
self._close_standalone()
def on_standalone_play_clicked(self, widget):
self.emit('play', self._selected_albums[0])
self._close_standalone()
def set_item_size(self, item_size):
if self._item_size != item_size:
self._item_size = item_size
self._redraw()
def get_item_size(self):
return self._item_size
def set_playlist(self, host, playlist):
self._host = host
self._playlist_stop.set()
threading.Thread(target=self._set_playlist, args=(host, playlist, self._item_size,)).start()
def stop_threads(self):
self._playlist_stop.set()
def _set_playlist(self, host, playlist, size):
if not self._is_selected and self._playlist != playlist:
GObject.idle_add(
self.get().get_parent().child_set_property,
self.get(),
'needs-attention',
True
)
self._playlist_lock.acquire()
self._playlist_stop.clear()
self._playlist = playlist
self._playlist_albums = {}
for album in playlist:
self._playlist_albums[album.get_id()] = album
self._playlist_grid.set_model(None)
self._playlist_grid.freeze_child_notify()
self._playlist_grid_model.clear()
GObject.idle_add(self._playlist_grid.set_item_padding, size / 100)
cache = client.MCGCache(host, size)
for album in playlist:
pixbuf = None
if album.get_cover() is not None:
try:
pixbuf = Utils.load_thumbnail(cache, album, size)
except Exception as e:
print(e)
if pixbuf is None:
pixbuf = self._icon_theme.load_icon(
Window.STOCK_ICON_DEFAULT,
self._item_size,
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
)
if pixbuf is not None:
self._playlist_grid_model.append([
pixbuf,
GObject.markup_escape_text("\n".join([
album.get_title(),
', '.join(album.get_dates()),
Utils.create_artists_label(album)
])),
album.get_id()
])
if self._playlist_stop.is_set():
self._playlist_lock.release()
return
self._playlist_grid.set_model(self._playlist_grid_model)
self._playlist_grid.thaw_child_notify()
# TODO why set_columns()?
#self._playlist_grid.set_columns(len(playlist))
self._playlist_lock.release()
def _redraw(self):
if self._playlist is not None:
self.set_playlist(self._host, self._playlist)
def _open_standalone(self):
self._panel.set_visible_child(self._panel_standalone)
self._appwindow.set_titlebar(self._headerbar_standalone)
def _close_standalone(self):
self._panel.set_visible_child(self._panel.get_children()[0])
self._appwindow.set_titlebar(self._headerbar)
def _show_standalone_image(self, album):
self._standalone_stack.set_visible_child(self._standalone_spinner)
self._standalone_spinner.start()
url = album.get_cover()
if url is not None and url is not "":
# Load image and draw it
self._standalone_pixbuf = Utils.load_cover(url)
self._resize_standalone_image()
else:
# Reset image
self._standalone_image.clear()
self._standalone_stack.set_visible_child(self._standalone_scroll)
self._standalone_spinner.stop()
def _resize_standalone_image(self):
"""Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
"""
pixbuf = self._standalone_pixbuf
size = self._standalone_scroll.get_allocation()
# Check pixelbuffer
if pixbuf is None:
return
# Skalierungswert für Breite und Höhe ermitteln
ratioW = float(size.width) / float(pixbuf.get_width())
ratioH = float(size.height) / float(pixbuf.get_height())
# Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
ratio = min(ratioW, ratioH)
ratio = min(ratio, 1)
# Neue Breite und Höhe berechnen
width = int(math.floor(pixbuf.get_width()*ratio))
height = int(math.floor(pixbuf.get_height()*ratio))
if width <= 0 or height <= 0:
return
# Pixelpuffer auf Oberfläche zeichnen
self._standalone_image.set_allocation(self._standalone_scroll.get_allocation())
self._standalone_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
self._standalone_image.show()
class LibraryPanel(GObject.GObject):
__gsignals__ = {
'update': (GObject.SIGNAL_RUN_FIRST, None, ()),
'play': (GObject.SIGNAL_RUN_FIRST, None, (str,)),
'queue': (GObject.SIGNAL_RUN_FIRST, None, (str,)),
'queue-multiple': (GObject.SIGNAL_RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
'item-size-changed': (GObject.SIGNAL_RUN_FIRST, None, (int,)),
'sort-order-changed': (GObject.SIGNAL_RUN_FIRST, None, (int,)),
'sort-type-changed': (GObject.SIGNAL_RUN_FIRST, None, (Gtk.SortType,))
}
def __init__(self, builder):
GObject.GObject.__init__(self)
self._buttons = {}
self._albums = None
self._host = "localhost"
self._filter_string = ""
self._item_size = 150
self._sort_order = SortOrder.YEAR
self._sort_type = Gtk.SortType.DESCENDING
self._grid_pixbufs = {}
self._old_ranges = {}
self._library_lock = threading.Lock()
self._library_stop = threading.Event()
self._icon_theme = Gtk.IconTheme.get_default()
self._standalone_pixbuf = None
self._selected_albums = []
self._allocation = (0, 0)
self._is_selected = False
# Widgets
self._appwindow = builder.get_object('appwindow')
self._panel = builder.get_object('library-panel')
self._toolbar = builder.get_object('library-toolbar')
self._headerbar = builder.get_object('headerbar')
self._headerbar_standalone = builder.get_object('headerbar-library-standalone')
self._panel_normal = builder.get_object('library-panel-normal')
self._panel_standalone = builder.get_object('library-panel-standalone')
self._actionbar_revealer = builder.get_object('library-actionbar-revealer')
# Select button
self._select_button = builder.get_object('library-toolbar-select')
# Filter/search bar
self._filter_bar = builder.get_object('library-filter-bar')
self._filter_entry = builder.get_object('library-filter')
# Progress Bar
self._stack = builder.get_object('library-stack')
self._progress_box = builder.get_object('library-progress-box')
self._pgross_image = builder.get_object('library-progress-image')
self._pgross_image.set_from_pixbuf(self._get_default_image())
self._progress_label = builder.get_object('library-progress-label')
self._loading_text = self._progress_label.get_label()
self._progress_bar = builder.get_object('library-progress')
self._scroll = builder.get_object('library-scroll')
# Toolbar menu
self._toolbar_search_bar = builder.get_object('library-toolbar-search')
self._toolbar_popover = builder.get_object('library-toolbar-popover')
self._toolbar_sort_buttons = {
SortOrder.ARTIST: builder.get_object('library-toolbar-sort-artist'),
SortOrder.TITLE: builder.get_object('library-toolbar-sort-title'),
SortOrder.YEAR: builder.get_object('library-toolbar-sort-year')
}
self._toolbar_sort_order_button = builder.get_object('library-toolbar-sort-order')
self._grid_scale = builder.get_object('library-toolbar-scale')
self._grid_scale.set_value(self._item_size)
self._grid_adjustment = builder.get_object('library-scale-adjustment')
# Library Grid: Model
self._library_grid_model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
self._library_grid_model.set_sort_func(2, self.compare_albums, self._sort_order)
self._library_grid_model.set_sort_column_id(2, self._sort_type)
self._library_grid_filter = self._library_grid_model.filter_new()
self._library_grid_filter.set_visible_func(self.on_filter_visible)
# Library Grid
self._library_grid = builder.get_object('library-iconview')
self._library_grid.set_model(self._library_grid_filter)
self._library_grid.set_pixbuf_column(0)
self._library_grid.set_text_column(-1)
self._library_grid.set_tooltip_column(1)
# Action bar (normal)
actionbar = builder.get_object('library-actionbar')
cancel_button = Gtk.Button(gettext.gettext("cancel"))
cancel_button.connect('clicked', self.on_selection_cancel_clicked)
actionbar.pack_start(cancel_button)
add_button = Gtk.Button(gettext.gettext("queue"))
add_button.connect('clicked', self.on_selection_add_clicked)
actionbar.pack_end(add_button)
# Standalone labels
self._standalone_title = builder.get_object('headerbar-library-standalone-title')
self._standalone_artist = builder.get_object('headerbar-library-standalone-artist')
# Standalone Image
self._standalone_stack = builder.get_object('library-standalone-stack')
self._standalone_spinner = builder.get_object('library-standalone-spinner')
self._standalone_scroll = builder.get_object('library-standalone-scroll')
self._standalone_image = builder.get_object('library-standalone-image')
# Action bar (standalone)
actionbar_standalone = builder.get_object('library-standalone-actionbar')
play_button = Gtk.Button(gettext.gettext("play"))
play_button.connect('clicked', self.on_standalone_play_clicked)
actionbar_standalone.pack_end(play_button)
queue_button = Gtk.Button(gettext.gettext("queue"))
queue_button.connect('clicked', self.on_standalone_queue_clicked)
actionbar_standalone.pack_end(queue_button)
def get(self):
return self._panel
def set_selected(self, selected):
self._is_selected = selected
def get_toolbar(self):
return self._toolbar
def get_signal_handlers(self):
return {
'on_library-toolbar-search_toggled': self.on_search_toggled,
'on_library-toolbar-select_toggled': self.on_select_toggled,
'on_library-toolbar-scale_change_value': self.on_grid_scale_change,
'on_library-toolbar-scale_button_release_event': self.on_grid_scale_changed,
'on_library-toolbar-update_clicked': self.on_update_clicked,
'on_library-toolbar-sort-toggled': self.on_sort_toggled,
'on_library-toolbar-sort-order_toggled': self.on_sort_order_toggled,
'on_library-filter-bar_notify': self.on_filter_bar_notify,
'on_library-filter_search_changed': self.on_filter_entry_changed,
'on_library-iconview_item_activated': self.on_library_grid_clicked,
'on_library-iconview_selection_changed': self.on_library_grid_selection_changed,
'on_library-standalone-scroll_size_allocate': self.on_standalone_scroll_size_allocate,
'on_headerbar-library-standalone-close_clicked': self.on_standalone_close_clicked,
'on_library-iconview_size_allocate': self.on_resize
}
def on_resize(self, widget, event):
new_allocation = (widget.get_allocation().width, widget.get_allocation().height)
if new_allocation == self._allocation:
return
self._allocation = new_allocation
self._grid_scale.clear_marks()
width = widget.get_allocation().width
lower = int(self._grid_adjustment.get_lower())
upper = int(self._grid_adjustment.get_upper())
countMin = max(int(width / upper), 1)
countMax = max(int(width / lower), 1)
for index in range(countMin, countMax):
pixel = int(width / index)
pixel = pixel - (2 * int(pixel / 100))
self._grid_scale.add_mark(
pixel,
Gtk.PositionType.BOTTOM,
None
)
def on_search_toggled(self, widget):
self._filter_bar.set_search_mode(widget.get_active())
def on_select_toggled(self, widget):
if widget.get_active():
self._actionbar_revealer.set_reveal_child(True)
self._library_grid.set_selection_mode(Gtk.SelectionMode.MULTIPLE)
self._library_grid.get_style_context().add_class(Window._CSS_SELECTION)
else:
self._actionbar_revealer.set_reveal_child(False)
self._library_grid.set_selection_mode(Gtk.SelectionMode.SINGLE)
self._library_grid.get_style_context().remove_class(Window._CSS_SELECTION)
def on_grid_scale_change(self, widget, scroll, value):
size = math.floor(value)
range = self._grid_scale.get_adjustment()
if size < range.get_lower() or size > range.get_upper():
return
self._item_size = size
GObject.idle_add(self._library_grid.set_item_padding, size / 100)
GObject.idle_add(self._set_widget_grid_size, self._library_grid, size, True)
def on_grid_scale_changed(self, widget, event):
size = round(self._grid_scale.get_value())
range = self._grid_scale.get_adjustment()
if size < range.get_lower() or size > range.get_upper():
return False
self.emit('item-size-changed', size)
self._toolbar_popover.popdown()
self._redraw()
return False
def on_update_clicked(self, widget):
self.emit('update')
def on_sort_toggled(self, widget):
if widget.get_active():
sort = [key for key, value in self._toolbar_sort_buttons.items() if value is widget][0]
self._change_sort(sort)
def on_sort_order_toggled(self, button):
if button.get_active():
sort_type = Gtk.SortType.DESCENDING
else:
sort_type = Gtk.SortType.ASCENDING
self._sort_type = sort_type
self._library_grid_model.set_sort_column_id(2, sort_type)
self.emit('sort-type-changed', sort_type)
def on_filter_bar_notify(self, widget, value):
if self._toolbar_search_bar.get_active() is not self._filter_bar.get_search_mode():
self._toolbar_search_bar.set_active(self._filter_bar.get_search_mode())
def on_filter_entry_changed(self, widget):
self._filter_string = self._filter_entry.get_text()
GObject.idle_add(self._library_grid_filter.refilter)
def on_library_grid_clicked(self, widget, path):
# Get selected album
path = self._library_grid_filter.convert_path_to_child_path(path)
iter = self._library_grid_model.get_iter(path)
id = self._library_grid_model.get_value(iter, 2)
album = self._albums[id]
self._selected_albums = [album]
# Show standalone album
if widget.get_selection_mode() == Gtk.SelectionMode.SINGLE:
# Set labels
self._standalone_title.set_text(album.get_title())
self._standalone_artist.set_text(", ".join(album.get_albumartists()))
# Show panel
self._open_standalone()
# Load cover
threading.Thread(target=self._show_standalone_image, args=(album,)).start()
def on_library_grid_selection_changed(self, widget):
self._selected_albums = []
for path in widget.get_selected_items():
path = self._library_grid_filter.convert_path_to_child_path(path)
iter = self._library_grid_model.get_iter(path)
id = self._library_grid_model.get_value(iter, 2)
self._selected_albums.insert(0, self._albums[id])
def on_filter_visible(self, model, iter, data):
id = model.get_value(iter, 2)
if not id in self._albums.keys():
return
album = self._albums[id]
return album.filter(self._filter_string)
def on_selection_cancel_clicked(self, widget):
self._select_button.set_active(False)
def on_selection_add_clicked(self, widget):
ids = [album.get_id() for album in self._selected_albums]
self.emit('queue-multiple', ids)
self._select_button.set_active(False)
def on_standalone_scroll_size_allocate(self, widget, allocation):
self._resize_standalone_image()
def on_standalone_play_clicked(self, widget):
self.emit('play', self._selected_albums[0].get_id())
self._close_standalone()
def on_standalone_queue_clicked(self, widget):
self.emit('queue', self._selected_albums[0].get_id())
self._close_standalone()
def on_standalone_close_clicked(self, widget):
self._close_standalone()
def set_item_size(self, item_size):
if self._item_size != item_size:
self._item_size = item_size
self._grid_scale.set_value(item_size)
self._redraw()
def get_item_size(self):
return self._item_size
def set_sort_order(self, sort):
if self._sort_order != sort:
button = self._toolbar_sort_buttons[sort]
if button and not button.get_active():
button.set_active(True)
self._sort_order = sort
self._library_grid_model.set_sort_func(2, self.compare_albums, self._sort_order)
def get_sort_order(self):
return self._sort_order
def set_sort_type(self, sort_type):
if self._sort_type != sort_type:
if sort_type:
sort_type_gtk = Gtk.SortType.DESCENDING
self._toolbar_sort_order_button.set_active(True)
else:
sort_type_gtk = Gtk.SortType.ASCENDING
self._toolbar_sort_order_button.set_active(False)
if self._sort_type != sort_type_gtk:
self._sort_type = sort_type_gtk
self._library_grid_model.set_sort_column_id(2, sort_type)
def get_sort_type(self):
return (self._sort_type != Gtk.SortType.ASCENDING)
def set_albums(self, host, albums):
self._host = host
self._library_stop.set()
threading.Thread(target=self._set_albums, args=(host, albums, self._item_size,)).start()
def compare_albums(self, model, row1, row2, criterion):
id1 = model.get_value(row1, 2)
id2 = model.get_value(row2, 2)
if not id1 or not id2:
return
return client.MCGAlbum.compare(self._albums[id1], self._albums[id2], criterion)
def stop_threads(self):
self._library_stop.set()
def _change_sort(self, sort):
self._sort_order = sort
self._library_grid_model.set_sort_func(2, self.compare_albums, sort)
self.emit('sort-order-changed', sort)
def _set_albums(self, host, albums, size):
if not self._is_selected and albums != self._albums:
GObject.idle_add(
self.get().get_parent().child_set_property,
self.get(),
'needs-attention',
True
)
self._library_lock.acquire()
self._library_stop.clear()
self._albums = albums
GObject.idle_add(self._stack.set_visible_child, self._progress_box)
GObject.idle_add(self._progress_bar.set_fraction, 0.0)
GObject.idle_add(self._library_grid.set_item_padding, size / 100)
self._library_grid.set_model(None)
self._library_grid.freeze_child_notify()
self._library_grid_model.clear()
i = 0
n = len(albums)
cache = client.MCGCache(host, size)
self._grid_pixbufs.clear()
for album_id in albums.keys():
album = albums[album_id]
pixbuf = None
try:
pixbuf = Utils.load_thumbnail(cache, album, size)
except Exception as e:
print(e)
if pixbuf is None:
pixbuf = self._icon_theme.load_icon(
Window.STOCK_ICON_DEFAULT,
self._item_size,
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
)
if pixbuf is not None:
self._grid_pixbufs[album.get_id()] = pixbuf
self._library_grid_model.append([
pixbuf,
GObject.markup_escape_text("\n".join([
album.get_title(),
', '.join(album.get_dates()),
Utils.create_artists_label(album)
])),
album_id
])
i += 1
GObject.idle_add(self._progress_bar.set_fraction, i/n)
GObject.idle_add(self._progress_label.set_markup, self._loading_text.format(i, n))
if self._library_stop.is_set():
self._library_lock.release()
return
self._library_grid.set_model(self._library_grid_filter)
self._library_grid.thaw_child_notify()
self._library_grid.set_item_width(-1)
self._library_lock.release()
self._stack.set_visible_child(self._scroll)
def _set_widget_grid_size(self, grid_widget, size, vertical):
self._library_stop.set()
threading.Thread(target=self._set_widget_grid_size_thread, args=(grid_widget, size, vertical,)).start()
def _set_widget_grid_size_thread(self, grid_widget, size, vertical):
self._library_lock.acquire()
self._library_stop.clear()
grid_filter = grid_widget.get_model()
grid_model = grid_filter.get_model()
# get old_range
grid_widget_id = id(grid_widget)
if grid_widget_id not in self._old_ranges or self._old_ranges[grid_widget_id] is None:
self._old_ranges[grid_widget_id] = range(0, len(grid_filter))
old_range = self._old_ranges[grid_widget_id]
old_start = len(old_range) > 0 and old_range[0] or 0
old_end = len(old_range) > 0 and old_range[len(old_range)-1] + 1 or 0
# calculate visible range
w = (grid_widget.get_allocation().width // size) + (vertical and 0 or 1)
h = (grid_widget.get_allocation().height // size) + (vertical and 1 or 0)
c = w * h
vis_range = grid_widget.get_visible_range()
if vis_range is None:
self._library_lock.release()
return
(vis_start,), (vis_end,) = vis_range
vis_end = min(vis_start + c, len(grid_filter))
vis_range = range(vis_start, vis_end)
# set pixbuf
cur_start = min(old_start, vis_start)
cur_end = max(old_end, vis_end)
cur_range = range(cur_start, cur_end)
for index in cur_range:
iter = grid_filter.convert_iter_to_child_iter(grid_filter[index].iter)
if index in vis_range:
album_id = grid_model.get_value(iter, 2)
pixbuf = self._grid_pixbufs[album_id]
pixbuf = pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.NEAREST)
else:
pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, 1, 1)
grid_model.set_value(iter, 0, pixbuf)
if self._library_stop.is_set():
self._library_lock.release()
return
self._old_ranges[grid_widget_id] = vis_range
grid_widget.set_item_width(size)
self._library_lock.release()
def _redraw(self):
if self._albums is not None:
self.set_albums(self._host, self._albums)
def _open_standalone(self):
self._panel.set_visible_child(self._panel_standalone)
self._appwindow.set_titlebar(self._headerbar_standalone)
def _close_standalone(self):
self._panel.set_visible_child(self._panel.get_children()[0])
self._appwindow.set_titlebar(self._headerbar)
def _show_standalone_image(self, album):
self._standalone_stack.set_visible_child(self._standalone_spinner)
self._standalone_spinner.start()
url = album.get_cover()
if url is not None and url is not "":
# Load image and draw it
self._standalone_pixbuf = Utils.load_cover(url)
self._resize_standalone_image()
else:
# Reset image
self._standalone_image.clear()
self._standalone_stack.set_visible_child(self._standalone_scroll)
self._standalone_spinner.stop()
def _resize_standalone_image(self):
"""Diese Methode skaliert das geladene Bild aus dem Pixelpuffer
auf die Größe des Fensters unter Beibehalt der Seitenverhältnisse
"""
pixbuf = self._standalone_pixbuf
size = self._standalone_scroll.get_allocation()
# Check pixelbuffer
if pixbuf is None:
return
# Skalierungswert für Breite und Höhe ermitteln
ratioW = float(size.width) / float(pixbuf.get_width())
ratioH = float(size.height) / float(pixbuf.get_height())
# Kleineren beider Skalierungswerte nehmen, nicht Hochskalieren
ratio = min(ratioW, ratioH)
ratio = min(ratio, 1)
# Neue Breite und Höhe berechnen
width = int(math.floor(pixbuf.get_width()*ratio))
height = int(math.floor(pixbuf.get_height()*ratio))
if width <= 0 or height <= 0:
return
# Pixelpuffer auf Oberfläche zeichnen
self._standalone_image.set_allocation(self._standalone_scroll.get_allocation())
self._standalone_image.set_from_pixbuf(pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.HYPER))
self._standalone_image.show()
def _get_default_image(self):
return self._icon_theme.load_icon(
Window.STOCK_ICON_DEFAULT,
64,
Gtk.IconLookupFlags.FORCE_SVG & Gtk.IconLookupFlags.FORCE_SIZE
)
class StackSwitcher(GObject.GObject):
__gsignals__ = {
'stack-switched': (GObject.SIGNAL_RUN_FIRST, None, ())
}
def __init__(self, builder):
GObject.GObject.__init__(self)
self._temp_button = None
self._stack_switcher = builder.get_object('header-panelswitcher')
for child in self._stack_switcher.get_children():
if type(child) is Gtk.RadioButton:
child.connect('clicked', self.on_clicked)
def on_clicked(self, widget):
if not self._temp_button:
self._temp_button = widget
else:
self._temp_button = None
self.emit('stack-switched')
def get(self):
return self._stack_switcher