#!/usr/bin/env python3 import gi gi.require_version('Gtk', '3.0') try: import keyring use_keyring = True except: use_keyring = False import logging from gi.repository import Gtk, Gdk, GObject, GLib, Gio from mcg import client from mcg.shortcutsdialog import ShortcutsDialog from mcg.connectionpanel import ConnectionPanel from mcg.serverpanel import ServerPanel from mcg.coverpanel import CoverPanel from mcg.playlistpanel import PlaylistPanel from mcg.librarypanel import LibraryPanel from mcg.zeroconf import ZeroconfProvider 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): super().__init__() @Gtk.Template(resource_path='/de/coderkun/mcg/ui/window.ui') class Window(Gtk.ApplicationWindow): __gtype_name__ = 'McgAppWindow' SETTING_HOST = 'host' SETTING_PORT = 'port' SETTING_CONNECTED = 'connected' 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' _CUSTOM_STARTUP_COMPLETE = 'startup-complete' # Widgets content_stack = Gtk.Template.Child() panel_stack = Gtk.Template.Child() toolbar_stack = Gtk.Template.Child() # Headerbar headerbar = Gtk.Template.Child() headerbar_title_stack = Gtk.Template.Child() headerbar_panel_switcher = Gtk.Template.Child() headerbar_connection_label = Gtk.Template.Child() headerbar_button_connect = Gtk.Template.Child() headerbar_button_playpause = Gtk.Template.Child() headerbar_button_volume = Gtk.Template.Child() # Infobar info_revealer = Gtk.Template.Child() info_bar = Gtk.Template.Child() info_label = Gtk.Template.Child() def __init__(self, app, title, settings): super().__init__() self.set_application(app) self.set_title(title) self._settings = settings self._panels = [] self._mcg = client.Client() self._state = WindowState() self._changing_volume = False self._setting_volume = False # Help/Shortcuts dialog self.set_help_overlay(ShortcutsDialog()) # Login screen self._connection_panel = ConnectionPanel() # Server panel self._server_panel = ServerPanel() self._panels.append(self._server_panel) # Cover panel self._cover_panel = CoverPanel() self._panels.append(self._cover_panel) # Playlist panel self._playlist_panel = PlaylistPanel(self._mcg) self._playlist_panel.connect('open-standalone', self.on_panel_open_standalone) self._playlist_panel.connect('close-standalone', self.on_panel_close_standalone) self._panels.append(self._playlist_panel) # Library panel self._library_panel = LibraryPanel(self._mcg) self._library_panel.connect('open-standalone', self.on_panel_open_standalone) self._library_panel.connect('close-standalone', self.on_panel_close_standalone) self._panels.append(self._library_panel) # Stack self.content_stack.add(self._connection_panel) self.panel_stack.add_titled(self._server_panel, 'server-panel', "Server") self.panel_stack.add_titled(self._cover_panel, 'cover-panel', "Cover") self.panel_stack.add_titled(self._playlist_panel, 'playlist-panel', "Playlist") self.panel_stack.add_titled(self._library_panel, 'library-panel', "Library") # Header self._playlist_panel.get_headerbar_standalone().connect('close', self.on_panel_close_standalone) self._library_panel.get_headerbar_standalone().connect('close', self.on_panel_close_standalone) # Toolbar stack self.toolbar_stack.add(self._server_panel.get_toolbar()) self.toolbar_stack.add(self._cover_panel.get_toolbar()) self.toolbar_stack.add(self._playlist_panel.get_toolbar()) self.toolbar_stack.add(self._library_panel.get_toolbar()) # Properties self._set_headerbar_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._playlist_panel.set_item_size(self._settings.get_int(Window.SETTING_ITEM_SIZE)) self._library_panel.set_item_size(self._settings.get_int(Window.SETTING_ITEM_SIZE)) self._library_panel.set_sort_order(self._settings.get_enum(Window.SETTING_SORT_ORDER)) self._library_panel.set_sort_type(self._settings.get_boolean(Window.SETTING_SORT_TYPE)) # Signals self._connection_panel.connect('connection-changed', self.on_connection_panel_connection_changed) self.panel_stack.connect('notify::visible-child', self.on_stack_switched) self._server_panel.connect('change-output-device', self.on_server_panel_output_device_changed) self._cover_panel.connect('toggle-fullscreen', self.on_cover_panel_toggle_fullscreen) self._cover_panel.connect('set-song', self.on_cover_panel_set_song) self._cover_panel.connect('albumart', self.on_cover_panel_albumart) self._playlist_panel.connect('clear-playlist', self.on_playlist_panel_clear_playlist) self._playlist_panel.connect('remove-album', self.on_playlist_panel_remove) self._playlist_panel.connect('remove-multiple-albums', self.on_playlist_panel_remove_multiple) self._playlist_panel.connect('play', self.on_playlist_panel_play) self._playlist_panel.connect('albumart', self.on_playlist_panel_albumart) self._library_panel.connect('update', self.on_library_panel_update) self._library_panel.connect('play', self.on_library_panel_play) self._library_panel.connect('queue', self.on_library_panel_queue) self._library_panel.connect('queue-multiple', self.on_library_panel_queue_multiple) self._library_panel.connect('item-size-changed', self.on_library_panel_item_size_changed) self._library_panel.connect('sort-order-changed', self.on_library_panel_sort_order_changed) self._library_panel.connect('sort-type-changed', self.on_library_panel_sort_type_changed) self._library_panel.connect('albumart', self.on_library_panel_albumart) 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_PULSE_ALBUMS, self.on_mcg_pulse_albums) self._mcg.connect_signal(client.Client.SIGNAL_INIT_ALBUMS, self.on_mcg_init_albums) self._mcg.connect_signal(client.Client.SIGNAL_LOAD_ALBUMS, self.on_mcg_load_albums) self._mcg.connect_signal(client.Client.SIGNAL_LOAD_ALBUMART, self.on_mcg_load_albumart) 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) # Actions self.set_default_size(self._state.width, self._state.height) if self._state.get_property(WindowState.IS_MAXIMIZED): self.maximize() self.show_all() self.content_stack.set_visible_child(self._connection_panel) 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.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.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.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.add_action(self._panel_action) self._toggle_fullscreen_action = Gio.SimpleAction.new("toggle-fullscreen", None) self._toggle_fullscreen_action.set_enabled(True) self._toggle_fullscreen_action.connect('activate', self.on_menu_toggle_fullscreen) self.add_action(self._toggle_fullscreen_action) # Menu callbacks 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.panel_stack.set_visible_child(self._panels[int(value.get_string())]) def on_menu_toggle_fullscreen(self, action, value): self.panel_stack.set_visible_child(self._cover_panel) if not self._state.get_property(WindowState.IS_FULLSCREENED): self.fullscreen() else: self.unfullscreen() # Window callbacks @Gtk.Template.Callback() def on_resize(self, widget, event): if not self._state.get_property(WindowState.IS_MAXIMIZED): size = self.get_size() self._state.set_property(WindowState.WIDTH, size.width) self._state.set_property(WindowState.HEIGHT, size.height) @Gtk.Template.Callback() 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)) # HeaderBar callbacks @Gtk.Template.Callback() def on_headerbar_connection_active_notify(self, widget, status): self._connect() @Gtk.Template.Callback() def on_headerbar_connection_state_set(self, widget, state): return True @Gtk.Template.Callback() def on_headerbar_volume_press(self, widget, active): self._changing_volume = active @Gtk.Template.Callback() def on_headerbar_volume_release(self, widget, active): self._changing_volume = active @Gtk.Template.Callback() def on_headerbar_playpause_toggled(self, widget): self._mcg.playpause() self._mcg.get_status() @Gtk.Template.Callback() def on_headerbar_volume_changed(self, widget, value): if not self._setting_volume: self._mcg.set_volume(int(value*100)) # Infobar callback @Gtk.Template.Callback() def on_info_bar_close(self, *args): self.info_revealer.set_reveal_child(False) @Gtk.Template.Callback() def on_info_bar_response(self, widget, response): self.info_revealer.set_reveal_child(False) # Panel callbacks def on_stack_switched(self, widget, prop): self._set_visible_toolbar() self._save_visible_panel() self._set_menu_visible_panel() for panel in self._panels: panel.set_selected(panel == self.panel_stack.get_visible_child()) GObject.idle_add( self.panel_stack.child_set_property, self.panel_stack.get_visible_child(), 'needs-attention', False ) def on_panel_open_standalone(self, panel): self.set_titlebar(panel.get_headerbar_standalone()) def on_panel_close_standalone(self, headerbar): self.set_titlebar(self.headerbar) def on_connection_panel_connection_changed(self, widget, host, port, password): 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) 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_playlist_panel_albumart(self, widget, album): self._mcg.get_albumart(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.fullscreen() else: self.unfullscreen() def on_cover_panel_set_song(self, widget, pos, time): self._mcg.seek(pos, time) def on_cover_panel_albumart(self, widget, album): self._mcg.get_albumart(album) 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._playlist_panel.set_item_size(size) self._settings.set_int(Window.SETTING_ITEM_SIZE, self._library_panel.get_item_size()) def on_library_panel_sort_order_changed(self, widget, sort_order): self._settings.set_enum(Window.SETTING_SORT_ORDER, self._library_panel.get_sort_order()) def on_library_panel_sort_type_changed(self, widget, sort_type): self._settings.set_boolean(Window.SETTING_SORT_TYPE, self._library_panel.get_sort_type()) def on_library_panel_albumart(self, widget, album): self._mcg.get_albumart(album) # 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._cover_panel.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._set_play) GObject.idle_add(self._cover_panel.set_play, pos, time) self._play_action.set_state(GLib.Variant.new_boolean(True)) elif state == 'pause' or state == 'stop': GObject.idle_add(self._set_pause) GObject.idle_add(self._cover_panel.set_pause) self._play_action.set_state(GLib.Variant.new_boolean(False)) # Volume GObject.idle_add(self._set_volume, volume) # Status self._server_panel.set_status(file, audio, bitrate, error) # Error if error is None: self.info_revealer.set_reveal_child(False) else: self._show_error(error) def on_mcg_stats(self, artists, albums, songs, dbplaytime, playtime, uptime): self._server_panel.set_stats(artists, albums, songs, dbplaytime, playtime, uptime) def on_mcg_load_output_devices(self, devices): self._server_panel.set_output_devices(devices) def on_mcg_load_playlist(self, playlist): self._playlist_panel.set_playlist(self._connection_panel.get_host(), playlist) def on_mcg_init_albums(self): GObject.idle_add(self._library_panel.init_albums) def on_mcg_pulse_albums(self): GObject.idle_add(self._library_panel.load_albums) def on_mcg_load_albums(self, albums): self._library_panel.set_albums(self._connection_panel.get_host(), albums) def on_mcg_load_albumart(self, album, data): GObject.idle_add(self._cover_panel.set_albumart, album, data) GObject.idle_add(self._playlist_panel.set_albumart, album, data) GObject.idle_add(self._library_panel.set_albumart, album, data) def on_mcg_custom(self, name): if name == Window._CUSTOM_STARTUP_COMPLETE: for panel in self._panels: GObject.idle_add( self.panel_stack.child_set_property, panel, '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.panel_stack.set_visible_child(self._panels[panel_index]) def on_settings_item_size_changed(self, settings, key): size = settings.get_int(key) self._playlist_panel.set_item_size(size) self._library_panel.set_item_size(size) def on_settings_sort_order_changed(self, settings, key): sort_order = settings.get_enum(key) self._library_panel.set_sort_order(sort_order) def on_settings_sort_type_changed(self, settings, key): sort_type = settings.get_boolean(key) self._library_panel.set_sort_type(sort_type) # Private methods def _connect(self): self._connection_panel.set_sensitive(False) self._set_headerbar_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() self._mcg.connect(host, port, password) self._settings.set_boolean(Window.SETTING_CONNECTED, True) def _connect_connected(self): self._headerbar_connected() self._set_headerbar_sensitive(True, False) self.content_stack.set_visible_child(self.panel_stack) self.panel_stack.set_visible_child(self._panels[self._settings.get_int(Window.SETTING_PANEL)]) def _connect_disconnected(self): self._playlist_panel.stop_threads(); self._library_panel.stop_threads(); self._headerbar_disconnected() self._set_headerbar_sensitive(False, False) self._save_visible_panel() self.content_stack.set_visible_child(self._connection_panel) self._connection_panel.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.headerbar.hide() self._cover_panel.set_fullscreen(True) # Hide cursor self.get_window().set_cursor( Gdk.Cursor.new_from_name(Gdk.Display.get_default(), "none") ) else: self.headerbar.show() self._cover_panel.set_fullscreen(False) # Reset cursor self.get_window().set_cursor( Gdk.Cursor.new_from_name(Gdk.Display.get_default(), "default") ) def _save_visible_panel(self): panel_index_selected = self._panels.index(self.panel_stack.get_visible_child()) self._settings.set_int(Window.SETTING_PANEL, panel_index_selected) def _set_menu_visible_panel(self): panel_index_selected = self._panels.index(self.panel_stack.get_visible_child()) self._panel_action.set_state(GLib.Variant.new_string(str(panel_index_selected))) def _set_visible_toolbar(self): panel_index_selected = self._panels.index(self.panel_stack.get_visible_child()) toolbar = self._panels[panel_index_selected].get_toolbar() self.toolbar_stack.set_visible_child(toolbar) def _set_play(self): self.headerbar_button_playpause.handler_block_by_func( self.on_headerbar_playpause_toggled ) self.headerbar_button_playpause.set_active(True) self.headerbar_button_playpause.handler_unblock_by_func( self.on_headerbar_playpause_toggled ) def _set_pause(self): self.headerbar_button_playpause.handler_block_by_func( self.on_headerbar_playpause_toggled ) self.headerbar_button_playpause.set_active(False) self.headerbar_button_playpause.handler_unblock_by_func( self.on_headerbar_playpause_toggled ) def _set_volume(self, volume): if volume >= 0: self.headerbar_button_volume.set_visible(True) if not self._changing_volume: self._setting_volume = True self.headerbar_button_volume.set_value(volume / 100) self._setting_volume = False else: self.headerbar_button_volume.set_visible(False) def _headerbar_connected(self): self.headerbar_button_connect.handler_block_by_func( self.on_headerbar_connection_active_notify ) self.headerbar_button_connect.set_active(True) self.headerbar_button_connect.set_state(True) self.headerbar_button_connect.handler_unblock_by_func( self.on_headerbar_connection_active_notify ) self.headerbar_title_stack.set_visible_child(self.headerbar_panel_switcher) def _headerbar_disconnected(self): self.headerbar_button_connect.handler_block_by_func( self.on_headerbar_connection_active_notify ) self.headerbar_button_connect.set_active(False) self.headerbar_button_connect.set_state(False) self.headerbar_button_connect.handler_unblock_by_func( self.on_headerbar_connection_active_notify ) self.headerbar_title_stack.set_visible_child(self.headerbar_connection_label) def _set_headerbar_sensitive(self, sensitive, connecting): self.headerbar_button_playpause.set_sensitive(sensitive) self.headerbar_button_volume.set_sensitive(sensitive) self.headerbar_panel_switcher.set_sensitive(sensitive) self.headerbar_button_connect.set_sensitive(not connecting) def _show_error(self, message): self.info_bar.set_message_type(Gtk.MessageType.ERROR) self.info_label.set_text(message) self.info_revealer.set_reveal_child(True)