From 4ced8e607b160896b00f1a087777e4729f3d0d71 Mon Sep 17 00:00:00 2001 From: Benjamin Schaaf Date: Thu, 6 Jan 2022 20:40:40 +1100 Subject: [PATCH] WIP --- sublime_music/__main__.py | 3 ++ sublime_music/app.py | 59 ++++++++++++++++++++++++++++++++----- sublime_music/ui/actions.py | 52 ++++++++++++++++++++++++-------- 3 files changed, 95 insertions(+), 19 deletions(-) diff --git a/sublime_music/__main__.py b/sublime_music/__main__.py index 6cfb557..0c1f198 100644 --- a/sublime_music/__main__.py +++ b/sublime_music/__main__.py @@ -65,3 +65,6 @@ def main(): app = SublimeMusicApp(Path(config_file)) app.run(unknown_args) + +if __name__ == '__main__': + main() diff --git a/sublime_music/app.py b/sublime_music/app.py index 85bf68c..5be2ef8 100644 --- a/sublime_music/app.py +++ b/sublime_music/app.py @@ -19,6 +19,7 @@ except Exception: tap_imported = False import gi +gi.require_version('GIRepository', '2.0') from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk, GIRepository # Temporary for development @@ -57,7 +58,7 @@ from .players import PlayerDeviceEvent, PlayerEvent, PlayerManager from .ui.configure_provider import ConfigureProviderDialog from .ui.main import MainWindow from .ui.state import RepeatType, UIState -from .ui.actions import register_action +from .ui.actions import register_action, register_dataclass_actions from .util import resolve_path @@ -75,6 +76,8 @@ class SublimeMusicApp(Gtk.Application): self.connect("shutdown", self.on_app_shutdown) + self._download_progress = {} + player_manager: Optional[PlayerManager] = None exiting: bool = False @@ -125,6 +128,7 @@ class SublimeMusicApp(Gtk.Application): add_action("go-online", self.on_go_online) add_action("refresh-devices", self.on_refresh_devices) + register_action(self, self.refresh) register_action(self, self.force_refresh) add_action( "update-play-queue-from-server", @@ -138,10 +142,11 @@ class SublimeMusicApp(Gtk.Application): self.tap.on("prev_track", self.prev_track) self.tap.start() - # self.add_accelerator("F", 'app.play-pause') - self.add_accelerator("space", 'app.play-pause') - self.add_accelerator("Home", 'app.prev-track') - self.add_accelerator("End", 'app.next-track') + # self.set_accels_for_action('app.play-pause', ["F"]) + self.set_accels_for_action('app.play-pause', ["space"]) + self.set_accels_for_action('app.prev-track', ["Home"]) + self.set_accels_for_action('app.next-track', ["End"]) + # self.set_accels_for_action('app.quit', ["space"]) def do_activate(self): # We only allow a single window and raise any existing ones @@ -169,6 +174,16 @@ class SublimeMusicApp(Gtk.Application): register_action(albums, self.albums_select_album, 'select-album') self.window.insert_action_group('albums', albums) + settings = Gio.SimpleActionGroup() + register_dataclass_actions(settings, self.app_config, after=self._save_and_refresh) + self.window.insert_action_group('settings', settings) + + players = Gio.SimpleActionGroup() + register_action(players, self.players_set_option, name='set-str-option', types=(str, str, str)) + register_action(players, self.players_set_option, name='set-bool-option', types=(str, str, bool)) + register_action(players, self.players_set_option, name='set-int-option', types=(str, str, int)) + self.window.insert_action_group('players', players) + # Configure the CSS provider so that we can style elements on the # window. css_provider = Gtk.CssProvider() @@ -606,10 +621,19 @@ class SublimeMusicApp(Gtk.Application): # for k, v in state_updates.items(): # setattr(self.app_config.state, k, v) # self.update_window(force=force) + + @dbus_propagate() + def refresh(self): + self.update_window(force=False) + @dbus_propagate() def force_refresh(self): self.update_window(force=True) + def _save_and_refresh(self): + self.app_config.save() + self.refresh() + def on_notification_closed(self, _): self.app_config.state.current_notification = None self.update_window() @@ -953,8 +977,20 @@ class SublimeMusicApp(Gtk.Application): self.update_window() def on_song_download_progress(self, song_id: str, progress: DownloadProgress): - assert self.window - GLib.idle_add(self.window.update_song_download_progress, song_id, progress) + if len(self._download_progress) == 0: + GLib.timeout_add(100, self._propagate_download_progress) + + # Amortize progress updates + self._download_progress[song_id] = progress + + def _propagate_download_progress(self): + items = list(self._download_progress.items()) + self._download_progress = {} + + for song_id, progress in items: + self.window.update_song_download_progress(song_id, progress) + + return False def on_app_shutdown(self, app: "SublimeMusicApp"): self.exiting = True @@ -994,6 +1030,15 @@ class SublimeMusicApp(Gtk.Application): self.app_config.state.selected_album_id = album_id self.update_window() + def players_set_option(self, player: str, option: str, value: Any): + self.app_config.player_config[player][option] = value + + if pm := self.player_manager: + pm.change_settings(self.app_config.player_config) + + self.app_config.save() + self.update_window() + # ########## HELPER METHODS ########## # def show_configure_servers_dialog( diff --git a/sublime_music/ui/actions.py b/sublime_music/ui/actions.py index aff8a5c..bcb1991 100644 --- a/sublime_music/ui/actions.py +++ b/sublime_music/ui/actions.py @@ -1,7 +1,8 @@ import inspect import enum import dataclasses -from typing import Callable, Optional, Tuple, Any, Union +import pathlib +from typing import Callable, Optional, Tuple, Any, Union, List, Type from gi.repository import Gio, GLib @@ -10,7 +11,7 @@ NoneType = type(None) def run_action(widget, name, *args): - # print('run action', name, args) + print('run action', name, args) group, action = name.split('.') action_group = widget.get_action_group(group) @@ -29,24 +30,43 @@ def run_action(widget, name, *args): action_group.activate_action(action, param) -def register_action(group, fn: Callable, name: Optional[str] = None): +def register_dataclass_actions(group, data, after=None): + fields = dataclasses.fields(type(data)) + + for field in fields: + if field.name[0] == '_': + continue + + def set_field(value, name=field.name): + setattr(data, name, value) + if after: + after() + + name = field.name.replace('_', '-') + try: + register_action(group, set_field, name=f'set-{name}', types=(field.type,)) + except ValueError: + continue + + +def register_action(group, fn: Callable, name: Optional[str] = None, types: Tuple[Type] = None): if name is None: name = fn.__name__.replace('_', '-') # Determine the type from the signature signature = inspect.signature(fn) + if types is None: + types = tuple(p.annotation for p in signature.parameters.values()) if signature.parameters: - arg_types = tuple(p.annotation for p in signature.parameters.values()) - - if inspect.Parameter.empty in arg_types: + if inspect.Parameter.empty in types: raise ValueError('Missing parameter annotation for action ' + name) - has_multiple = len(arg_types) > 1 + has_multiple = len(types) > 1 if has_multiple: - param_type = Tuple.__getitem__(arg_types) + param_type = Tuple.__getitem__(types) else: - param_type = arg_types[0] + param_type = types[0] type_str = variant_type_from_python(param_type) var_type = GLib.VariantType(type_str) @@ -59,7 +79,7 @@ def register_action(group, fn: Callable, name: Optional[str] = None): action = Gio.SimpleAction.new(name, var_type) def activate(action, param): - if param: + if param is not None: if has_multiple: fn(*build(param.unpack())) else: @@ -85,6 +105,8 @@ def variant_type_from_python(py_type: type) -> str: return 's' elif py_type is Any: return 'v' + elif isinstance(py_type, type) and issubclass(py_type, pathlib.PurePath): + return 's' elif isinstance(py_type, type) and issubclass(py_type, enum.Enum): return variant_type_from_python(type(list(py_type)[0].value)) elif dataclasses.is_dataclass(py_type): @@ -119,7 +141,9 @@ def variant_type_from_python(py_type: type) -> str: def unbuilt_type(py_type: type) -> type: - if isinstance(py_type, type) and issubclass(py_type, enum.Enum): + if isinstance(py_type, type) and issubclass(py_type, pathlib.PurePath): + return str + elif isinstance(py_type, type) and issubclass(py_type, enum.Enum): return type(list(py_type)[0].value) elif dataclasses.is_dataclass(py_type): return tuple @@ -132,7 +156,9 @@ def generate_build_function(py_type: type) -> Optional[Callable]: unpacking a GVariant. When no reconstruction is needed None is returned. """ - if isinstance(py_type, type) and issubclass(py_type, enum.Enum): + if isinstance(py_type, type) and issubclass(py_type, pathlib.PurePath): + return py_type + elif isinstance(py_type, type) and issubclass(py_type, enum.Enum): return py_type elif dataclasses.is_dataclass(py_type): types = tuple(f.type for f in dataclasses.fields(py_type)) @@ -214,6 +240,8 @@ from gi._gi import variant_type_from_string def _create_variant(type_str, value): if isinstance(value, enum.Enum): value = value.value + elif isinstance(value, pathlib.PurePath): + value = str(value) elif dataclasses.is_dataclass(type(value)): fields = dataclasses.fields(type(value)) value = tuple(getattr(value, field.name) for field in fields)