15 Commits

Author SHA1 Message Date
Martin Wagner
8c0c1fe59e preparations for 1.2.1 2021-05-04 17:29:20 +02:00
Martin Wagner
364faf67be added force mode to "back_to_current_album_button" 2021-05-02 14:42:01 +02:00
Martin Wagner
7cd76f34bb removed tooltips in ArtistPopover 2021-04-25 19:40:43 +02:00
Martin Wagner
113d07a70a fixed single mode oneshot indicator with some themes 2021-04-25 19:33:44 +02:00
Martin Wagner
1e5b894d82 fixed mem leak 2021-04-25 19:00:54 +02:00
Martin Wagner
5815ca79a7 fixed styling of OutputPopover 2021-04-25 17:08:22 +02:00
Martin Wagner
d54cf001d7 changed oneshot single mode indication 2021-04-25 11:54:33 +02:00
Martin Wagner
3bdf185788 fixed button packing issue 2021-04-24 11:42:35 +02:00
Martin Wagner
87786099ea provide settings button on connection error 2021-04-24 11:03:46 +02:00
Martin Wagner
60afc4e412 fixed PlaybackOptions 2021-04-24 10:24:59 +02:00
Martin Wagner
b32a065b66 removed "search-focus-changed" signal 2021-04-23 22:06:13 +02:00
Martin Wagner
1660f26fa5 avoid multiple implementations of simple mpd commands 2021-04-23 21:29:46 +02:00
Martin Wagner
3474c027f5 fixed connect_button 2021-04-23 16:17:37 +02:00
Martin Wagner
4d1b20f7a5 internal cleanups in SettingsDialog 2021-04-23 15:30:08 +02:00
Martin Wagner
3caac937b7 reworked ProfileSettings 2021-04-22 23:40:51 +02:00
3 changed files with 224 additions and 311 deletions

View File

