#!/usr/bin/env python3 import gi gi.require_version('Gtk', '4.0') gi.require_version('Adw', '1') try: import keyring use_keyring = True except: use_keyring = False import locale import logging from gi.repository import Gtk, Adw, Gdk, GObject, GLib, Gio from . import client #from .shortcutsdialog import ShortcutsDialog from .connectionpanel import ConnectionPanel from .serverpanel import ServerPanel from .coverpanel import CoverPanel from .playlistpanel import PlaylistPanel from .librarypanel import LibraryPanel from .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='/xyz/suruatoel/mcg/ui/window.ui') class Window(Adw.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_toast = Gtk.Template.Child() def __init__(self, app, title, settings, **kwargs): super().__init__(**kwargs) self.set_application(app) self.set_title(title) self._settings = settings self._panels = [] self._mcg = client.Client() self._state = WindowState() self._setting_volume = False self._headerbar_connection_button_active = True self._headerbar_playpause_button_active = True # FIXME 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_child(self._connection_panel) self.panel_stack.add_titled_with_icon(self._server_panel, 'server-panel', locale.gettext("Server"), "network-wired-symbolic") self.panel_stack.add_titled_with_icon(self._cover_panel, 'cover-panel', locale.gettext("Cover"), "image-x-generic-symbolic") self.panel_stack.add_titled_with_icon(self._playlist_panel, 'playlist-panel', locale.gettext("Playlist"), "view-list-symbolic") self.panel_stack.add_titled_with_icon(self._library_panel, 'library-panel', locale.gettext("Library"), "emblem-music-symbolic") # 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_child(self._server_panel.get_toolbar()) self.toolbar_stack.add_child(self._cover_panel.get_toolbar()) self.toolbar_stack.add_child(self._playlist_panel.get_toolbar()) self.toolbar_stack.add_child(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.connect("notify::default-width", self.on_resize) self.connect("notify::maximized", self.on_maximized) self.connect("notify::fullscreened", self.on_fullscreened) 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.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) self._search_library_action = Gio.SimpleAction.new("search-library", None) self._search_library_action.set_enabled(True) self._search_library_action.connect('activate', self.on_menu_search_library) self.add_action(self._search_library_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() def on_menu_search_library(self, action, value): self.panel_stack.set_visible_child(self.library_panel_page) self._library_panel.show_search() # Window callbacks def on_resize(self, widget, event): width = self.get_size(Gtk.Orientation.HORIZONTAL) height = self.get_size(Gtk.Orientation.VERTICAL) if width > 0: self._cover_panel.set_width(width) if not self._state.get_property(WindowState.IS_MAXIMIZED): self._state.set_property(WindowState.WIDTH, width) self._state.set_property(WindowState.HEIGHT, height) def on_maximized(self, widget, maximized): self._state.set_property(WindowState.IS_MAXIMIZED, maximized is True) def on_fullscreened(self, widget, fullscreened): self._fullscreen(self.is_fullscreen()) # HeaderBar callbacks @Gtk.Template.Callback() def on_headerbar_connection_state_set(self, widget, state): if self._headerbar_connection_button_active: self._connect() @Gtk.Template.Callback() def on_headerbar_volume_changed(self, widget, value): if not self._setting_volume: self._mcg.set_volume(int(value*100)) @Gtk.Template.Callback() def on_headerbar_playpause_toggled(self, widget): if self._headerbar_playpause_button_active: self._mcg.playpause() self._mcg.get_status() # 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: 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): self._cover_panel.set_albumart(album, data) self._playlist_panel.set_albumart(album, data) self._library_panel.set_albumart(album, data) def on_mcg_custom(self, name): pass """ 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) self.set_cursor(Gdk.Cursor.new_from_name("none", None)) else: self.headerbar.show() self._cover_panel.set_fullscreen(False) self.set_cursor(Gdk.Cursor.new_from_name("default", None)) 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_playpause_button_active = False self.headerbar_button_playpause.set_active(True) self._headerbar_playpause_button_active = True def _set_pause(self): self._headerbar_playpause_button_active = False self.headerbar_button_playpause.set_active(False) self._headerbar_playpause_button_active = True def _set_volume(self, volume): if volume >= 0: self.headerbar_button_volume.set_visible(True) 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_connection_button_active = False self.headerbar_button_connect.set_active(True) self.headerbar_button_connect.set_state(True) self._headerbar_connection_button_active = True def _headerbar_disconnected(self): self._headerbar_connection_button_active = False self.headerbar_button_connect.set_active(False) self.headerbar_button_connect.set_state(False) self._headerbar_connection_button_active = True 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_toast.add_toast(Adw.Toast.new(message))