diff --git a/sublime_music/app.py b/sublime_music/app.py index 3c39ac4..149be08 100644 --- a/sublime_music/app.py +++ b/sublime_music/app.py @@ -18,11 +18,13 @@ try: except Exception: tap_imported = False +import gi from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gtk -try: - import gi +gi.require_version('Handy', '1') +from gi.repository import Handy +try: gi.require_version("Notify", "0.7") from gi.repository import Notify @@ -59,6 +61,8 @@ class SublimeMusicApp(Gtk.Application): if glib_notify_exists: Notify.init("Sublime Music") + Handy.init() + self.window: Optional[Gtk.Window] = None self.app_config = AppConfiguration.load_from_file(config_file) self.dbus_manager: Optional[DBusManager] = None diff --git a/sublime_music/ui/albums.py b/sublime_music/ui/albums.py index caf5330..0d4d29f 100644 --- a/sublime_music/ui/albums.py +++ b/sublime_music/ui/albums.py @@ -4,7 +4,7 @@ import logging import math from typing import Any, Callable, cast, Iterable, List, Optional, Tuple -from gi.repository import Gdk, Gio, GLib, GObject, Gtk, Pango +from gi.repository import Gdk, Gio, GLib, GObject, Gtk, Pango, Handy from ..adapters import ( AdapterManager, @@ -71,6 +71,8 @@ class AlbumsPanel(Gtk.Box): def __init__(self): super().__init__(orientation=Gtk.Orientation.VERTICAL) + leaflet = Handy.Leaflet(transition_type=Handy.LeafletTransitionType.SLIDE, can_swipe_forward=False) + actionbar = Gtk.ActionBar() # Sort by @@ -160,6 +162,7 @@ class AlbumsPanel(Gtk.Box): actionbar.pack_end(Gtk.Label(label="Show")) # self.add(actionbar) + leaflet.add(actionbar) scrolled_window = Gtk.ScrolledWindow() self.grid = AlbumsGrid() diff --git a/sublime_music/ui/main.py b/sublime_music/ui/main.py index 30eb4a8..9430f92 100644 --- a/sublime_music/ui/main.py +++ b/sublime_music/ui/main.py @@ -4,10 +4,6 @@ from typing import Any, Callable, Dict, Optional, Set, Tuple import bleach -import gi -from gi.repository import GIRepository -GIRepository.Repository.prepend_search_path('/usr/local/lib/x86_64-linux-gnu/girepository-1.0') -gi.require_version('Handy', '1') from gi.repository import Gdk, GLib, GObject, Gtk, Pango, Handy from ..adapters import ( @@ -19,7 +15,7 @@ from ..adapters import ( from ..config import AppConfiguration, ProviderConfiguration from ..players import PlayerManager from ..ui import albums, artists, browse, player_controls, playlists, util -from ..ui.common import IconButton, IconMenuButton, SpinnerImage +from ..ui.common import IconButton, IconToggleButton, IconMenuButton, SpinnerImage class MainWindow(Gtk.ApplicationWindow): @@ -49,10 +45,27 @@ class MainWindow(Gtk.ApplicationWindow): _pending_downloads_label: Optional[Gtk.Label] = None _current_downloads_placeholder: Optional[Gtk.Label] = None + # Settings + setting_song_play_notification = GObject.Property(type=bool, default=False) + setting_allow_song_downloads = GObject.Property(type=bool, default=False) + setting_download_on_stream = GObject.Property(type=bool, default=False) + setting_prefetch_amount = GObject.Property(type=int, default=0) + setting_concurrent_download_limit = GObject.Property(type=int, default=0) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # self.set_default_size(1342, 756) + def connect_setting(setting): + prop = setting.replace("_", "-") + self.connect(f"notify::setting-{prop}", + partial(self._on_settings_change, setting)) + connect_setting("song_play_notification") + connect_setting("allow_song_downloads") + connect_setting("download_on_stream") + connect_setting("prefetch_amount") + connect_setting("concurrent_download_limit") + # Create the stack self.albums_panel = albums.AlbumsPanel() self.artists_panel = artists.ArtistsPanel() @@ -60,16 +73,22 @@ class MainWindow(Gtk.ApplicationWindow): self.playlists_panel = playlists.PlaylistsPanel() self.stack = self._create_stack( Albums=self.albums_panel, - # Artists=self.artists_panel, - # Browse=self.browse_panel, - # Playlists=self.playlists_panel, + Artists=self.artists_panel, + Browse=self.browse_panel, + Playlists=self.playlists_panel, ) self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) - self.titlebar = self._create_headerbar(self.stack) - # self.set_titlebar(self.titlebar) + self.sidebar_flap = Handy.Flap( + orientation=Gtk.Orientation.HORIZONTAL, + fold_policy=Handy.FlapFoldPolicy.ALWAYS, + transition_type=Handy.FlapTransitionType.OVER, + modal=True) - flap = Handy.Flap(orientation=Gtk.Orientation.VERTICAL, fold_policy=Handy.FlapFoldPolicy.ALWAYS, flap_position=Gtk.PackType.END) + self.titlebar = self._create_headerbar(self.stack) + self.set_titlebar(self.titlebar) + + drawer = Handy.Flap(orientation=Gtk.Orientation.VERTICAL, fold_policy=Handy.FlapFoldPolicy.ALWAYS, flap_position=Gtk.PackType.END) notification_container = Gtk.Overlay() notification_container.add(self.stack) @@ -96,7 +115,7 @@ class MainWindow(Gtk.ApplicationWindow): self.notification_revealer.add(notification_box) notification_container.add_overlay(self.notification_revealer) - flap.set_content(notification_container) + drawer.set_content(notification_container) # Player state self.player_manager = player_controls.Manager() @@ -112,7 +131,7 @@ class MainWindow(Gtk.ApplicationWindow): ) # Player Controls - flap.set_separator(Gtk.Separator()) + drawer.set_separator(Gtk.Separator()) squeezer = Handy.Squeezer(vexpand=True, homogeneous=False) @@ -121,13 +140,13 @@ class MainWindow(Gtk.ApplicationWindow): mobile_handle = player_controls.MobileHandle(self.player_manager) - # Toggle flap when handle is pressed - def toggle_flap(handle, event): - if event.get_click_count() != (True, 1) or not flap.get_swipe_to_open(): + # Toggle drawer when handle is pressed + def toggle_drawer(handle, event): + if event.get_click_count() != (True, 1) or not drawer.get_swipe_to_open(): return - flap.set_reveal_flap(not flap.get_reveal_flap()) - mobile_handle.connect("button-press-event", toggle_flap) + drawer.set_reveal_flap(not drawer.get_reveal_flap()) + mobile_handle.connect("button-press-event", toggle_drawer) squeezer.add(mobile_handle) @@ -135,27 +154,31 @@ class MainWindow(Gtk.ApplicationWindow): def squeezer_changed(squeezer, _): is_desktop = squeezer.get_visible_child() == desktop_controls - flap.set_swipe_to_open(not is_desktop) + drawer.set_swipe_to_open(not is_desktop) # When transitioning, don't play the reveal animation - dur = flap.get_reveal_duration() - flap.set_reveal_duration(0) + dur = drawer.get_reveal_duration() + drawer.set_reveal_duration(0) - flap.set_reveal_flap(False) + drawer.set_reveal_flap(False) - flap.set_reveal_duration(dur) + drawer.set_reveal_duration(dur) squeezer.connect("notify::visible-child", squeezer_changed) - flap.set_handle(squeezer) + drawer.set_handle(squeezer) mobile_flap = player_controls.MobileFlap(self.player_manager) - flap.set_flap(mobile_flap) + drawer.set_flap(mobile_flap) - def flap_reveal_progress(flap, _): - self.player_manager.flap_open = flap.get_reveal_progress() > 0.5 - flap.connect("notify::reveal-progress", flap_reveal_progress) + def drawer_reveal_progress(drawer, _): + self.player_manager.flap_open = drawer.get_reveal_progress() > 0.5 + drawer.connect("notify::reveal-progress", drawer_reveal_progress) - self.add(flap) + self.sidebar_flap.set_content(drawer) + + self.sidebar_flap.set_flap(self._create_sidebar()) + + self.add(self.sidebar_flap) self.connect("button-release-event", self._on_button_release) @@ -254,156 +277,147 @@ class MainWindow(Gtk.ApplicationWindow): self.provider_options_box.show_all() - # Main Settings - self.notification_switch.set_active(app_config.song_play_notification) - - # Player settings - for c in self.player_settings_box.get_children(): - self.player_settings_box.remove(c) - - def emit_player_settings_change( - player_name: str, option_name: str, value_extraction_fn: Callable, *args - ): - if self._updating_settings: - return - self.emit( - "refresh-window", - { - "__player_setting__": ( - player_name, - option_name, - value_extraction_fn(*args), - ) - }, - False, - ) - - for player_name, options in player_manager.get_configuration_options().items(): - self.player_settings_box.add(Gtk.Separator()) - self.player_settings_box.add( - self._create_label( - f"{player_name} Settings", name="menu-settings-separator" - ) - ) - - for option_name, descriptor in options.items(): - setting_box = Gtk.Box() - setting_box.add(option_name_label := Gtk.Label(label=option_name)) - option_name_label.get_style_context().add_class("menu-label") - - option_value = app_config.player_config.get(player_name, {}).get( - option_name - ) - - if type(descriptor) == tuple: - option_store = Gtk.ListStore(str) - for option in descriptor: - option_store.append([option]) - combo = Gtk.ComboBox.new_with_model(option_store) - combo.set_id_column(0) - renderer_text = Gtk.CellRendererText() - combo.pack_start(renderer_text, True) - combo.add_attribute(renderer_text, "text", 0) - combo.set_active_id(option_value) - combo.connect( - "changed", - partial( - emit_player_settings_change, - player_name, - option_name, - lambda c: c.get_active_id(), - ), - ) - - setting_box.pack_end(combo, False, False, 0) - - elif descriptor == bool: - switch = Gtk.Switch(active=option_value) - switch.connect( - "notify::active", - partial( - emit_player_settings_change, - player_name, - option_name, - lambda s, _: s.get_active(), - ), - ) - setting_box.pack_end(switch, False, False, 0) - - elif descriptor == int: - int_editor_box = Gtk.Box() - - def restrict_to_ints( - entry: Gtk.Entry, text: str, length: int, position: int - ) -> bool: - if self._updating_settings: - return False - if not text.isdigit(): - entry.emit_stop_by_name("insert-text") - return True - return False - - entry = Gtk.Entry(width_chars=8, text=option_value, sensitive=False) - entry.connect("insert-text", restrict_to_ints) - int_editor_box.add(entry) - - buttons_box = Gtk.Box() - - edit_button = IconButton("document-edit-symbolic", relief=True) - confirm_button = IconButton("object-select-symbolic", relief=True) - cancel_button = IconButton("process-stop-symbolic", relief=True) - - def on_edit_button_click(*a): - entry.set_sensitive(True) - buttons_box.remove(edit_button) - buttons_box.add(cancel_button) - buttons_box.add(confirm_button) - buttons_box.show_all() - - def on_cancel_button_click(*a): - entry.set_text(str(option_value)) - entry.set_sensitive(False) - buttons_box.remove(cancel_button) - buttons_box.remove(confirm_button) - buttons_box.add(edit_button) - buttons_box.show_all() - - edit_button.connect("clicked", on_edit_button_click) - confirm_button.connect( - "clicked", - partial( - emit_player_settings_change, - player_name, - option_name, - lambda b: int(entry.get_text()), - ), - ) - cancel_button.connect("clicked", on_cancel_button_click) - buttons_box.add(edit_button) - - int_editor_box.add(buttons_box) - - setting_box.pack_end(int_editor_box, False, False, 0) - - setting_box.get_style_context().add_class("menu-button") - self.player_settings_box.add(setting_box) - - self.player_settings_box.show_all() - - # Download Settings - allow_song_downloads = app_config.allow_song_downloads - self.allow_song_downloads_switch.set_active(allow_song_downloads) - self.download_on_stream_switch.set_active(app_config.download_on_stream) - self.prefetch_songs_entry.set_value(app_config.prefetch_amount) - self.max_concurrent_downloads_entry.set_value( - app_config.concurrent_download_limit - ) - self.download_on_stream_switch.set_sensitive(allow_song_downloads) - self.prefetch_songs_entry.set_sensitive(allow_song_downloads) - self.max_concurrent_downloads_entry.set_sensitive(allow_song_downloads) + self.setting_song_play_notification = app_config.song_play_notification + self.setting_allow_song_downloads = app_config.allow_song_downloads + self.setting_download_on_stream = app_config.download_on_stream + self.setting_prefetch_amount = app_config.prefetch_amount + self.setting_concurrent_download_limit = app_config.concurrent_download_limit self._updating_settings = False + # Player settings + # for c in self.player_settings_box.get_children(): + # self.player_settings_box.remove(c) + + # def emit_player_settings_change( + # player_name: str, option_name: str, value_extraction_fn: Callable, *args + # ): + # if self._updating_settings: + # return + # self.emit( + # "refresh-window", + # { + # "__player_setting__": ( + # player_name, + # option_name, + # value_extraction_fn(*args), + # ) + # }, + # False, + # ) + + # for player_name, options in player_manager.get_configuration_options().items(): + # self.player_settings_box.add(Gtk.Separator()) + # self.player_settings_box.add( + # self._create_label( + # f"{player_name} Settings", name="menu-settings-separator" + # ) + # ) + + # for option_name, descriptor in options.items(): + # setting_box = Gtk.Box() + # setting_box.add(option_name_label := Gtk.Label(label=option_name)) + # option_name_label.get_style_context().add_class("menu-label") + + # option_value = app_config.player_config.get(player_name, {}).get( + # option_name + # ) + + # if type(descriptor) == tuple: + # option_store = Gtk.ListStore(str) + # for option in descriptor: + # option_store.append([option]) + # combo = Gtk.ComboBox.new_with_model(option_store) + # combo.set_id_column(0) + # renderer_text = Gtk.CellRendererText() + # combo.pack_start(renderer_text, True) + # combo.add_attribute(renderer_text, "text", 0) + # combo.set_active_id(option_value) + # combo.connect( + # "changed", + # partial( + # emit_player_settings_change, + # player_name, + # option_name, + # lambda c: c.get_active_id(), + # ), + # ) + + # setting_box.pack_end(combo, False, False, 0) + + # elif descriptor == bool: + # switch = Gtk.Switch(active=option_value) + # switch.connect( + # "notify::active", + # partial( + # emit_player_settings_change, + # player_name, + # option_name, + # lambda s, _: s.get_active(), + # ), + # ) + # setting_box.pack_end(switch, False, False, 0) + + # elif descriptor == int: + # int_editor_box = Gtk.Box() + + # def restrict_to_ints( + # entry: Gtk.Entry, text: str, length: int, position: int + # ) -> bool: + # if self._updating_settings: + # return False + # if not text.isdigit(): + # entry.emit_stop_by_name("insert-text") + # return True + # return False + + # entry = Gtk.Entry(width_chars=8, text=option_value, sensitive=False) + # entry.connect("insert-text", restrict_to_ints) + # int_editor_box.add(entry) + + # buttons_box = Gtk.Box() + + # edit_button = IconButton("document-edit-symbolic", relief=True) + # confirm_button = IconButton("object-select-symbolic", relief=True) + # cancel_button = IconButton("process-stop-symbolic", relief=True) + + # def on_edit_button_click(*a): + # entry.set_sensitive(True) + # buttons_box.remove(edit_button) + # buttons_box.add(cancel_button) + # buttons_box.add(confirm_button) + # buttons_box.show_all() + + # def on_cancel_button_click(*a): + # entry.set_text(str(option_value)) + # entry.set_sensitive(False) + # buttons_box.remove(cancel_button) + # buttons_box.remove(confirm_button) + # buttons_box.add(edit_button) + # buttons_box.show_all() + + # edit_button.connect("clicked", on_edit_button_click) + # confirm_button.connect( + # "clicked", + # partial( + # emit_player_settings_change, + # player_name, + # option_name, + # lambda b: int(entry.get_text()), + # ), + # ) + # cancel_button.connect("clicked", on_cancel_button_click) + # buttons_box.add(edit_button) + + # int_editor_box.add(buttons_box) + + # setting_box.pack_end(int_editor_box, False, False, 0) + + # setting_box.get_style_context().add_class("menu-button") + # self.player_settings_box.add(setting_box) + + # self.player_settings_box.show_all() + self.stack.set_visible_child_name(app_config.state.current_tab) active_panel = self.stack.get_visible_child() @@ -556,7 +570,7 @@ class MainWindow(Gtk.ApplicationWindow): ) def _create_stack(self, **kwargs: Gtk.Widget) -> Gtk.Stack: - stack = Gtk.Stack() + stack = Gtk.Stack(homogeneous=True) for name, child in kwargs.items(): child.connect( "song-clicked", @@ -573,9 +587,12 @@ class MainWindow(Gtk.ApplicationWindow): """ Configure the header bar for the window. """ - header = Handy.HeaderBar() - header.set_show_close_button(True) - header.props.title = "Sublime Music" + squeezer = Handy.Squeezer() + + # Desktop header + desktop_header = Handy.HeaderBar() + desktop_header.set_show_close_button(True) + desktop_header.props.title = "Sublime Music" # Search self.search_entry = Gtk.SearchEntry(placeholder_text="Search everything...") @@ -586,14 +603,14 @@ class MainWindow(Gtk.ApplicationWindow): self.search_entry.connect("focus-out-event", self._on_search_entry_loose_focus) self.search_entry.connect("changed", self._on_search_entry_changed) self.search_entry.connect("stop-search", self._on_search_entry_stop_search) - header.pack_start(self.search_entry) + # desktop_header.pack_start(self.search_entry) # Search popup self._create_search_popup() # Stack switcher switcher = Gtk.StackSwitcher(stack=stack) - header.set_custom_title(switcher) + desktop_header.set_custom_title(switcher) button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5) @@ -608,16 +625,14 @@ class MainWindow(Gtk.ApplicationWindow): self.downloads_popover.set_relative_to(self.downloads_menu_button) button_box.add(self.downloads_menu_button) - # Menu button - self.main_menu_popover = self._create_main_menu() - main_menu_button = IconMenuButton( + # Preferences + preferences_button = IconButton( "emblem-system-symbolic", tooltip_text="Open Sublime Music settings", - popover=self.main_menu_popover, + relief=True, ) - main_menu_button.connect("clicked", self._on_main_menu_clicked) - self.main_menu_popover.set_relative_to(main_menu_button) - button_box.add(main_menu_button) + preferences_button.connect("clicked", self._show_settings) + button_box.add(preferences_button) # Server icon and change server dropdown self.server_connection_popover = self._create_server_connection_popover() @@ -634,9 +649,56 @@ class MainWindow(Gtk.ApplicationWindow): ) button_box.add(self.server_connection_menu_button) - header.pack_end(button_box) + desktop_header.pack_end(button_box) - return header + squeezer.add(desktop_header) + + # Mobile header + mobile_header = Handy.HeaderBar() + mobile_header.set_show_close_button(True) + + button = IconToggleButton("open-menu-symbolic") + self.sidebar_flap.bind_property("reveal-flap", button, "active", GObject.BindingFlags.BIDIRECTIONAL) + mobile_header.pack_start(button) + + squeezer.add(mobile_header) + + def squeezer_changed(squeezer, _): + is_desktop = squeezer.get_visible_child() == desktop_header + + self.sidebar_flap.set_swipe_to_open(not is_desktop) + + # When transitioning, don't play the reveal animation + dur = self.sidebar_flap.get_reveal_duration() + self.sidebar_flap.set_reveal_duration(0) + + self.sidebar_flap.set_reveal_flap(False) + + self.sidebar_flap.set_reveal_duration(dur) + squeezer.connect("notify::visible-child", squeezer_changed) + + return squeezer + + def _create_sidebar(self): + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, width_request=150) + box.get_style_context().add_class("background") + + stack = Gtk.StackSidebar(stack=self.stack, vexpand=True) + box.pack_start(stack, True, True, 0) + + box.pack_end(Gtk.Separator(), False, False, 0) + + servers = IconButton("list-add-symbolic", label="Servers") + box.pack_end(servers, False, False, 0) + + settings = IconButton("emblem-system-symbolic", label="Settings") + settings.connect("clicked", self._show_settings) + box.pack_end(settings, False, False, 0) + + downloads = IconButton("folder-download-symbolic", label="Downloads") + box.pack_end(downloads, False, False, 0) + + return box def _create_label( self, text: str, *args, halign: Gtk.Align = Gtk.Align.START, **kwargs @@ -712,23 +774,6 @@ class MainWindow(Gtk.ApplicationWindow): return box - def _create_spin_button_menu_item( - self, label: str, low: int, high: int, step: int, settings_name: str - ) -> Tuple[Gtk.Box, Gtk.Entry]: - def on_change(entry: Gtk.SpinButton) -> bool: - self._emit_settings_change({settings_name: int(entry.get_value())}) - return False - - box = Gtk.Box() - box.add(spin_button_label := Gtk.Label(label=label)) - spin_button_label.get_style_context().add_class("menu-label") - - entry = Gtk.SpinButton.new_with_range(low, high, step) - entry.connect("value-changed", on_change) - box.pack_end(entry, False, False, 0) - box.get_style_context().add_class("menu-button") - return box, entry - def _create_downloads_popover(self) -> Gtk.PopoverMenu: menu = Gtk.PopoverMenu() vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, name="downloads-menu") @@ -860,66 +905,74 @@ class MainWindow(Gtk.ApplicationWindow): menu.child_set_property(switch_provider_options, "submenu", "switch-provider") return menu - def _create_main_menu(self) -> Gtk.PopoverMenu: - main_menu = Gtk.PopoverMenu() - vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, name="main-menu-box") + def _create_settings_window(self) -> Gtk.PopoverMenu: + window = Handy.PreferencesWindow() + + general = Handy.PreferencesPage(icon_name="emblem-system-symbolic", title="General") + general_group = Handy.PreferencesGroup(title="General") + + def create_switch(setting, **kwargs): + row = Handy.ActionRow(**kwargs) + switch = Gtk.Switch(valign=Gtk.Align.CENTER) + prop = setting.replace("_", "-") + self.bind_property(f"setting-{prop}", switch, "active", GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.SYNC_CREATE) + + row.add(switch) + return row + + def create_spin_button(low: int, high: int, step: int, setting: str, **kwargs): + row = Handy.ActionRow(**kwargs) + button = Gtk.SpinButton.new_with_range(low, high, step) + prop = setting.replace("_", "-") + self.bind_property(f"setting-{prop}", button, "value", GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.SYNC_CREATE) + + row.add(button) + return row # Notifications - notifications_box, self.notification_switch = self._create_toggle_menu_button( - "Enable Song Notifications", "song_play_notification" - ) - vbox.add(notifications_box) + row = create_switch("song_play_notification", title="Enable Song Notifications") + general_group.add(row) - # PLAYER SETTINGS - # ============================================================================== + # Player settings self.player_settings_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - vbox.add(self.player_settings_box) + # vbox.add(self.player_settings_box) + + general.add(general_group) + window.add(general) # DOWNLOAD SETTINGS # ============================================================================== - vbox.add(Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)) - vbox.add( - self._create_label("Download Settings", name="menu-settings-separator") - ) + download = Handy.PreferencesPage(icon_name="folder-download-symbolic", title="Downloads") + download_group = Handy.PreferencesGroup(title="Downloads", description="Settings for downloads") # Allow Song Downloads - ( - allow_song_downloads, - self.allow_song_downloads_switch, - ) = self._create_toggle_menu_button( - "Allow Song Downloads", "allow_song_downloads" - ) - vbox.add(allow_song_downloads) + download_row = Handy.ExpanderRow(show_enable_switch=True, title="Allow Song Downloads", expanded=True) + self.bind_property("setting-allow-song-downloads", download_row, "enable-expansion", GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.SYNC_CREATE) # Download on Stream - ( - download_on_stream, - self.download_on_stream_switch, - ) = self._create_toggle_menu_button( - "When Streaming, Also Download Song", "download_on_stream" - ) - vbox.add(download_on_stream) + row = create_switch("download_on_stream", title="When Streaming, Also Download Song") + download_row.add(row) # Prefetch Songs - ( - prefetch_songs_box, - self.prefetch_songs_entry, - ) = self._create_spin_button_menu_item( - "Number of Songs to Prefetch", 0, 10, 1, "prefetch_amount" - ) - vbox.add(prefetch_songs_box) + row = create_spin_button(0, 10, 1, "prefetch_amount", title="Number of Songs to Prefetch") + download_row.add(row) # Max Concurrent Downloads - ( - max_concurrent_downloads, - self.max_concurrent_downloads_entry, - ) = self._create_spin_button_menu_item( - "Maximum Concurrent Downloads", 0, 10, 1, "concurrent_download_limit" - ) - vbox.add(max_concurrent_downloads) + row = create_spin_button(0, 10, 1, "concurrent_download_limit", title="Maximum Concurrent Downloads") + download_row.add(row) - main_menu.add(vbox) - return main_menu + download_group.add(download_row) + download.add(download_group) + + window.add(download) + + return window + + def _on_settings_change(self, setting, _, prop): + if self._updating_settings: + return + + self._emit_settings_change({setting: self.get_property(prop.name)}) def _create_search_popup(self) -> Gtk.PopoverMenu: self.search_popup = Gtk.PopoverMenu(modal=False) @@ -1032,9 +1085,10 @@ class MainWindow(Gtk.ApplicationWindow): self.server_connection_popover.popup() self.server_connection_popover.show_all() - def _on_main_menu_clicked(self, *args): - self.main_menu_popover.popup() - self.main_menu_popover.show_all() + def _show_settings(self, *args): + window = self._create_settings_window() + window.set_transient_for(self) + window.show_all() def _on_search_entry_focus(self, *args): self._show_search()