@@ -36,7 +36,7 @@ if os.path.isfile("/.flatpak-info"): # test for flatpak environment
else:
bindtextdomain("mpdevil", localedir=None) # replace "None" by a static path if needed (e.g when installing on a non-FHS distro)
VERSION="1.2.0" # sync with setup.py
VERSION="1.2.1" # sync with setup.py
COVER_REGEX=r"^\.?(album|cover|folder|front).*\.(gif|jpeg|jpg|png)$"
@@ -930,7 +930,6 @@ class GeneralSettings(Gtk.Box):
def __init__(self, settings):
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=6, border_width=18)
self._settings=settings
self._settings_handlers=[]
# int settings
int_settings={}
@@ -943,10 +942,7 @@ class GeneralSettings(Gtk.Box):
for label, (vmin, vmax, step), key in int_settings_data:
int_settings[key]=(Gtk.Label(label=label, xalign=0), Gtk.SpinButton.new_with_range(vmin, vmax, step))
int_settings[key][1].set_value(self._settings.get_int(key))
int_settings[key][1].connect("value-changed", self._on_int_changed, key)
self._settings_handlers.append(
self._settings.connect("changed::{}".format(key), self._on_int_settings_changed, int_settings[key][1])
)
int_settings[key][1].connect("value-changed", lambda widget,key: self._settings.set_int(key, int(widget.get_value())), key)
# check buttons
check_buttons={}
@@ -963,21 +959,16 @@ class GeneralSettings(Gtk.Box):
(_("Support “MPRIS”"), "mpris"),
]
for label, key in check_buttons_data:
check_buttons[key]=Gtk.CheckButton(label=label)
check_buttons[key]=Gtk.CheckButton(label=label, margin_start=12)
check_buttons[key].set_active(self._settings.get_boolean(key))
check_buttons[key].set_margin_start(12)
check_buttons[key].connect("toggled", self._on_toggled, key)
self._settings_handlers.append(
self._settings.connect("changed::{}".format(key), self._on_check_settings_changed, check_buttons[key])
)
check_buttons[key].connect("toggled", lambda widget,key: self._settings.set_boolean(key, widget.get_active()), key)
# headings
view_heading=Gtk.Label(label=_("<b>View</b>"), use_markup=True, xalign=0)
behavior_heading=Gtk.Label(label=_("<b>Behavior</b>"), use_markup=True, xalign=0)
# view grid
view_grid=Gtk.Grid(row_spacing=6, column_spacing=12)
view_grid.set_margin_start(12)
view_grid=Gtk.Grid(row_spacing=6, column_spacing=12, margin_start=12)
view_grid.add(int_settings["track-cover"][0])
view_grid.attach_next_to(int_settings["album-cover"][0], int_settings["track-cover"][0], Gtk.PositionType.BOTTOM, 1, 1)
view_grid.attach_next_to(int_settings["icon-size"][0], int_settings["album-cover"][0], Gtk.PositionType.BOTTOM, 1, 1)
@@ -987,9 +978,6 @@ class GeneralSettings(Gtk.Box):
view_grid.attach_next_to(int_settings["icon-size"][1], int_settings["icon-size"][0], Gtk.PositionType.RIGHT, 1, 1)
view_grid.attach_next_to(int_settings["icon-size-sec"][1], int_settings["icon-size-sec"][0], Gtk.PositionType.RIGHT, 1, 1)
# connect
self.connect("destroy", self._remove_handlers)
# packing
csd_box=Gtk.Box(spacing=12)
csd_box.pack_start(check_buttons["use-csd"], False, False, 0)
@@ -1011,41 +999,20 @@ class GeneralSettings(Gtk.Box):
self.pack_start(check_buttons["force-mode"], False, False, 0)
self.pack_start(check_buttons["stop-on-quit"], False, False, 0)
def _remove_handlers(self, *args):
for handler in self._settings_handlers:
self._settings.disconnect(handler)
def _on_int_settings_changed(self, settings, key, entry):
entry.set_value(settings.get_int(key))
def _on_check_settings_changed(self, settings, key, button):
button.set_active(settings.get_boolean(key))
def _on_int_changed(self, widget, key):
self._settings.set_int(key, int(widget.get_value()))
def _on_toggled(self, widget, key):
self._settings.set_boolean(key, widget.get_active())
class ProfileSettings(Gtk.Grid):
def __init__(self, parent, client, settings):
super().__init__(row_spacing=6, column_spacing=12, border_width=18)
self._client=client
self._settings=settings
self._gui_modification=False # indicates whether the settings were changed from the settings dialog
# widgets
self._profiles_combo=Gtk.ComboBoxText(entry_text_column=0, hexpand=True)
self._profiles_combo=ComboBoxEntry()
add_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("list-add-symbolic", Gtk.IconSize.BUTTON))
delete_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("list-remove-symbolic", Gtk.IconSize.BUTTON))
add_delete_buttons=Gtk.ButtonBox(layout_style=Gtk.ButtonBoxStyle.EXPAND)
add_delete_buttons.pack_start(add_button, True, True, 0)
add_delete_buttons.pack_start(delete_button, True, True, 0)
connect_button=Gtk.Button.new_with_mnemonic(_("_Connect"))
self._profile_entry=Gtk.Entry(hexpand=True)
self._host_entry=Gtk.Entry(hexpand=True)
self._port_entry=Gtk.SpinButton.new_with_range(0, 65535, 1)
address_entry=Gtk.Box(spacing=6)
@@ -1053,19 +1020,14 @@ class ProfileSettings(Gtk.Grid):
address_entry.pack_start(self._port_entry, False, False, 0)
self._password_entry=PasswordEntry(hexpand=True)
self._path_entry=Gtk.Entry(hexpand=True, placeholder_text=GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_MUSIC))
self._path_select_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("folder-open-symbolic", Gtk.IconSize.BUTTON))
path_box=Gtk.Box(spacing=6)
path_box.pack_start(self._path_entry, True, True, 0)
path_box.pack_start(self._path_select_button, False, False, 0)
self._path_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "folder-open-symbolic")
self._regex_entry=Gtk.Entry(hexpand=True, placeholder_text=COVER_REGEX)
self._regex_entry.set_tooltip_text(
_("The first image in the same directory as the song file "\
"matching this regex will be displayed. %AlbumArtist% and "\
"%Album% will be replaced by the corresponding tags of the song.")
)
profiles_label=Gtk.Label(label=_("Profile:"), xalign=1)
profile_label=Gtk.Label(label=_("Name:"), xalign=1)
host_label=Gtk.Label(label=_("Host:"), xalign=1)
password_label=Gtk.Label(label=_("Password:"), xalign=1)
path_label=Gtk.Label(label=_("Music lib:"), xalign=1)
@@ -1077,44 +1039,34 @@ class ProfileSettings(Gtk.Grid):
connect_button.connect("clicked", self._on_connect_button_clicked)
style_context=connect_button.get_style_context()
style_context.add_class("suggested-action")
self._path_select_button.connect("clicked", self._on_path_select_button_clicked, parent)
self._profiles_changed=self._profiles_combo.connect("changed", self._on_profiles_changed)
self._path_entry.connect("icon-release", self._on_path_entry_icon_release, parent)
self._profiles_select=self._profiles_combo.connect("select", self._on_profiles_select)
self.entry_changed_handlers=[]
self.entry_changed_handlers.append((self._profile_entry, self._profile_entry.connect("changed", self._on_profile_entry_changed)))
self.entry_changed_handlers.append((self._profiles_combo, self._profiles_combo.connect("text", self._on_profile_entry_changed)))
self.entry_changed_handlers.append((self._host_entry, self._host_entry.connect("changed", self._on_host_entry_changed)))
self.entry_changed_handlers.append((self._port_entry, self._port_entry.connect("value-changed", self._on_port_entry_changed)))
self.entry_changed_handlers.append((self._password_entry, self._password_entry.connect("changed", self._on_password_entry_changed)))
self.entry_changed_handlers.append((self._path_entry, self._path_entry.connect("changed", self._on_path_entry_changed)))
self.entry_changed_handlers.append((self._regex_entry, self._regex_entry.connect("changed", self._on_regex_entry_changed)))
self._settings_handlers=[]
self._settings_handlers.append(self._settings.connect("changed::profiles", self._on_settings_changed))
self._settings_handlers.append(self._settings.connect("changed::hosts", self._on_settings_changed))
self._settings_handlers.append(self._settings.connect("changed::ports", self._on_settings_changed))
self._settings_handlers.append(self._settings.connect("changed::passwords", self._on_settings_changed))
self._settings_handlers.append(self._settings.connect("changed::paths", self._on_settings_changed))
self._settings_handlers.append(self._settings.connect("changed::regex", self._on_settings_changed))
self.connect("destroy", self._remove_handlers)
self._profiles_combo_reload()
self._profiles_combo.set_active(0)
# packing
self.add(profiles_label)
self.attach_next_to(profile_label, profiles_label, Gtk.PositionType.BOTTOM, 1, 1)
self.attach_next_to(host_label, profile_label, Gtk.PositionType.BOTTOM, 1, 1)
self.attach_next_to(host_label, profiles_label, Gtk.PositionType.BOTTOM, 1, 1)
self.attach_next_to(password_label, host_label, Gtk.PositionType.BOTTOM, 1, 1)
self.attach_next_to(path_label, password_label, Gtk.PositionType.BOTTOM, 1, 1)
self.attach_next_to(regex_label, path_label, Gtk.PositionType.BOTTOM, 1, 1)
self.attach_next_to(self._profiles_combo, profiles_label, Gtk.PositionType.RIGHT, 2, 1)
self.attach_next_to(add_delete_buttons, self._profiles_combo, Gtk.PositionType.RIGHT, 1, 1)
self.attach_next_to(self._profile_entry, profile_label, Gtk.PositionType.RIGHT, 2, 1)
self.attach_next_to(address_entry, host_label, Gtk.PositionType.RIGHT, 2, 1)
self.attach_next_to(self._password_entry, password_label, Gtk.PositionType.RIGHT, 2, 1)
self.attach_next_to(path_box, path_label, Gtk.PositionType.RIGHT, 2, 1)
self.attach_next_to(self._path_entry, path_label, Gtk.PositionType.RIGHT, 2, 1)
self.attach_next_to(self._regex_entry, regex_label, Gtk.PositionType.RIGHT, 2, 1)
connect_button.set_margin_top(12)
self.attach_next_to(connect_button, self._regex_entry, Gtk.PositionType.BOTTOM, 2, 1)
self._profiles_combo_reload()
self._profiles_combo.set_active(self._settings.get_int("active-profile"))
def _block_entry_changed_handlers(self, *args):
for obj, handler in self.entry_changed_handlers:
obj.handler_block(handler)
@@ -1124,24 +1076,11 @@ class ProfileSettings(Gtk.Grid):
obj.handler_unblock(handler)
def _profiles_combo_reload(self, *args):
self._profiles_combo.handler_block(self._profiles_changed)
self._profiles_combo.handler_block(self._profiles_select)
self._profiles_combo.remove_all()
for profile in self._settings.get_value("profiles"):
self._profiles_combo.append_text(profile)
self._profiles_combo.handler_unblock(self._profiles_changed)
def _remove_handlers(self, *args):
for handler in self._settings_handlers:
self._settings.disconnect(handler)
def _on_settings_changed(self, *args):
if self._gui_modification:
self._gui_modification=False
else:
self._profiles_combo_reload()
self._profiles_combo.set_active(0)
self._profiles_combo.handler_unblock(self._profiles_select)
def _on_add_button_clicked(self, *args):
model=self._profiles_combo.get_model()
@@ -1156,7 +1095,7 @@ class ProfileSettings(Gtk.Grid):
self._profiles_combo.set_active(new_pos)
def _on_delete_button_clicked(self, *args):
pos=self._profiles_combo.get_active()
pos=self._profiles_combo.get_selected()
self._settings.array_delete("as", "profiles", pos)
self._settings.array_delete("as", "hosts", pos)
self._settings.array_delete("ai", "ports", pos)
@@ -1171,55 +1110,45 @@ class ProfileSettings(Gtk.Grid):
self._profiles_combo.set_active(new_pos)
def _on_connect_button_clicked(self, *args):
self._settings.set_int("active-profile", self._profiles_combo.get_active())
self._client.reconnect()
selected=self._profiles_combo.get_selected()
if selected == self._settings.get_int("active-profile"):
self._client.reconnect()
else:
self._settings.set_int("active-profile", selected)
def _on_profile_entry_changed(self, *args):
self._gui_modification=True
pos=self._profiles_combo.get_active()
self._settings.array_modify("as", "profiles", pos, self._profile_entry.get_text())
self._profiles_combo_reload()
self._profiles_combo.handler_block(self._profiles_changed) # do not reload all settings
self._profiles_combo.set_active(pos)
self._profiles_combo.handler_unblock(self._profiles_changed)
self._settings.array_modify("as", "profiles", self._profiles_combo.get_selected(), self._profiles_combo.get_text())
def _on_host_entry_changed(self, *args):
self._gui_modification=True
self._settings.array_modify("as", "hosts", self._profiles_combo.get_active(), self._host_entry.get_text())
self._settings.array_modify("as", "hosts", self._profiles_combo.get_selected(), self._host_entry.get_text())
def _on_port_entry_changed(self, *args):
self._gui_modification=True
self._settings.array_modify("ai", "ports", self._profiles_combo.get_active(), int(self._port_entry.get_value()))
self._settings.array_modify("ai", "ports", self._profiles_combo.get_selected(), int(self._port_entry.get_value()))
def _on_password_entry_changed(self, *args):
self._gui_modification=True
self._settings.array_modify("as", "passwords", self._profiles_combo.get_active(), self._password_entry.get_text())
self._settings.array_modify("as", "passwords", self._profiles_combo.get_selected(), self._password_entry.get_text())
def _on_path_entry_changed(self, *args):
self._gui_modification=True
self._settings.array_modify("as", "paths", self._profiles_combo.get_active(), self._path_entry.get_text())
self._settings.array_modify("as", "paths", self._profiles_combo.get_selected(), self._path_entry.get_text())
def _on_regex_entry_changed(self, *args):
self._gui_modification=True
self._settings.array_modify("as", "regex", self._profiles_combo.get_active(), self._regex_entry.get_text())
self._settings.array_modify("as", "regex", self._profiles_combo.get_selected(), self._regex_entry.get_text())
def _on_path_select_button_clicked(self, widget, parent):
def _on_path_entry_icon_release(self, widget, icon_pos, event, parent):
dialog=Gtk.FileChooserNative(title=_("Choose directory"), transient_for=parent, action=Gtk.FileChooserAction.SELECT_FOLDER)
folder=self._settings.get_lib_path(self._profiles_combo.get_active())
folder=self._settings.get_lib_path(self._profiles_combo.get_selected())
if folder is not None:
dialog.set_current_folder(folder)
response=dialog.run()
if response == Gtk.ResponseType.ACCEPT:
self._gui_modification=True
self._settings.array_modify("as", "paths", self._profiles_combo.get_active(), dialog.get_filename())
self._settings.array_modify("as", "paths", self._profiles_combo.get_selected(), dialog.get_filename())
self._path_entry.set_text(dialog.get_filename())
dialog.destroy()
def _on_profiles_changed(self, *args):
def _on_profiles_select(self, *args):
active=self._profiles_combo.get_active()
if active >= 0:
self._block_entry_changed_handlers()
self._profile_entry.set_text(self._settings.get_value("profiles")[active])
self._host_entry.set_text(self._settings.get_value("hosts")[active])
self._port_entry.set_value(self._settings.get_value("ports")[active])
self._password_entry.set_text(self._settings.get_value("passwords")[active])
@@ -1282,10 +1211,6 @@ class PlaylistSettings(Gtk.Box):
self._up_button.connect("clicked", self._on_up_button_clicked)
self._down_button.connect("clicked", self._on_down_button_clicked)
self._selection.connect("changed", self._set_button_sensitivity)
self._settings_handlers=[]
self._settings_handlers.append(self._settings.connect("changed::column-visibilities", self._on_visibilities_changed))
self._settings_handlers.append(self._settings.connect("changed::column-permutation", self._on_permutation_changed))
self.connect("destroy", self._remove_handlers)
# packing
self.pack_start(label, False, False, 0)
@@ -1319,10 +1244,6 @@ class PlaylistSettings(Gtk.Box):
self._up_button.set_sensitive(True)
self._down_button.set_sensitive(True)
def _remove_handlers(self, *args):
for handler in self._settings_handlers:
self._settings.disconnect(handler)
def _on_cell_toggled(self, widget, path):
self._store[path][0]=not self._store[path][0]
self._settings.array_modify("ab", "column-visibilities", self._store[path][2], self._store[path][0])
@@ -1344,26 +1265,8 @@ class PlaylistSettings(Gtk.Box):
self._set_button_sensitivity()
self._save_permutation()
def _on_visibilities_changed(self, *args):
visibilities=self._settings.get_value("column-visibilities").unpack()
for i, actual_index in enumerate(self._settings.get_value("column-permutation")):
self._store[i][0]=visibilities[actual_index]
def _on_permutation_changed(self, *args):
equal=True
perm=self._settings.get_value("column-permutation")
for i, e in enumerate(self._store):
if e[2] != perm[i]:
equal=False
break
if not equal:
self._store.handler_block(self._row_deleted)
self._store.clear()
self._fill()
self._store.handler_unblock(self._row_deleted)
class SettingsDialog(Gtk.Dialog):
def __init__(self, parent, client, settings):
def __init__(self, parent, client, settings, tab="general"):
use_csd=settings.get_boolean("use-csd")
if use_csd:
super().__init__(title=_("Settings"), transient_for=parent, use_header_bar=True)
@@ -1397,6 +1300,10 @@ class SettingsDialog(Gtk.Dialog):
vbox.set_property("border-width", 6)
vbox.pack_start(tabs, True, True, 0)
self.show_all()
if use_csd:
stack.set_visible_child_name(tab)
else:
tabs.set_current_page({"general": 0, "profiles": 1, "playlist": 2}[tab])
#################
# other dialogs #
@@ -1490,6 +1397,35 @@ class PasswordEntry(Gtk.Entry):
self.set_visibility(False)
self.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "view-conceal-symbolic")
class ComboBoxEntry(Gtk.ComboBoxText):
__gsignals__={"text": (GObject.SignalFlags.RUN_FIRST, None, ()), "select": (GObject.SignalFlags.RUN_FIRST, None, ())}
def __init__(self):
super().__init__(entry_text_column=0, has_entry=True)
self._store=self.get_property("model")
self._entry=self.get_child()
self._selected=-1
# connect
self.connect("changed", self._on_changed)
def get_text(self):
return self._entry.get_text()
def get_selected(self):
return self._selected
def _on_changed(self, *args):
active=self.get_active()
if active >= 0:
self._selected=active
self.emit("select")
else:
try:
self._store[self._selected][0]=self._entry.get_text()
self.emit("text")
except:
pass
class FocusFrame(Gtk.Overlay):
def __init__(self):
super().__init__()
@@ -1840,13 +1776,12 @@ class ArtistPopover(Gtk.Popover):
# buttons
vbox=Gtk.ButtonBox(orientation=Gtk.Orientation.VERTICAL, border_width=9)
data=((_("Append"), _("Add all titles to playlist"), "list-add-symbolic", "append"),
(_("Play"), _("Directly play all titles"), "media-playback-start-symbolic", "play"),
(_("Enqueue"), _("Append all titles after the currently playing track and clear the playlist from all other songs"),
"insert-object-symbolic", "enqueue")
data=((_("Append"), "list-add-symbolic", "append"),
(_("Play"), "media-playback-start-symbolic", "play"),
(_("Enqueue"), "insert-object-symbolic", "enqueue")
)
for label, tooltip, icon, mode in data:
button=Gtk.ModelButton(label=label, tooltip_text=tooltip, image=Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.BUTTON))
for label, icon, mode in data:
button=Gtk.ModelButton(label=label, image=Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.BUTTON))
button.get_child().set_property("xalign", 0)
button.connect("clicked", self._on_button_clicked, mode)
vbox.pack_start(button, True, True, 0)
@@ -1943,6 +1878,8 @@ class SearchWindow(Gtk.Box):
# connect
self._search_entry_changed=self.search_entry.connect("search-changed", self._search)
self.search_entry.connect("focus_in_event", self._on_search_entry_focus_event, True)
self.search_entry.connect("focus_out_event", self._on_search_entry_focus_event, False)
self._tag_combo_box_changed=self._tag_combo_box.connect("changed", self._search)
self._client.emitter.connect("reconnected", self._on_reconnected)
self._client.emitter.connect("disconnected", self._on_disconnected)
@@ -2042,6 +1979,13 @@ class SearchWindow(Gtk.Box):
pass
return False
def _on_search_entry_focus_event(self, widget, event, focus):
app=self.get_toplevel().get_application()
if focus:
app.set_accels_for_action("mpd.toggle-play", [])
else:
app.set_accels_for_action("mpd.toggle-play", ["space"])
class GenreSelect(Gtk.ComboBoxText):
__gsignals__={"genre_changed": (GObject.SignalFlags.RUN_FIRST, None, ())}
def __init__(self, client):
@@ -2531,7 +2475,6 @@ class AlbumWindow(FocusFrame):
GLib.idle_add(callback)
class Browser(Gtk.Paned):
__gsignals__={"search-focus-changed": (GObject.SignalFlags.RUN_FIRST, None, (bool,))}
def __init__(self, client, settings):
super().__init__(orientation=Gtk.Orientation.HORIZONTAL)
self._client=client
@@ -2558,10 +2501,9 @@ class Browser(Gtk.Paned):
self._album_window=AlbumWindow(self._client, self._settings, self._artist_window)
# connect
self.back_to_current_album_button.connect("clicked", self._back_to_current_album)
self.back_to_current_album_button.connect("clicked", self._on_back_to_current_album_button_clicked)
self.back_to_current_album_button.connect("button-press-event", self._on_back_to_current_album_button_press_event)
self.search_button.connect("toggled", self._on_search_toggled)
self._search_window.search_entry.connect("focus_in_event", lambda *args: self.emit("search-focus-changed", True))
self._search_window.search_entry.connect("focus_out_event", lambda *args: self.emit("search-focus-changed", False))
self._artist_window.connect("artists_changed", self._on_artists_changed)
self._settings.connect("notify::mini-player", self._on_mini_player)
self._client.emitter.connect("disconnected", self._on_disconnected)
@@ -2585,7 +2527,7 @@ class Browser(Gtk.Paned):
self.pack1(box1, False, False)
self.pack2(self._stack, True, False)
def _back_to_current_album(self, *args):
def _back_to_current_album(self, force=False):
song=ClientHelper.song_to_first_str_dict(self._client.currentsong())
if song != {}:
self.search_button.set_active(False)
@@ -2594,16 +2536,23 @@ class Browser(Gtk.Paned):
if artist is None:
artist=song.get("artist", "")
# deactivate genre filter to show all artists (if needed)
if song.get("genre", "") != self._genre_select.get_selected_genre():
if song.get("genre", "") != self._genre_select.get_selected_genre() or force:
self._genre_select.deactivate()
# select artist
if self._artist_window.get_selected_artist() is None: # all artists selected
if self._artist_window.get_selected_artist() is None and not force: # all artists selected
self.search_button.set_active(False)
self._artist_window.highlight_selected()
else: # one artist selected
self._artist_window.select(artist)
self._album_window.scroll_to_current_album()
def _on_back_to_current_album_button_clicked(self, *args):
self._back_to_current_album()
def _on_back_to_current_album_button_press_event(self, widget, event):
if event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS:
self._back_to_current_album(force=True)
def _on_search_toggled(self, widget):
if widget.get_active():
self._stack.set_visible_child_name("search")
@@ -2879,18 +2828,19 @@ class PlaylistWindow(Gtk.Box):
self._back_to_current_song_button=Gtk.Button(
image=AutoSizedIcon("go-previous-symbolic", "icon-size-sec", self._settings),
tooltip_text=_("Scroll to current song"),
relief=Gtk.ReliefStyle.NONE
relief=Gtk.ReliefStyle.NONE,
can_focus=False
)
self._back_to_current_song_button.set_can_focus(False)
style_context=self._back_to_current_song_button.get_style_context()
style_context.add_provider(provider, 600)
self._clear_button=Gtk.Button(
clear_button=Gtk.Button(
image=AutoSizedIcon("edit-clear-symbolic", "icon-size-sec", self._settings),
tooltip_text=_("Clear playlist"),
relief=Gtk.ReliefStyle.NONE
relief=Gtk.ReliefStyle.NONE,
action_name="mpd.clear",
can_focus=False
)
self._clear_button.set_can_focus(False)
style_context=self._clear_button.get_style_context()
style_context=clear_button.get_style_context()
style_context.add_class("destructive-action")
style_context.add_provider(provider, 600)
@@ -2945,7 +2895,7 @@ class PlaylistWindow(Gtk.Box):
action_bar.pack_start(self._playlist_info)
audio.set_margin_end(3)
audio.set_margin_start(12)
action_bar.pack_end(self._clear_button)
action_bar.pack_end(clear_button)
action_bar.pack_end(audio)
# song popover
@@ -2960,7 +2910,6 @@ class PlaylistWindow(Gtk.Box):
self._row_deleted=self._store.connect("row-deleted", self._on_row_deleted)
self._row_inserted=self._store.connect("row-inserted", self._on_row_inserted)
self._back_to_current_song_button.connect("clicked", self._on_back_to_current_song_button_clicked)
self._clear_button.connect("clicked", self._on_clear_button_clicked)
self._client.emitter.connect("playlist_changed", self._on_playlist_changed)
self._client.emitter.connect("current_song_changed", self._on_song_changed)
@@ -3127,19 +3076,14 @@ class PlaylistWindow(Gtk.Box):
break
self._scroll_to_selected_title()
def _on_clear_button_clicked(self, *args):
self._client.clear()
def _on_disconnected(self, *args):
self._treeview.set_sensitive(False)
self._back_to_current_song_button.set_sensitive(False)
self._clear_button.set_sensitive(False)
self._song_popover.popdown()
self._clear()
def _on_reconnected(self, *args):
self._back_to_current_song_button.set_sensitive(True)
self._clear_button.set_sensitive(True)
self._treeview.set_sensitive(True)
def _on_show_info(self, *args):
@@ -3247,27 +3191,26 @@ class PlaybackControl(Gtk.ButtonBox):
self._play_icon=AutoSizedIcon("media-playback-start-symbolic", "icon-size", self._settings)
self._pause_icon=AutoSizedIcon("media-playback-pause-symbolic", "icon-size", self._settings)
self._play_button=Gtk.Button(image=self._play_icon)
self._play_button.set_action_name("mpd.toggle-play")
self._play_button.set_can_focus(False)
self._stop_button=Gtk.Button(image=AutoSizedIcon("media-playback-stop-symbolic", "icon-size", self._settings))
self._stop_button.set_property("no-show-all", not(self._settings.get_boolean("show-stop")))
self._stop_button.set_action_name("mpd.stop")
self._stop_button.set_can_focus(False)
self._prev_button=Gtk.Button(image=AutoSizedIcon("media-skip-backward-symbolic", "icon-size", self._settings))
self._prev_button.set_action_name("mpd.prev")
self._prev_button.set_can_focus(False)
self._next_button=Gtk.Button(image=AutoSizedIcon("media-skip-forward-symbolic", "icon-size", self._settings))
self._next_button.set_action_name("mpd.next")
self._next_button.set_can_focus(False)
# connect
self._play_button.connect("clicked", self._on_play_clicked)
self._stop_button.connect("clicked", self._on_stop_clicked)
self._stop_button.set_property("no-show-all", not(self._settings.get_boolean("show-stop")))
self._prev_button.connect("clicked", self._on_prev_clicked)
self._next_button.connect("clicked", self._on_next_clicked)
self._settings.connect("notify::mini-player", self._on_mini_player)
self._settings.connect("changed::show-stop", self._on_show_stop_changed)
self._client.emitter.connect("state", self._on_state)
self._client.emitter.connect("playlist_changed", self._refresh_tooltips)
self._client.emitter.connect("current_song_changed", self._refresh_tooltips)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
# packing
self.pack_start(self._prev_button, True, True, 0)
@@ -3289,40 +3232,18 @@ class PlaybackControl(Gtk.ButtonBox):
self._prev_button.set_tooltip_text("")
self._next_button.set_tooltip_text("")
def _on_play_clicked(self, widget):
self._client.toggle_play()
def _on_stop_clicked(self, widget):
self._client.stop()
def _on_prev_clicked(self, widget):
self._client.previous()
def _on_next_clicked(self, widget):
self._client.next()
def _on_state(self, emitter, state):
if state == "play":
self._play_button.set_image(self._pause_icon)
self._prev_button.set_sensitive(True)
self._next_button.set_sensitive(True)
elif state == "pause":
self._play_button.set_image(self._play_icon)
self._prev_button.set_sensitive(True)
self._next_button.set_sensitive(True)
else:
self._play_button.set_image(self._play_icon)
self._prev_button.set_sensitive(False)
self._next_button.set_sensitive(False)
def _on_disconnected(self, *args):
self.set_sensitive(False)
self._prev_button.set_tooltip_text("")
self._next_button.set_tooltip_text("")
def _on_reconnected(self, *args):
self.set_sensitive(True)
def _on_mini_player(self, obj, typestring):
self._on_show_stop_changed()
@@ -3453,77 +3374,112 @@ class OutputPopover(Gtk.Popover):
self._client=client
# widgets
box=Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6, border_width=6)
box=Gtk.Box(orientation=Gtk.Orientation.VERTICAL, border_width=9)
for output in self._client.outputs():
button=Gtk.CheckButton(label="{} ({})".format(output["outputname"], output["plugin"]))
button=Gtk.ModelButton(label="{} ({})".format(output["outputname"], output["plugin"]), role=Gtk.ButtonRole.CHECK)
button.get_child().set_property("xalign", 0)
if output["outputenabled"] == "1":
button.set_active(True)
button.connect("toggled", self._on_button_toggled, output["outputid"])
button.set_property("active", True)
button.connect("clicked", self._on_button_clicked, output["outputid"])
box.pack_start(button, False, False, 0)
#connect
self.connect("closed", lambda *args: self.destroy())
# packing
self.add(box)
box.show_all()
def _on_button_toggled(self, button, out_id):
if button.get_active():
self._client.enableoutput(out_id)
else:
def _on_button_clicked(self, button, out_id):
if button.get_property("active"):
self._client.disableoutput(out_id)
button.set_property("active", False)
else:
self._client.enableoutput(out_id)
button.set_property("active", True)
class PlaybackOptions(Gtk.Box):
class VolumeButton(Gtk.VolumeButton):
def __init__(self, client, settings):
super().__init__(spacing=6)
super().__init__(use_symbolic=True, size=settings.get_gtk_icon_size("icon-size"), can_focus=False)
self._client=client
self._settings=settings
self._popover=None
# widgets
icons={}
for icon_name in ("media-playlist-shuffle-symbolic","media-playlist-repeat-symbolic",
"org.mpdevil.mpdevil-single-symbolic","org.mpdevil.mpdevil-consume-symbolic"):
icons[icon_name]=AutoSizedIcon(icon_name, "icon-size", self._settings)
self._random_button=Gtk.ToggleButton(image=icons["media-playlist-shuffle-symbolic"], tooltip_text=_("Random mode"))
self._random_button.set_can_focus(False)
self._repeat_button=Gtk.ToggleButton(image=icons["media-playlist-repeat-symbolic"], tooltip_text=_("Repeat mode"))
self._repeat_button.set_can_focus(False)
self._single_button=Gtk.ToggleButton(image=icons["org.mpdevil.mpdevil-single-symbolic"], tooltip_text=_("Single mode"))
self._single_button.set_can_focus(False)
self._consume_button=Gtk.ToggleButton(image=icons["org.mpdevil.mpdevil-consume-symbolic"], tooltip_text=_("Consume mode"))
self._consume_button.set_can_focus(False)
self._volume_button=Gtk.VolumeButton(use_symbolic=True, size=self._settings.get_gtk_icon_size("icon-size"))
self._volume_button.set_can_focus(False)
self._adj=self._volume_button.get_adjustment()
self._adj=self.get_adjustment()
self._adj.set_step_increment(0.05)
self._adj.set_page_increment(0.1)
self._adj.set_upper(0) # do not allow volume change by user when MPD has not yet reported volume (no output enabled/avail)
# connect
self._random_button_toggled=self._random_button.connect("toggled", self._set_option, "random")
self._repeat_button_toggled=self._repeat_button.connect("toggled", self._set_option, "repeat")
self._single_button_toggled=self._single_button.connect("toggled", self._set_option, "single")
self._consume_button_toggled=self._consume_button.connect("toggled", self._set_option, "consume")
self._volume_button_changed=self._volume_button.connect("value-changed", self._set_volume)
self._repeat_changed=self._client.emitter.connect("repeat", self._repeat_refresh)
self._random_changed=self._client.emitter.connect("random", self._random_refresh)
self._single_changed=self._client.emitter.connect("single", self._single_refresh)
self._consume_changed=self._client.emitter.connect("consume", self._consume_refresh)
self._volume_changed=self._client.emitter.connect("volume_changed", self._volume_refresh)
self._single_button.connect("button-press-event", self._on_single_button_press_event)
self._volume_button.connect("button-press-event", self._on_volume_button_press_event)
self._changed=self.connect("value-changed", self._set_volume)
self._client.emitter.connect("volume_changed", self._refresh)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
self.connect("button-press-event", self._on_button_press_event)
self._settings.connect("changed::icon-size", self._on_icon_size_changed)
def _set_volume(self, widget, value):
self._client.setvol(str(int(value*100)))
def _refresh(self, emitter, volume):
self.handler_block(self._changed)
if volume < 0:
self.set_value(0)
self._adj.set_upper(0)
else:
self._adj.set_upper(1)
self.set_value(volume/100)
self.handler_unblock(self._changed)
def _on_button_press_event(self, widget, event):
if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
self._popover=OutputPopover(self._client, self)
self._popover.popup()
def _on_reconnected(self, *args):
self.set_sensitive(True)
def _on_disconnected(self, *args):
self.set_sensitive(False)
self._refresh(None, -1)
if self._popover is not None:
self._popover.popdown()
self._popover=None
def _on_icon_size_changed(self, *args):
self.set_property("size", self._settings.get_gtk_icon_size("icon-size"))
class PlaybackOptions(Gtk.ButtonBox):
def __init__(self, client, settings):
super().__init__(layout_style=Gtk.ButtonBoxStyle.EXPAND)
self._client=client
self._settings=settings
# buttons
self._buttons={}
data=(
("repeat", "media-playlist-repeat-symbolic", _("Repeat mode")),
("random", "media-playlist-shuffle-symbolic", _("Random mode")),
("single", "org.mpdevil.mpdevil-single-symbolic", _("Single mode")),
("consume", "org.mpdevil.mpdevil-consume-symbolic", _("Consume mode")),
)
for name, icon, tooltip in data:
button=Gtk.ToggleButton(image=AutoSizedIcon(icon, "icon-size", self._settings), tooltip_text=tooltip, can_focus=False)
handler=button.connect("toggled", self._set_option, name)
self.pack_start(button, True, True, 0)
self._buttons[name]=(button, handler)
# css
self._provider=Gtk.CssProvider()
self._provider.load_from_data(b"""image {color: @error_color;}""") # red icon
# connect
for name in ("repeat", "random", "consume"):
self._client.emitter.connect(name, self._button_refresh, name)
self._client.emitter.connect("single", self._single_refresh)
self._buttons["single"][0].connect("button-press-event", self._on_single_button_press_event)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
self._settings.connect("notify::mini-player", self._on_mini_player)
self._settings.connect("changed::icon-size", self._on_icon_size_changed)
# packing
self._button_box=Gtk.ButtonBox(layout_style=Gtk.ButtonBoxStyle.EXPAND)
self._button_box.pack_start(self._repeat_button, True, True, 0)
self._button_box.pack_start(self._random_button, True, True, 0)
self._button_box.pack_start(self._single_button, True, True, 0)
self._button_box.pack_start(self._consume_button, True, True, 0)
self.pack_start(self._button_box, True, True, 0)
self.pack_start(self._volume_button, True, True, 0)
def _set_option(self, widget, option):
func=getattr(self._client, option)
@@ -3532,51 +3488,19 @@ class PlaybackOptions(Gtk.Box):
else:
func("0")
def _set_volume(self, widget, value):
self._client.setvol(str(int(value*100)))
def _repeat_refresh(self, emitter, val):
self._repeat_button.handler_block(self._repeat_button_toggled)
self._repeat_button.set_active(val)
self._repeat_button.handler_unblock(self._repeat_button_toggled)
def _random_refresh(self, emitter, val):
self._random_button.handler_block(self._random_button_toggled)
self._random_button.set_active(val)
self._random_button.handler_unblock(self._random_button_toggled)
def _button_refresh(self, emitter, val, name):
self._buttons[name][0].handler_block(self._buttons[name][1])
self._buttons[name][0].set_active(val)
self._buttons[name][0].handler_unblock(self._buttons[name][1])
def _single_refresh(self, emitter, val):
self._single_button.handler_block(self._single_button_toggled)
if val == "1":
self._single_button.get_style_context().remove_class("destructive-action")
self._single_button.set_active(True)
elif val == "oneshot":
self._single_button.get_style_context().add_class("destructive-action")
self._single_button.set_active(False)
self._buttons["single"][0].handler_block(self._buttons["single"][1])
self._buttons["single"][0].set_active((val in ("1", "oneshot")))
if val == "oneshot":
self._buttons["single"][0].get_image().get_style_context().add_provider(self._provider, 600)
else:
self._single_button.get_style_context().remove_class("destructive-action")
self._single_button.set_active(False)
self._single_button.handler_unblock(self._single_button_toggled)
def _consume_refresh(self, emitter, val):
self._consume_button.handler_block(self._consume_button_toggled)
self._consume_button.set_active(val)
self._consume_button.handler_unblock(self._consume_button_toggled)
def _volume_refresh(self, emitter, volume):
self._volume_button.handler_block(self._volume_button_changed)
if volume < 0:
self._volume_button.set_value(0)
self._adj.set_upper(0)
else:
self._adj.set_upper(1)
self._volume_button.set_value(volume/100)
self._volume_button.handler_unblock(self._volume_button_changed)
def _on_volume_button_press_event(self, widget, event):
if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
self._popover=OutputPopover(self._client, self._volume_button)
self._popover.popup()
self._buttons["single"][0].get_image().get_style_context().remove_provider(self._provider)
self._buttons["single"][0].handler_unblock(self._buttons["single"][1])
def _on_single_button_press_event(self, widget, event):
if event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
@@ -3586,38 +3510,22 @@ class PlaybackOptions(Gtk.Box):
else:
self._client.single("oneshot")
def _on_reconnected(self, *args):
self._repeat_button.set_sensitive(True)
self._random_button.set_sensitive(True)
self._single_button.set_sensitive(True)
self._consume_button.set_sensitive(True)
self._volume_button.set_sensitive(True)
def _on_disconnected(self, *args):
self._repeat_button.set_sensitive(False)
self._random_button.set_sensitive(False)
self._single_button.set_sensitive(False)
self._consume_button.set_sensitive(False)
self._volume_button.set_sensitive(False)
self._repeat_refresh(None, False)
self._random_refresh(None, False)
self.set_sensitive(False)
for name in ("repeat", "random", "consume"):
self._button_refresh(None, False, name)
self._single_refresh(None, "0")
self._consume_refresh(None, False)
self._volume_refresh(None, -1)
if self._popover is not None:
self._popover.destroy()
self._popover=None
def _on_reconnected(self, *args):
self.set_sensitive(True)
def _on_mini_player(self, obj, typestring):
if obj.get_property("mini-player"):
self._button_box.set_property("no-show-all", True)
self._button_box.set_property("visible", False)
self.set_property("no-show-all", True)
self.set_property("visible", False)
else:
self._button_box.set_property("no-show-all", False)
self._button_box.show_all()
def _on_icon_size_changed(self, *args):
self._volume_button.set_property("size", self._settings.get_gtk_icon_size("icon-size"))
self.set_property("no-show-all", False)
self.show_all()
###################
# MPD gio actions #
@@ -3760,6 +3668,8 @@ class ConnectionNotify(Gtk.Revealer):
close_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("window-close-symbolic", Gtk.IconSize.BUTTON))
close_button.set_relief(Gtk.ReliefStyle.NONE)
connect_button=Gtk.Button(label=_("Connect"))
settings_button=Gtk.Button(label=_("Settings"))
settings_button.set_action_name("win.profile-settings")
# connect
close_button.connect("clicked", self._on_close_button_clicked)
@@ -3773,6 +3683,7 @@ class ConnectionNotify(Gtk.Revealer):
box.pack_start(self._label, False, True, 6)
box.pack_end(close_button, False, True, 0)
box.pack_end(connect_button, False, True, 0)
box.pack_end(settings_button, False, True, 0)
self.add(box)
def _on_connection_error(self, *args):
@@ -3813,7 +3724,7 @@ class MainWindow(Gtk.ApplicationWindow):
# actions
simple_actions_data=(
"settings","stats","help","menu",
"settings","profile-settings","stats","help","menu",
"toggle-lyrics","back-to-current-album","toggle-search",
"profile-next","profile-prev","show-info","append","play","enqueue"
)
@@ -3826,8 +3737,6 @@ class MainWindow(Gtk.ApplicationWindow):
self._profiles_action=Gio.SimpleAction.new_stateful("profiles", GLib.VariantType.new("i"), GLib.Variant("i", 0))
self._profiles_action.connect("change-state", self._on_profiles)
self.add_action(self._profiles_action)
self._mpd_action_group=MPDActionGroup(self._client)
self.insert_action_group("mpd", self._mpd_action_group)
# shortcuts
shortcuts_window=ShortcutsWindow()
@@ -3846,6 +3755,7 @@ class MainWindow(Gtk.ApplicationWindow):
playback_control=PlaybackControl(self._client, self._settings)
seek_bar=SeekBar(self._client)
playback_options=PlaybackOptions(self._client, self._settings)
volume_button=VolumeButton(self._client, self._settings)
connection_notify=ConnectionNotify(self._client, self._settings)
# menu
@@ -3877,6 +3787,7 @@ class MainWindow(Gtk.ApplicationWindow):
action_bar.pack_start(playback_control)
action_bar.pack_start(seek_bar)
action_bar.pack_start(playback_options)
action_bar.pack_start(volume_button)
# connect
self._settings.connect("changed::profiles", self._refresh_profiles_menu)
@@ -3887,7 +3798,6 @@ class MainWindow(Gtk.ApplicationWindow):
self._client.emitter.connect("current_song_changed", self._on_song_changed)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
self._browser.connect("search-focus-changed", self._on_search_focus_changed)
# auto save window state and size
self.connect("size-allocate", self._on_size_allocate)
self._settings.bind("maximize", self, "is-maximized", Gio.SettingsBindFlags.SET)
@@ -3953,6 +3863,11 @@ class MainWindow(Gtk.ApplicationWindow):
settings.run()
settings.destroy()
def _on_profile_settings(self, action, param):
settings=SettingsDialog(self, self._client, self._settings, "profiles")
settings.run()
settings.destroy()
def _on_stats(self, action, param):
stats=ServerStats(self, self._client, self._settings)
stats.destroy()
@@ -4026,9 +3941,6 @@ class MainWindow(Gtk.ApplicationWindow):
for action in ("stats","toggle-lyrics","back-to-current-album","toggle-search"):
self.lookup_action(action).set_enabled(False)
def _on_search_focus_changed(self, obj, focus):
self._mpd_action_group.lookup_action("toggle-play").set_enabled(not(focus))
def _on_size_allocate(self, widget, rect):
if not self.is_maximized() and not self._settings.get_property("mini-player"):
size=self.get_size()
@@ -4089,6 +4001,7 @@ class mpdevil(Gtk.Application):
if not self._window: # allow just one instance
self._window=MainWindow(self, self._client, self._settings)
self._window.connect("delete-event", self._on_delete_event)
self._window.insert_action_group("mpd", MPDActionGroup(self._client))
# accelerators
action_accels=(
("app.quit", ["<Control>q"]),("win.mini-player", ["<Control>m"]),("win.help", ["F1"]),("win.menu", ["F10"]),

View File

@@ -23,7 +23,7 @@
</ul>
</description>
<releases>
<release version="1.2.0" date="2021-04-21"/>
<release version="1.2.1" date="2021-05-04"/>
</releases>
<launchable type="desktop-id">org.mpdevil.mpdevil.desktop</launchable>
<screenshots>

View File

@@ -4,7 +4,7 @@ import DistUtilsExtra.auto
DistUtilsExtra.auto.setup(
name='mpdevil',
version='1.2.0', # sync with bin/mpdevil
version='1.2.1', # sync with bin/mpdevil
author="Martin Wagner",
author_email="martin.wagner.dev@gmail.com",
description=('A simple music browser for MPD'),