diff --git a/bin/mpdevil b/bin/mpdevil
index dc89d0d..87324fb 100755
--- a/bin/mpdevil
+++ b/bin/mpdevil
@@ -449,138 +449,6 @@ class MPRISInterface(dbus.service.Object): # TODO emit Seeked if needed
pid=None
loop.quit()
-#################################
-# small general purpose widgets #
-#################################
-
-class PixelSizedIcon(Gtk.Image):
- def __init__(self, icon_name, pixel_size):
- super().__init__()
- self.set_from_icon_name(icon_name, Gtk.IconSize.BUTTON)
- if pixel_size > 0:
- self.set_pixel_size(pixel_size)
-
-class FocusFrame(Gtk.Overlay):
- def __init__(self):
- super().__init__()
-
- self._frame=Gtk.Frame()
- self._frame.set_no_show_all(True)
-
- # css
- style_context=self._frame.get_style_context()
- provider=Gtk.CssProvider()
- css=b"""* {border-color: @theme_selected_bg_color; border-width: 2px;}"""
- provider.load_from_data(css)
- style_context.add_provider(provider, 800)
-
- self.add_overlay(self._frame)
- self.set_overlay_pass_through(self._frame, True)
-
- def set_widget(self, widget):
- widget.connect("focus-in-event", self._on_focus_in_event)
- widget.connect("focus-out-event", self._on_focus_out_event)
-
- def _on_focus_in_event(self, *args):
- self._frame.show()
-
- def _on_focus_out_event(self, *args):
- self._frame.hide()
-
-class SongPopover(Gtk.Popover):
- def __init__(self, song, relative, x, y):
- super().__init__()
- rect=Gdk.Rectangle()
- rect.x=x
- # Gtk places popovers 26px above the given position for no obvious reasons, so I move them 26px
- rect.y=y+26
- rect.width = 1
- rect.height = 1
- self.set_pointing_to(rect)
- self.set_relative_to(relative)
-
- # Store
- # (tag, display-value, tooltip)
- store=Gtk.ListStore(str, str, str)
-
- # TreeView
- treeview=Gtk.TreeView(model=store, headers_visible=False, can_focus=False, search_column=-1, tooltip_column=2)
- sel=treeview.get_selection()
- sel.set_mode(Gtk.SelectionMode.NONE)
-
- frame=Gtk.Frame(border_width=3)
- frame.add(treeview)
-
- # Column
- renderer_text=Gtk.CellRendererText(width_chars=50, ellipsize=Pango.EllipsizeMode.MIDDLE, ellipsize_set=True)
- renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
-
- column_tag=Gtk.TreeViewColumn(_("MPD-Tag"), renderer_text_ralign, text=0)
- column_tag.set_property("resizable", False)
- treeview.append_column(column_tag)
-
- column_value=Gtk.TreeViewColumn(_("Value"), renderer_text, text=1)
- column_value.set_property("resizable", False)
- treeview.append_column(column_value)
-
- # packing
- self.add(frame)
-
- song=ClientHelper.song_to_str_dict(song)
- for tag, value in song.items():
- tooltip=value.replace("&", "&")
- if tag == "time":
- store.append([tag+":", str(datetime.timedelta(seconds=int(value))), tooltip])
- elif tag == "last-modified":
- time=datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
- store.append([tag+":", time.strftime('%a %d %B %Y, %H:%M UTC'), tooltip])
- else:
- store.append([tag+":", value, tooltip])
- frame.show_all()
-
-class Cover(object):
- def __init__(self, settings, song):
- self.path=None
- if song != {}:
- song_file=song["file"]
-
- active_profile=settings.get_int("active-profile")
-
- lib_path=settings.get_value("paths")[active_profile]
- regex_str=settings.get_value("regex")[active_profile]
-
- if regex_str == "":
- regex=re.compile(r''+COVER_REGEX+'', flags=re.IGNORECASE)
- else:
- try:
- artist=song["albumartist"]
- except:
- artist=""
- try:
- album=song["album"]
- except:
- album=""
- regex_str=regex_str.replace("%AlbumArtist%", artist)
- regex_str=regex_str.replace("%Album%", album)
- try:
- regex=re.compile(r''+regex_str+'', flags=re.IGNORECASE)
- except:
- print("illegal regex:", regex_str)
-
- if song_file is not None:
- head, tail=os.path.split(song_file)
- song_dir=os.path.join(lib_path, head)
- if os.path.exists(song_dir):
- for f in os.listdir(song_dir):
- if regex.match(f):
- self.path=os.path.join(song_dir, f)
- break
-
- def get_pixbuf(self, size):
- if self.path is None: # fallback needed
- self.path=Gtk.IconTheme.get_default().lookup_icon("media-optical", size, Gtk.IconLookupFlags.FORCE_SVG).get_filename()
- return GdkPixbuf.Pixbuf.new_from_file_at_size(self.path, size, size)
-
######################
# MPD client wrapper #
######################
@@ -867,130 +735,736 @@ class Settings(Gio.Settings):
else:
return ("artist")
-###########
-# browser #
-###########
+###################
+# settings dialog #
+###################
-class SearchWindow(Gtk.Box):
- def __init__(self, client):
- super().__init__(orientation=Gtk.Orientation.VERTICAL)
+class GeneralSettings(Gtk.Box):
+ def __init__(self, settings):
+ super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=6, border_width=18)
+
+ # adding vars
+ self._settings=settings
+ self._settings_handlers=[]
+
+ # int_settings
+ int_settings={}
+ int_settings_data=[
+ (_("Main cover size:"), (100, 1200, 10), "track-cover"),
+ (_("Album view cover size:"), (50, 600, 10), "album-cover"),
+ (_("Action bar icon size:"), (16, 64, 2), "icon-size"),
+ (_("Secondary icon size:"), (16, 64, 2), "icon-size-sec")
+ ]
+ 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::"+key, self._on_int_settings_changed, int_settings[key][1])
+ )
+
+ # combo_settings
+ combo_settings={}
+ combo_settings_data=[
+ (_("Sort albums by:"), _("name"), _("year"), "sort-albums-by-year"),
+ (_("Position of playlist:"), _("bottom"), _("right"), "playlist-right")
+ ]
+ for label, vfalse, vtrue, key in combo_settings_data:
+ combo_settings[key]=(Gtk.Label(label=label, xalign=0), Gtk.ComboBoxText(entry_text_column=0))
+ combo_settings[key][1].append_text(vfalse)
+ combo_settings[key][1].append_text(vtrue)
+ if self._settings.get_boolean(key):
+ combo_settings[key][1].set_active(1)
+ else:
+ combo_settings[key][1].set_active(0)
+ combo_settings[key][1].connect("changed", self._on_combo_changed, key)
+ self._settings_handlers.append(
+ self._settings.connect("changed::"+key, self._on_combo_settings_changed, combo_settings[key][1])
+ )
+
+ # check buttons
+ check_buttons={}
+ check_buttons_data=[
+ (_("Use Client-side decoration"), "use-csd"),
+ (_("Show stop button"), "show-stop"),
+ (_("Show lyrics button"), "show-lyrics-button"),
+ (_("Show initials in artist view"), "show-initials"),
+ (_("Show tooltips in album view"), "show-album-view-tooltips"),
+ (_("Use 'Album Artist' tag"), "use-album-artist"),
+ (_("Send notification on title change"), "send-notify"),
+ (_("Stop playback on quit"), "stop-on-quit"),
+ (_("Play selected albums and titles immediately"), "force-mode")
+ ]
+
+ for label, key in check_buttons_data:
+ check_buttons[key]=Gtk.CheckButton(label=label)
+ 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::"+key, self._on_check_settings_changed, check_buttons[key])
+ )
+
+ # headings
+ view_heading=Gtk.Label(label=_("View"), use_markup=True, xalign=0)
+ behavior_heading=Gtk.Label(label=_("Behavior"), 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.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)
+ view_grid.attach_next_to(int_settings["icon-size-sec"][0], int_settings["icon-size"][0], Gtk.PositionType.BOTTOM, 1, 1)
+ view_grid.attach_next_to(combo_settings["playlist-right"][0], int_settings["icon-size-sec"][0], Gtk.PositionType.BOTTOM, 1, 1)
+ view_grid.attach_next_to(int_settings["track-cover"][1], int_settings["track-cover"][0], Gtk.PositionType.RIGHT, 1, 1)
+ view_grid.attach_next_to(int_settings["album-cover"][1], int_settings["album-cover"][0], Gtk.PositionType.RIGHT, 1, 1)
+ 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)
+ view_grid.attach_next_to(combo_settings["playlist-right"][1], combo_settings["playlist-right"][0], Gtk.PositionType.RIGHT, 1, 1)
+
+ # behavior grid
+ behavior_grid=Gtk.Grid(row_spacing=6, column_spacing=12)
+ behavior_grid.set_margin_start(12)
+ behavior_grid.add(combo_settings["sort-albums-by-year"][0])
+ behavior_grid.attach_next_to(
+ combo_settings["sort-albums-by-year"][1],
+ combo_settings["sort-albums-by-year"][0],
+ Gtk.PositionType.RIGHT, 1, 1
+ )
+
+ # connect
+ self.connect("destroy", self._remove_handlers)
+
+ # packing
+ box=Gtk.Box(spacing=12)
+ box.pack_start(check_buttons["use-csd"], False, False, 0)
+ box.pack_start(Gtk.Label(label=_("(restart required)"), sensitive=False), False, False, 0)
+ self.pack_start(view_heading, False, False, 0)
+ self.pack_start(box, False, False, 0)
+ self.pack_start(check_buttons["show-stop"], False, False, 0)
+ self.pack_start(check_buttons["show-lyrics-button"], False, False, 0)
+ self.pack_start(check_buttons["show-initials"], False, False, 0)
+ self.pack_start(check_buttons["show-album-view-tooltips"], False, False, 0)
+ self.pack_start(view_grid, False, False, 0)
+ self.pack_start(behavior_heading, False, False, 0)
+ self.pack_start(check_buttons["use-album-artist"], False, False, 0)
+ self.pack_start(check_buttons["send-notify"], False, False, 0)
+ self.pack_start(check_buttons["stop-on-quit"], False, False, 0)
+ self.pack_start(check_buttons["force-mode"], False, False, 0)
+ self.pack_start(behavior_grid, 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_combo_settings_changed(self, settings, key, combo):
+ if settings.get_boolean(key):
+ combo.set_active(1)
+ else:
+ combo.set_active(0)
+
+ 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_combo_changed(self, box, key):
+ active=box.get_active()
+ if active == 0:
+ self._settings.set_boolean(key, False)
+ else:
+ self._settings.set_boolean(key, True)
+
+ 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)
# adding vars
self._client=client
+ self._settings=settings
+ self._gui_modification=False # indicates whether the settings were changed from the settings dialog
- # tag switcher
- self._tags=Gtk.ComboBoxText()
+ # widgets
+ self._profiles_combo=Gtk.ComboBoxText(entry_text_column=0, hexpand=True)
- # search entry
- self.search_entry=Gtk.SearchEntry()
+ add_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("list-add", Gtk.IconSize.BUTTON))
+ delete_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("list-remove", 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)
- # label
- self._hits_label=Gtk.Label(xalign=1)
+ connect_button=Gtk.Button(label=_("Connect"), image=Gtk.Image.new_from_icon_name("system-run", Gtk.IconSize.BUTTON))
- # store
- # (track, title, artist, album, duration, file)
- self._store=Gtk.ListStore(int, str, str, str, str, str)
+ 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)
+ address_entry.pack_start(self._host_entry, True, True, 0)
+ address_entry.pack_start(self._port_entry, False, False, 0)
+ self._password_entry=Gtk.Entry(hexpand=True, visibility=False)
+ self._path_entry=Gtk.Entry(hexpand=True)
+ self._path_select_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("folder-open", 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._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.")
+ )
- # songs window
- self._songs_window=SongsWindow(self._client, self._store, 5)
-
- # action bar
- self._action_bar=self._songs_window.get_action_bar()
- self._action_bar.set_sensitive(False)
-
- # songs view
- self._songs_view=self._songs_window.get_treeview()
-
- # columns
- renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
- renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
-
- column_track=Gtk.TreeViewColumn(_("No"), renderer_text_ralign, text=0)
- column_track.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
- column_track.set_property("resizable", False)
- self._songs_view.append_column(column_track)
-
- column_title=Gtk.TreeViewColumn(_("Title"), renderer_text, text=1)
- column_title.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
- column_title.set_property("resizable", False)
- column_title.set_property("expand", True)
- self._songs_view.append_column(column_title)
-
- column_artist=Gtk.TreeViewColumn(_("Artist"), renderer_text, text=2)
- column_artist.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
- column_artist.set_property("resizable", False)
- column_artist.set_property("expand", True)
- self._songs_view.append_column(column_artist)
-
- column_album=Gtk.TreeViewColumn(_("Album"), renderer_text, text=3)
- column_album.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
- column_album.set_property("resizable", False)
- column_album.set_property("expand", True)
- self._songs_view.append_column(column_album)
-
- column_time=Gtk.TreeViewColumn(_("Length"), renderer_text, text=4)
- column_time.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
- column_time.set_property("resizable", False)
- self._songs_view.append_column(column_time)
-
- column_track.set_sort_column_id(0)
- column_title.set_sort_column_id(1)
- column_artist.set_sort_column_id(2)
- column_album.set_sort_column_id(3)
- column_time.set_sort_column_id(4)
+ 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)
+ regex_label=Gtk.Label(label=_("Cover regex:"), xalign=1)
# connect
- self.search_entry.connect("search-changed", self._on_search_changed)
- self._tags.connect("changed", self._on_search_changed)
- self._client.emitter.connect("reconnected", self._on_reconnected)
- self._client.emitter.connect("disconnected", self._on_disconnected)
+ add_button.connect("clicked", self._on_add_button_clicked)
+ delete_button.connect("clicked", self._on_delete_button_clicked)
+ connect_button.connect("clicked", self._on_connect_button_clicked)
+ self._path_select_button.connect("clicked", self._on_path_select_button_clicked, parent)
+ self._profiles_combo.connect("changed", self._on_profiles_changed)
+ 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._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
- hbox=Gtk.Box(spacing=6, border_width=6)
- hbox.pack_start(self.search_entry, True, True, 0)
- hbox.pack_end(self._tags, False, False, 0)
- self._hits_label.set_margin_end(6)
- self._action_bar.pack_end(self._hits_label)
- self.pack_start(hbox, False, False, 0)
- self.pack_start(Gtk.Separator.new(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0)
- self.pack_start(self._songs_window, True, True, 0)
+ 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(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._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)
- def clear(self, *args):
- self._songs_view.clear()
- self.search_entry.set_text("")
- self._tags.remove_all()
+ def _block_entry_changed_handlers(self, *args):
+ for obj, handler in self.entry_changed_handlers:
+ obj.handler_block(handler)
- def _on_disconnected(self, *args):
- self._tags.set_sensitive(False)
- self.search_entry.set_sensitive(False)
- self.clear()
+ def _unblock_entry_changed_handlers(self, *args):
+ for obj, handler in self.entry_changed_handlers:
+ obj.handler_unblock(handler)
- def _on_reconnected(self, *args):
- self._tags.append_text("any")
- for tag in self._client.wrapped_call("tagtypes"):
- if not tag.startswith("MUSICBRAINZ"):
- self._tags.append_text(tag)
- self._tags.set_active(0)
- self._tags.set_sensitive(True)
- self.search_entry.set_sensitive(True)
+ def _profiles_combo_reload(self, *args):
+ self._block_entry_changed_handlers()
- def _on_search_changed(self, widget):
- self._songs_view.clear()
- self._hits_label.set_text("")
- if len(self.search_entry.get_text()) > 1:
- songs=self._client.wrapped_call("search", self._tags.get_active_text(), self.search_entry.get_text())
- for s in songs:
- song=ClientHelper.extend_song_for_display(ClientHelper.song_to_str_dict(s))
- self._store.append([
- int(song["track"]), song["title"],
- song["artist"], song["album"],
- song["human_duration"], song["file"]
- ])
- self._hits_label.set_text(_("%i hits") % (self._songs_view.count()))
- if self._songs_view.count() == 0:
- self._action_bar.set_sensitive(False)
+ self._profiles_combo.remove_all()
+ for profile in self._settings.get_value("profiles"):
+ self._profiles_combo.append_text(profile)
+
+ self._unblock_entry_changed_handlers()
+
+ 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._action_bar.set_sensitive(True)
+ self._profiles_combo_reload()
+ self._profiles_combo.set_active(0)
+
+ def _on_add_button_clicked(self, *args):
+ model=self._profiles_combo.get_model()
+ self._settings.array_append('as', "profiles", "new profile ("+str(len(model))+")")
+ self._settings.array_append('as', "hosts", "localhost")
+ self._settings.array_append('ai', "ports", 6600)
+ self._settings.array_append('as', "passwords", "")
+ self._settings.array_append('as', "paths", "")
+ self._settings.array_append('as', "regex", "")
+ self._profiles_combo_reload()
+ new_pos=len(model)-1
+ self._profiles_combo.set_active(new_pos)
+
+ def _on_delete_button_clicked(self, *args):
+ pos=self._profiles_combo.get_active()
+ self._settings.array_delete('as', "profiles", pos)
+ self._settings.array_delete('as', "hosts", pos)
+ self._settings.array_delete('ai', "ports", pos)
+ self._settings.array_delete('as', "passwords", pos)
+ self._settings.array_delete('as', "paths", pos)
+ self._settings.array_delete('as', "regex", pos)
+ if len(self._settings.get_value("profiles")) == 0:
+ self._on_add_button_clicked()
+ else:
+ self._profiles_combo_reload()
+ new_pos=max(pos-1,0)
+ 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()
+
+ 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.set_active(pos)
+
+ 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())
+
+ 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()))
+
+ 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())
+
+ 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())
+
+ 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())
+
+ def _on_path_select_button_clicked(self, widget, parent):
+ dialog=Gtk.FileChooserDialog(title=_("Choose directory"), transient_for=parent, action=Gtk.FileChooserAction.SELECT_FOLDER)
+ dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
+ dialog.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK)
+ dialog.set_default_size(800, 400)
+ dialog.set_current_folder(self._settings.get_value("paths")[self._profiles_combo.get_active()])
+ response=dialog.run()
+ if response == Gtk.ResponseType.OK:
+ self._gui_modification=True
+ self._settings.array_modify('as', "paths", self._profiles_combo.get_active(), dialog.get_filename())
+ self._path_entry.set_text(dialog.get_filename())
+ dialog.destroy()
+
+ def _on_profiles_changed(self, *args):
+ active=self._profiles_combo.get_active()
+ 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])
+ self._path_entry.set_text(self._settings.get_value("paths")[active])
+ self._regex_entry.set_text(self._settings.get_value("regex")[active])
+
+ self._unblock_entry_changed_handlers()
+
+class PlaylistSettings(Gtk.Box):
+ def __init__(self, settings):
+ super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=6, border_width=18)
+
+ # adding vars
+ self._settings=settings
+
+ # label
+ label=Gtk.Label(label=_("Choose the order of information to appear in the playlist:"), wrap=True, xalign=0)
+
+ # Store
+ # (toggle, header, actual_index)
+ self._store=Gtk.ListStore(bool, str, int)
+
+ # TreeView
+ treeview=Gtk.TreeView(model=self._store, reorderable=True, headers_visible=False)
+ treeview.set_search_column(-1)
+
+ # selection
+ self._selection=treeview.get_selection()
+
+ # Column
+ renderer_text=Gtk.CellRendererText()
+ renderer_toggle=Gtk.CellRendererToggle()
+
+ column_toggle=Gtk.TreeViewColumn("", renderer_toggle, active=0)
+ treeview.append_column(column_toggle)
+
+ column_text=Gtk.TreeViewColumn("", renderer_text, text=1)
+ treeview.append_column(column_text)
+
+ # fill store
+ self._headers=[_("No"), _("Disc"), _("Title"), _("Artist"), _("Album"), _("Length"), _("Year"), _("Genre")]
+ self._fill()
+
+ # scroll
+ scroll=Gtk.ScrolledWindow()
+ scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
+ scroll.add(treeview)
+ frame=Gtk.Frame()
+ frame.add(scroll)
+
+ # Toolbar
+ toolbar=Gtk.Toolbar()
+ style_context=toolbar.get_style_context()
+ style_context.add_class("inline-toolbar")
+ self._up_button=Gtk.ToolButton.new(Gtk.Image.new_from_icon_name("go-up-symbolic", Gtk.IconSize.SMALL_TOOLBAR))
+ self._up_button.set_sensitive(False)
+ self._down_button=Gtk.ToolButton.new(Gtk.Image.new_from_icon_name("go-down-symbolic", Gtk.IconSize.SMALL_TOOLBAR))
+ self._down_button.set_sensitive(False)
+ toolbar.insert(self._up_button, 0)
+ toolbar.insert(self._down_button, 1)
+
+ # column chooser
+ column_chooser=Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+ column_chooser.pack_start(frame, True, True, 0)
+ column_chooser.pack_start(toolbar, False, False, 0)
+
+ # connect
+ self._row_deleted=self._store.connect("row-deleted", self._save_permutation)
+ renderer_toggle.connect("toggled", self._on_cell_toggled)
+ 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)
+ self.pack_start(column_chooser, True, True, 0)
+
+ def _fill(self, *args):
+ visibilities=self._settings.get_value("column-visibilities").unpack()
+ for actual_index in self._settings.get_value("column-permutation"):
+ self._store.append([visibilities[actual_index], self._headers[actual_index], actual_index])
+
+ def _save_permutation(self, *args):
+ permutation=[]
+ for row in self._store:
+ permutation.append(row[2])
+ self._settings.set_value("column-permutation", GLib.Variant("ai", permutation))
+
+ def _set_button_sensitivity(self, *args):
+ treeiter=self._selection.get_selected()[1]
+ if treeiter is None:
+ self._up_button.set_sensitive(False)
+ self._down_button.set_sensitive(False)
+ else:
+ path=self._store.get_path(treeiter)
+ if self._store.iter_next(treeiter) is None:
+ self._up_button.set_sensitive(True)
+ self._down_button.set_sensitive(False)
+ elif not path.prev():
+ self._up_button.set_sensitive(False)
+ self._down_button.set_sensitive(True)
+ else:
+ 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])
+
+ def _on_up_button_clicked(self, *args):
+ treeiter=self._selection.get_selected()[1]
+ path=self._store.get_path(treeiter)
+ path.prev()
+ prev=self._store.get_iter(path)
+ self._store.move_before(treeiter, prev)
+ self._set_button_sensitivity()
+ self._save_permutation()
+
+ def _on_down_button_clicked(self, *args):
+ treeiter=self._selection.get_selected()[1]
+ path=self._store.get_path(treeiter)
+ next=self._store.iter_next(treeiter)
+ self._store.move_after(treeiter, next)
+ 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):
+ use_csd=settings.get_boolean("use-csd")
+ if use_csd:
+ super().__init__(title=_("Settings"), transient_for=parent, use_header_bar=True)
+ # css
+ style_context=self.get_style_context()
+ provider=Gtk.CssProvider()
+ css=b"""* {-GtkDialog-content-area-border: 0px;}"""
+ provider.load_from_data(css)
+ style_context.add_provider(provider, 800)
+ else:
+ super().__init__(title=_("Settings"), transient_for=parent)
+ self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
+ self.set_default_size(500, 400)
+
+ # widgets
+ general=GeneralSettings(settings)
+ profiles=ProfileSettings(parent, client, settings)
+ playlist=PlaylistSettings(settings)
+
+ # packing
+ tabs=Gtk.Notebook()
+ tabs.append_page(general, Gtk.Label(label=_("General")))
+ tabs.append_page(profiles, Gtk.Label(label=_("Profiles")))
+ tabs.append_page(playlist, Gtk.Label(label=_("Playlist")))
+ vbox=self.get_content_area()
+ vbox.set_spacing(6)
+ vbox.pack_start(tabs, True, True, 0)
+
+ self.show_all()
+
+#################
+# other dialogs #
+#################
+
+class ServerStats(Gtk.Dialog):
+ def __init__(self, parent, client, settings):
+ use_csd=settings.get_boolean("use-csd")
+ if use_csd:
+ super().__init__(title=_("Stats"), transient_for=parent, use_header_bar=True)
+ # css
+ style_context=self.get_style_context()
+ provider=Gtk.CssProvider()
+ css=b"""* {-GtkDialog-content-area-border: 0px;}"""
+ provider.load_from_data(css)
+ style_context.add_provider(provider, 800)
+ else:
+ super().__init__(title=_("Stats"), transient_for=parent)
+ self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
+ self.set_resizable(False)
+
+ # Store
+ # (tag, value)
+ store=Gtk.ListStore(str, str)
+
+ # TreeView
+ treeview=Gtk.TreeView(model=store, headers_visible=False, can_focus=False)
+ treeview.set_search_column(-1)
+
+ # selection
+ sel=treeview.get_selection()
+ sel.set_mode(Gtk.SelectionMode.NONE)
+
+ # Column
+ renderer_text=Gtk.CellRendererText()
+ renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
+
+ column_tag=Gtk.TreeViewColumn("", renderer_text_ralign, text=0)
+ treeview.append_column(column_tag)
+
+ column_value=Gtk.TreeViewColumn("", renderer_text, text=1)
+ treeview.append_column(column_value)
+
+ store.append(["protocol:", str(client.mpd_version)])
+
+ stats=client.wrapped_call("stats")
+ for key in stats:
+ print_key=key+":"
+ if key == "uptime" or key == "playtime" or key == "db_playtime":
+ store.append([print_key, str(datetime.timedelta(seconds=int(stats[key])))])
+ elif key == "db_update":
+ store.append([print_key, str(datetime.datetime.fromtimestamp(int(stats[key])))])
+ else:
+ store.append([print_key, stats[key]])
+ frame=Gtk.Frame()
+ frame.add(treeview)
+ self.vbox.pack_start(frame, True, True, 0)
+ self.vbox.set_spacing(6)
+ self.show_all()
+ self.run()
+
+class AboutDialog(Gtk.AboutDialog):
+ def __init__(self, window):
+ super().__init__(transient_for=window, modal=True)
+ self.set_program_name("mpdevil")
+ self.set_version(VERSION)
+ self.set_comments(_("A simple music browser for MPD"))
+ self.set_authors(["Martin Wagner"])
+ self.set_website("https://github.com/SoongNoonien/mpdevil")
+ self.set_copyright("\xa9 2020 Martin Wagner")
+ self.set_logo_icon_name("mpdevil")
+
+#################################
+# small general purpose widgets #
+#################################
+
+class PixelSizedIcon(Gtk.Image):
+ def __init__(self, icon_name, pixel_size):
+ super().__init__()
+ self.set_from_icon_name(icon_name, Gtk.IconSize.BUTTON)
+ if pixel_size > 0:
+ self.set_pixel_size(pixel_size)
+
+class FocusFrame(Gtk.Overlay):
+ def __init__(self):
+ super().__init__()
+
+ self._frame=Gtk.Frame()
+ self._frame.set_no_show_all(True)
+
+ # css
+ style_context=self._frame.get_style_context()
+ provider=Gtk.CssProvider()
+ css=b"""* {border-color: @theme_selected_bg_color; border-width: 2px;}"""
+ provider.load_from_data(css)
+ style_context.add_provider(provider, 800)
+
+ self.add_overlay(self._frame)
+ self.set_overlay_pass_through(self._frame, True)
+
+ def set_widget(self, widget):
+ widget.connect("focus-in-event", self._on_focus_in_event)
+ widget.connect("focus-out-event", self._on_focus_out_event)
+
+ def _on_focus_in_event(self, *args):
+ self._frame.show()
+
+ def _on_focus_out_event(self, *args):
+ self._frame.hide()
+
+class SongPopover(Gtk.Popover):
+ def __init__(self, song, relative, x, y):
+ super().__init__()
+ rect=Gdk.Rectangle()
+ rect.x=x
+ # Gtk places popovers 26px above the given position for no obvious reasons, so I move them 26px
+ rect.y=y+26
+ rect.width = 1
+ rect.height = 1
+ self.set_pointing_to(rect)
+ self.set_relative_to(relative)
+
+ # Store
+ # (tag, display-value, tooltip)
+ store=Gtk.ListStore(str, str, str)
+
+ # TreeView
+ treeview=Gtk.TreeView(model=store, headers_visible=False, can_focus=False, search_column=-1, tooltip_column=2)
+ sel=treeview.get_selection()
+ sel.set_mode(Gtk.SelectionMode.NONE)
+
+ frame=Gtk.Frame(border_width=3)
+ frame.add(treeview)
+
+ # Column
+ renderer_text=Gtk.CellRendererText(width_chars=50, ellipsize=Pango.EllipsizeMode.MIDDLE, ellipsize_set=True)
+ renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
+
+ column_tag=Gtk.TreeViewColumn(_("MPD-Tag"), renderer_text_ralign, text=0)
+ column_tag.set_property("resizable", False)
+ treeview.append_column(column_tag)
+
+ column_value=Gtk.TreeViewColumn(_("Value"), renderer_text, text=1)
+ column_value.set_property("resizable", False)
+ treeview.append_column(column_value)
+
+ # packing
+ self.add(frame)
+
+ song=ClientHelper.song_to_str_dict(song)
+ for tag, value in song.items():
+ tooltip=value.replace("&", "&")
+ if tag == "time":
+ store.append([tag+":", str(datetime.timedelta(seconds=int(value))), tooltip])
+ elif tag == "last-modified":
+ time=datetime.datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
+ store.append([tag+":", time.strftime('%a %d %B %Y, %H:%M UTC'), tooltip])
+ else:
+ store.append([tag+":", value, tooltip])
+ frame.show_all()
+
+class Cover(object):
+ def __init__(self, settings, song):
+ self.path=None
+ if song != {}:
+ song_file=song["file"]
+
+ active_profile=settings.get_int("active-profile")
+
+ lib_path=settings.get_value("paths")[active_profile]
+ regex_str=settings.get_value("regex")[active_profile]
+
+ if regex_str == "":
+ regex=re.compile(r''+COVER_REGEX+'', flags=re.IGNORECASE)
+ else:
+ try:
+ artist=song["albumartist"]
+ except:
+ artist=""
+ try:
+ album=song["album"]
+ except:
+ album=""
+ regex_str=regex_str.replace("%AlbumArtist%", artist)
+ regex_str=regex_str.replace("%Album%", album)
+ try:
+ regex=re.compile(r''+regex_str+'', flags=re.IGNORECASE)
+ except:
+ print("illegal regex:", regex_str)
+
+ if song_file is not None:
+ head, tail=os.path.split(song_file)
+ song_dir=os.path.join(lib_path, head)
+ if os.path.exists(song_dir):
+ for f in os.listdir(song_dir):
+ if regex.match(f):
+ self.path=os.path.join(song_dir, f)
+ break
+
+ def get_pixbuf(self, size):
+ if self.path is None: # fallback needed
+ self.path=Gtk.IconTheme.get_default().lookup_icon("media-optical", size, Gtk.IconLookupFlags.FORCE_SVG).get_filename()
+ return GdkPixbuf.Pixbuf.new_from_file_at_size(self.path, size, size)
+
+###########
+# browser #
+###########
class SongsView(Gtk.TreeView):
def __init__(self, client, store, file_column_id):
@@ -1131,73 +1605,35 @@ class SongsWindow(Gtk.Box):
def _on_enqueue_button_clicked(self, *args):
self._client.wrapped_call("files_to_playlist", self._songs_view.get_files(), "enqueue")
-class AlbumDialog(Gtk.Dialog):
- def __init__(self, parent, client, settings, album, album_artist, year):
- use_csd=settings.get_boolean("use-csd")
- if use_csd:
- super().__init__(transient_for=parent, use_header_bar=True)
- else:
- super().__init__(transient_for=parent)
-
- # css
- style_context=self.get_style_context()
- provider=Gtk.CssProvider()
- if use_csd:
- css=b"""* {-GtkDialog-content-area-border: 0px;}"""
- else:
- css=b"""* {-GtkDialog-action-area-border: 0px;}"""
- provider.load_from_data(css)
- style_context.add_provider(provider, 800)
+class SearchWindow(Gtk.Box):
+ def __init__(self, client):
+ super().__init__(orientation=Gtk.Orientation.VERTICAL)
# adding vars
self._client=client
- self._settings=settings
- songs=self._client.wrapped_call("find", "album", album, "date", year, self._settings.get_artist_type(), album_artist)
- # determine size
- size=parent.get_size()
- diagonal=(size[0]**2+size[1]**2)**(0.5)
- h=diagonal//4
- w=h*5//4
- self.set_default_size(w, h)
+ # tag switcher
+ self._tags=Gtk.ComboBoxText()
- # title
- album_duration=ClientHelper.calc_display_length(songs)
- if year == "":
- self.set_title(album_artist+" - "+album+" ("+album_duration+")")
- else:
- self.set_title(album_artist+" - "+album+" ("+year+") ("+album_duration+")")
+ # search entry
+ self.search_entry=Gtk.SearchEntry()
+
+ # label
+ self._hits_label=Gtk.Label(xalign=1)
# store
- # (track, title (artist), duration, file)
- store=Gtk.ListStore(int, str, str, str)
- for s in songs:
- song=ClientHelper.extend_song_for_display(s)
- if type(song["title"]) == list: # could be impossible
- title=(', '.join(song["title"]))
- else:
- title=song["title"]
- if type(song["artist"]) == list:
- try:
- song["artist"].remove(album_artist)
- except:
- pass
- artist=(', '.join(song["artist"]))
- else:
- artist=song["artist"]
- if artist == album_artist:
- title_artist=""+title+""
- else:
- title_artist=""+title+" - "+artist
-
- title_artist=title_artist.replace("&", "&")
- store.append([int(song["track"]), title_artist, song["human_duration"], song["file"]])
+ # (track, title, artist, album, duration, file)
+ self._store=Gtk.ListStore(int, str, str, str, str, str)
# songs window
- songs_window=SongsWindow(self._client, store, 3)
+ self._songs_window=SongsWindow(self._client, self._store, 5)
+
+ # action bar
+ self._action_bar=self._songs_window.get_action_bar()
+ self._action_bar.set_sensitive(False)
# songs view
- songs_view=songs_window.get_treeview()
+ self._songs_view=self._songs_window.get_treeview()
# columns
renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
@@ -1206,38 +1642,89 @@ class AlbumDialog(Gtk.Dialog):
column_track=Gtk.TreeViewColumn(_("No"), renderer_text_ralign, text=0)
column_track.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
column_track.set_property("resizable", False)
- songs_view.append_column(column_track)
+ self._songs_view.append_column(column_track)
- column_title=Gtk.TreeViewColumn(_("Title"), renderer_text, markup=1)
+ column_title=Gtk.TreeViewColumn(_("Title"), renderer_text, text=1)
column_title.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
column_title.set_property("resizable", False)
column_title.set_property("expand", True)
- songs_view.append_column(column_title)
+ self._songs_view.append_column(column_title)
- column_time=Gtk.TreeViewColumn(_("Length"), renderer_text, text=2)
+ column_artist=Gtk.TreeViewColumn(_("Artist"), renderer_text, text=2)
+ column_artist.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ column_artist.set_property("resizable", False)
+ column_artist.set_property("expand", True)
+ self._songs_view.append_column(column_artist)
+
+ column_album=Gtk.TreeViewColumn(_("Album"), renderer_text, text=3)
+ column_album.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ column_album.set_property("resizable", False)
+ column_album.set_property("expand", True)
+ self._songs_view.append_column(column_album)
+
+ column_time=Gtk.TreeViewColumn(_("Length"), renderer_text, text=4)
column_time.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
column_time.set_property("resizable", False)
- songs_view.append_column(column_time)
+ self._songs_view.append_column(column_time)
- # close button
- close_button=Gtk.ToggleButton(image=Gtk.Image.new_from_icon_name("window-close", Gtk.IconSize.BUTTON), label=_("Close"))
-
- # action bar
- action_bar=songs_window.get_action_bar()
- action_bar.pack_end(close_button)
+ column_track.set_sort_column_id(0)
+ column_title.set_sort_column_id(1)
+ column_artist.set_sort_column_id(2)
+ column_album.set_sort_column_id(3)
+ column_time.set_sort_column_id(4)
# connect
- close_button.connect("clicked", self._on_close_button_clicked)
+ self.search_entry.connect("search-changed", self._on_search_changed)
+ self._tags.connect("changed", self._on_search_changed)
+ self._client.emitter.connect("reconnected", self._on_reconnected)
+ self._client.emitter.connect("disconnected", self._on_disconnected)
# packing
- self.vbox.pack_start(songs_window, True, True, 0) # vbox default widget of dialogs
- self.show_all()
+ hbox=Gtk.Box(spacing=6, border_width=6)
+ hbox.pack_start(self.search_entry, True, True, 0)
+ hbox.pack_end(self._tags, False, False, 0)
+ self._hits_label.set_margin_end(6)
+ self._action_bar.pack_end(self._hits_label)
+ self.pack_start(hbox, False, False, 0)
+ self.pack_start(Gtk.Separator.new(orientation=Gtk.Orientation.HORIZONTAL), False, False, 0)
+ self.pack_start(self._songs_window, True, True, 0)
- def open(self):
- response=self.run()
+ def clear(self, *args):
+ self._songs_view.clear()
+ self.search_entry.set_text("")
+ self._tags.remove_all()
- def _on_close_button_clicked(self, *args):
- self.destroy()
+ def _on_disconnected(self, *args):
+ self._tags.set_sensitive(False)
+ self.search_entry.set_sensitive(False)
+ self.clear()
+
+ def _on_reconnected(self, *args):
+ self._tags.append_text("any")
+ for tag in self._client.wrapped_call("tagtypes"):
+ if not tag.startswith("MUSICBRAINZ"):
+ self._tags.append_text(tag)
+ self._tags.set_active(0)
+ self._tags.set_sensitive(True)
+ self.search_entry.set_sensitive(True)
+
+ def _on_search_changed(self, widget):
+ self._songs_view.clear()
+ self._hits_label.set_text("")
+ if len(self.search_entry.get_text()) > 1:
+ songs=self._client.wrapped_call("search", self._tags.get_active_text(), self.search_entry.get_text())
+ for s in songs:
+ song=ClientHelper.extend_song_for_display(ClientHelper.song_to_str_dict(s))
+ self._store.append([
+ int(song["track"]), song["title"],
+ song["artist"], song["album"],
+ song["human_duration"], song["file"]
+ ])
+ self._hits_label.set_text(_("%i hits") % (self._songs_view.count()))
+ if self._songs_view.count() == 0:
+ self._action_bar.set_sensitive(False)
+ else:
+ self._action_bar.set_sensitive(True)
class GenreSelect(Gtk.ComboBoxText):
__gsignals__={'genre_changed': (GObject.SignalFlags.RUN_FIRST, None, ())}
@@ -1418,6 +1905,114 @@ class ArtistWindow(FocusFrame):
def _on_show_initials_changed(self, *args):
self._column_initials.set_visible(self._settings.get_boolean("show-initials"))
+class AlbumDialog(Gtk.Dialog): # also used by 'MainCover'
+ def __init__(self, parent, client, settings, album, album_artist, year):
+ use_csd=settings.get_boolean("use-csd")
+ if use_csd:
+ super().__init__(transient_for=parent, use_header_bar=True)
+ else:
+ super().__init__(transient_for=parent)
+
+ # css
+ style_context=self.get_style_context()
+ provider=Gtk.CssProvider()
+ if use_csd:
+ css=b"""* {-GtkDialog-content-area-border: 0px;}"""
+ else:
+ css=b"""* {-GtkDialog-action-area-border: 0px;}"""
+ provider.load_from_data(css)
+ style_context.add_provider(provider, 800)
+
+ # adding vars
+ self._client=client
+ self._settings=settings
+ songs=self._client.wrapped_call("find", "album", album, "date", year, self._settings.get_artist_type(), album_artist)
+
+ # determine size
+ size=parent.get_size()
+ diagonal=(size[0]**2+size[1]**2)**(0.5)
+ h=diagonal//4
+ w=h*5//4
+ self.set_default_size(w, h)
+
+ # title
+ album_duration=ClientHelper.calc_display_length(songs)
+ if year == "":
+ self.set_title(album_artist+" - "+album+" ("+album_duration+")")
+ else:
+ self.set_title(album_artist+" - "+album+" ("+year+") ("+album_duration+")")
+
+ # store
+ # (track, title (artist), duration, file)
+ store=Gtk.ListStore(int, str, str, str)
+ for s in songs:
+ song=ClientHelper.extend_song_for_display(s)
+ if type(song["title"]) == list: # could be impossible
+ title=(', '.join(song["title"]))
+ else:
+ title=song["title"]
+ if type(song["artist"]) == list:
+ try:
+ song["artist"].remove(album_artist)
+ except:
+ pass
+ artist=(', '.join(song["artist"]))
+ else:
+ artist=song["artist"]
+ if artist == album_artist:
+ title_artist=""+title+""
+ else:
+ title_artist=""+title+" - "+artist
+
+ title_artist=title_artist.replace("&", "&")
+ store.append([int(song["track"]), title_artist, song["human_duration"], song["file"]])
+
+ # songs window
+ songs_window=SongsWindow(self._client, store, 3)
+
+ # songs view
+ songs_view=songs_window.get_treeview()
+
+ # columns
+ renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
+ renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
+
+ column_track=Gtk.TreeViewColumn(_("No"), renderer_text_ralign, text=0)
+ column_track.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ column_track.set_property("resizable", False)
+ songs_view.append_column(column_track)
+
+ column_title=Gtk.TreeViewColumn(_("Title"), renderer_text, markup=1)
+ column_title.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ column_title.set_property("resizable", False)
+ column_title.set_property("expand", True)
+ songs_view.append_column(column_title)
+
+ column_time=Gtk.TreeViewColumn(_("Length"), renderer_text, text=2)
+ column_time.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ column_time.set_property("resizable", False)
+ songs_view.append_column(column_time)
+
+ # close button
+ close_button=Gtk.ToggleButton(image=Gtk.Image.new_from_icon_name("window-close", Gtk.IconSize.BUTTON), label=_("Close"))
+
+ # action bar
+ action_bar=songs_window.get_action_bar()
+ action_bar.pack_end(close_button)
+
+ # connect
+ close_button.connect("clicked", self._on_close_button_clicked)
+
+ # packing
+ self.vbox.pack_start(songs_window, True, True, 0) # vbox default widget of dialogs
+ self.show_all()
+
+ def open(self):
+ response=self.run()
+
+ def _on_close_button_clicked(self, *args):
+ self.destroy()
+
class AlbumWindow(FocusFrame):
def __init__(self, client, settings, artist_window, window):
super().__init__()
@@ -2446,530 +3041,6 @@ class CoverPlaylistWindow(Gtk.Paned):
self._settings.set_int("paned0", self.get_position())
self._playlist_window.save_settings()
-###################
-# settings dialog #
-###################
-
-class GeneralSettings(Gtk.Box):
- def __init__(self, settings):
- super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=6, border_width=18)
-
- # adding vars
- self._settings=settings
- self._settings_handlers=[]
-
- # int_settings
- int_settings={}
- int_settings_data=[
- (_("Main cover size:"), (100, 1200, 10), "track-cover"),
- (_("Album view cover size:"), (50, 600, 10), "album-cover"),
- (_("Action bar icon size:"), (16, 64, 2), "icon-size"),
- (_("Secondary icon size:"), (16, 64, 2), "icon-size-sec")
- ]
- 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::"+key, self._on_int_settings_changed, int_settings[key][1])
- )
-
- # combo_settings
- combo_settings={}
- combo_settings_data=[
- (_("Sort albums by:"), _("name"), _("year"), "sort-albums-by-year"),
- (_("Position of playlist:"), _("bottom"), _("right"), "playlist-right")
- ]
- for label, vfalse, vtrue, key in combo_settings_data:
- combo_settings[key]=(Gtk.Label(label=label, xalign=0), Gtk.ComboBoxText(entry_text_column=0))
- combo_settings[key][1].append_text(vfalse)
- combo_settings[key][1].append_text(vtrue)
- if self._settings.get_boolean(key):
- combo_settings[key][1].set_active(1)
- else:
- combo_settings[key][1].set_active(0)
- combo_settings[key][1].connect("changed", self._on_combo_changed, key)
- self._settings_handlers.append(
- self._settings.connect("changed::"+key, self._on_combo_settings_changed, combo_settings[key][1])
- )
-
- # check buttons
- check_buttons={}
- check_buttons_data=[
- (_("Use Client-side decoration"), "use-csd"),
- (_("Show stop button"), "show-stop"),
- (_("Show lyrics button"), "show-lyrics-button"),
- (_("Show initials in artist view"), "show-initials"),
- (_("Show tooltips in album view"), "show-album-view-tooltips"),
- (_("Use 'Album Artist' tag"), "use-album-artist"),
- (_("Send notification on title change"), "send-notify"),
- (_("Stop playback on quit"), "stop-on-quit"),
- (_("Play selected albums and titles immediately"), "force-mode")
- ]
-
- for label, key in check_buttons_data:
- check_buttons[key]=Gtk.CheckButton(label=label)
- 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::"+key, self._on_check_settings_changed, check_buttons[key])
- )
-
- # headings
- view_heading=Gtk.Label(label=_("View"), use_markup=True, xalign=0)
- behavior_heading=Gtk.Label(label=_("Behavior"), 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.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)
- view_grid.attach_next_to(int_settings["icon-size-sec"][0], int_settings["icon-size"][0], Gtk.PositionType.BOTTOM, 1, 1)
- view_grid.attach_next_to(combo_settings["playlist-right"][0], int_settings["icon-size-sec"][0], Gtk.PositionType.BOTTOM, 1, 1)
- view_grid.attach_next_to(int_settings["track-cover"][1], int_settings["track-cover"][0], Gtk.PositionType.RIGHT, 1, 1)
- view_grid.attach_next_to(int_settings["album-cover"][1], int_settings["album-cover"][0], Gtk.PositionType.RIGHT, 1, 1)
- 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)
- view_grid.attach_next_to(combo_settings["playlist-right"][1], combo_settings["playlist-right"][0], Gtk.PositionType.RIGHT, 1, 1)
-
- # behavior grid
- behavior_grid=Gtk.Grid(row_spacing=6, column_spacing=12)
- behavior_grid.set_margin_start(12)
- behavior_grid.add(combo_settings["sort-albums-by-year"][0])
- behavior_grid.attach_next_to(
- combo_settings["sort-albums-by-year"][1],
- combo_settings["sort-albums-by-year"][0],
- Gtk.PositionType.RIGHT, 1, 1
- )
-
- # connect
- self.connect("destroy", self._remove_handlers)
-
- # packing
- box=Gtk.Box(spacing=12)
- box.pack_start(check_buttons["use-csd"], False, False, 0)
- box.pack_start(Gtk.Label(label=_("(restart required)"), sensitive=False), False, False, 0)
- self.pack_start(view_heading, False, False, 0)
- self.pack_start(box, False, False, 0)
- self.pack_start(check_buttons["show-stop"], False, False, 0)
- self.pack_start(check_buttons["show-lyrics-button"], False, False, 0)
- self.pack_start(check_buttons["show-initials"], False, False, 0)
- self.pack_start(check_buttons["show-album-view-tooltips"], False, False, 0)
- self.pack_start(view_grid, False, False, 0)
- self.pack_start(behavior_heading, False, False, 0)
- self.pack_start(check_buttons["use-album-artist"], False, False, 0)
- self.pack_start(check_buttons["send-notify"], False, False, 0)
- self.pack_start(check_buttons["stop-on-quit"], False, False, 0)
- self.pack_start(check_buttons["force-mode"], False, False, 0)
- self.pack_start(behavior_grid, 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_combo_settings_changed(self, settings, key, combo):
- if settings.get_boolean(key):
- combo.set_active(1)
- else:
- combo.set_active(0)
-
- 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_combo_changed(self, box, key):
- active=box.get_active()
- if active == 0:
- self._settings.set_boolean(key, False)
- else:
- self._settings.set_boolean(key, True)
-
- 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)
-
- # adding vars
- 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)
-
- add_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("list-add", Gtk.IconSize.BUTTON))
- delete_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("list-remove", 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(label=_("Connect"), image=Gtk.Image.new_from_icon_name("system-run", Gtk.IconSize.BUTTON))
-
- 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)
- address_entry.pack_start(self._host_entry, True, True, 0)
- address_entry.pack_start(self._port_entry, False, False, 0)
- self._password_entry=Gtk.Entry(hexpand=True, visibility=False)
- self._path_entry=Gtk.Entry(hexpand=True)
- self._path_select_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("folder-open", 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._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)
- regex_label=Gtk.Label(label=_("Cover regex:"), xalign=1)
-
- # connect
- add_button.connect("clicked", self._on_add_button_clicked)
- delete_button.connect("clicked", self._on_delete_button_clicked)
- connect_button.connect("clicked", self._on_connect_button_clicked)
- self._path_select_button.connect("clicked", self._on_path_select_button_clicked, parent)
- self._profiles_combo.connect("changed", self._on_profiles_changed)
- 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._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(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._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)
-
- def _block_entry_changed_handlers(self, *args):
- for obj, handler in self.entry_changed_handlers:
- obj.handler_block(handler)
-
- def _unblock_entry_changed_handlers(self, *args):
- for obj, handler in self.entry_changed_handlers:
- obj.handler_unblock(handler)
-
- def _profiles_combo_reload(self, *args):
- self._block_entry_changed_handlers()
-
- self._profiles_combo.remove_all()
- for profile in self._settings.get_value("profiles"):
- self._profiles_combo.append_text(profile)
-
- self._unblock_entry_changed_handlers()
-
- 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)
-
- def _on_add_button_clicked(self, *args):
- model=self._profiles_combo.get_model()
- self._settings.array_append('as', "profiles", "new profile ("+str(len(model))+")")
- self._settings.array_append('as', "hosts", "localhost")
- self._settings.array_append('ai', "ports", 6600)
- self._settings.array_append('as', "passwords", "")
- self._settings.array_append('as', "paths", "")
- self._settings.array_append('as', "regex", "")
- self._profiles_combo_reload()
- new_pos=len(model)-1
- self._profiles_combo.set_active(new_pos)
-
- def _on_delete_button_clicked(self, *args):
- pos=self._profiles_combo.get_active()
- self._settings.array_delete('as', "profiles", pos)
- self._settings.array_delete('as', "hosts", pos)
- self._settings.array_delete('ai', "ports", pos)
- self._settings.array_delete('as', "passwords", pos)
- self._settings.array_delete('as', "paths", pos)
- self._settings.array_delete('as', "regex", pos)
- if len(self._settings.get_value("profiles")) == 0:
- self._on_add_button_clicked()
- else:
- self._profiles_combo_reload()
- new_pos=max(pos-1,0)
- 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()
-
- 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.set_active(pos)
-
- 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())
-
- 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()))
-
- 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())
-
- 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())
-
- 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())
-
- def _on_path_select_button_clicked(self, widget, parent):
- dialog=Gtk.FileChooserDialog(title=_("Choose directory"), transient_for=parent, action=Gtk.FileChooserAction.SELECT_FOLDER)
- dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
- dialog.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK)
- dialog.set_default_size(800, 400)
- dialog.set_current_folder(self._settings.get_value("paths")[self._profiles_combo.get_active()])
- response=dialog.run()
- if response == Gtk.ResponseType.OK:
- self._gui_modification=True
- self._settings.array_modify('as', "paths", self._profiles_combo.get_active(), dialog.get_filename())
- self._path_entry.set_text(dialog.get_filename())
- dialog.destroy()
-
- def _on_profiles_changed(self, *args):
- active=self._profiles_combo.get_active()
- 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])
- self._path_entry.set_text(self._settings.get_value("paths")[active])
- self._regex_entry.set_text(self._settings.get_value("regex")[active])
-
- self._unblock_entry_changed_handlers()
-
-class PlaylistSettings(Gtk.Box):
- def __init__(self, settings):
- super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=6, border_width=18)
-
- # adding vars
- self._settings=settings
-
- # label
- label=Gtk.Label(label=_("Choose the order of information to appear in the playlist:"), wrap=True, xalign=0)
-
- # Store
- # (toggle, header, actual_index)
- self._store=Gtk.ListStore(bool, str, int)
-
- # TreeView
- treeview=Gtk.TreeView(model=self._store, reorderable=True, headers_visible=False)
- treeview.set_search_column(-1)
-
- # selection
- self._selection=treeview.get_selection()
-
- # Column
- renderer_text=Gtk.CellRendererText()
- renderer_toggle=Gtk.CellRendererToggle()
-
- column_toggle=Gtk.TreeViewColumn("", renderer_toggle, active=0)
- treeview.append_column(column_toggle)
-
- column_text=Gtk.TreeViewColumn("", renderer_text, text=1)
- treeview.append_column(column_text)
-
- # fill store
- self._headers=[_("No"), _("Disc"), _("Title"), _("Artist"), _("Album"), _("Length"), _("Year"), _("Genre")]
- self._fill()
-
- # scroll
- scroll=Gtk.ScrolledWindow()
- scroll.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
- scroll.add(treeview)
- frame=Gtk.Frame()
- frame.add(scroll)
-
- # Toolbar
- toolbar=Gtk.Toolbar()
- style_context=toolbar.get_style_context()
- style_context.add_class("inline-toolbar")
- self._up_button=Gtk.ToolButton.new(Gtk.Image.new_from_icon_name("go-up-symbolic", Gtk.IconSize.SMALL_TOOLBAR))
- self._up_button.set_sensitive(False)
- self._down_button=Gtk.ToolButton.new(Gtk.Image.new_from_icon_name("go-down-symbolic", Gtk.IconSize.SMALL_TOOLBAR))
- self._down_button.set_sensitive(False)
- toolbar.insert(self._up_button, 0)
- toolbar.insert(self._down_button, 1)
-
- # column chooser
- column_chooser=Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
- column_chooser.pack_start(frame, True, True, 0)
- column_chooser.pack_start(toolbar, False, False, 0)
-
- # connect
- self._row_deleted=self._store.connect("row-deleted", self._save_permutation)
- renderer_toggle.connect("toggled", self._on_cell_toggled)
- 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)
- self.pack_start(column_chooser, True, True, 0)
-
- def _fill(self, *args):
- visibilities=self._settings.get_value("column-visibilities").unpack()
- for actual_index in self._settings.get_value("column-permutation"):
- self._store.append([visibilities[actual_index], self._headers[actual_index], actual_index])
-
- def _save_permutation(self, *args):
- permutation=[]
- for row in self._store:
- permutation.append(row[2])
- self._settings.set_value("column-permutation", GLib.Variant("ai", permutation))
-
- def _set_button_sensitivity(self, *args):
- treeiter=self._selection.get_selected()[1]
- if treeiter is None:
- self._up_button.set_sensitive(False)
- self._down_button.set_sensitive(False)
- else:
- path=self._store.get_path(treeiter)
- if self._store.iter_next(treeiter) is None:
- self._up_button.set_sensitive(True)
- self._down_button.set_sensitive(False)
- elif not path.prev():
- self._up_button.set_sensitive(False)
- self._down_button.set_sensitive(True)
- else:
- 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])
-
- def _on_up_button_clicked(self, *args):
- treeiter=self._selection.get_selected()[1]
- path=self._store.get_path(treeiter)
- path.prev()
- prev=self._store.get_iter(path)
- self._store.move_before(treeiter, prev)
- self._set_button_sensitivity()
- self._save_permutation()
-
- def _on_down_button_clicked(self, *args):
- treeiter=self._selection.get_selected()[1]
- path=self._store.get_path(treeiter)
- next=self._store.iter_next(treeiter)
- self._store.move_after(treeiter, next)
- 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):
- use_csd=settings.get_boolean("use-csd")
- if use_csd:
- super().__init__(title=_("Settings"), transient_for=parent, use_header_bar=True)
- # css
- style_context=self.get_style_context()
- provider=Gtk.CssProvider()
- css=b"""* {-GtkDialog-content-area-border: 0px;}"""
- provider.load_from_data(css)
- style_context.add_provider(provider, 800)
- else:
- super().__init__(title=_("Settings"), transient_for=parent)
- self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
- self.set_default_size(500, 400)
-
- # widgets
- general=GeneralSettings(settings)
- profiles=ProfileSettings(parent, client, settings)
- playlist=PlaylistSettings(settings)
-
- # packing
- tabs=Gtk.Notebook()
- tabs.append_page(general, Gtk.Label(label=_("General")))
- tabs.append_page(profiles, Gtk.Label(label=_("Profiles")))
- tabs.append_page(playlist, Gtk.Label(label=_("Playlist")))
- vbox=self.get_content_area()
- vbox.set_spacing(6)
- vbox.pack_start(tabs, True, True, 0)
-
- self.show_all()
-
###################
# control widgets #
###################
@@ -3324,77 +3395,6 @@ class PlaybackOptions(Gtk.Box):
icon.set_pixel_size(pixel_size)
self._volume_button.set_property("size", self._settings.get_gtk_icon_size("icon-size"))
-#################
-# other dialogs #
-#################
-
-class ServerStats(Gtk.Dialog):
- def __init__(self, parent, client, settings):
- use_csd=settings.get_boolean("use-csd")
- if use_csd:
- super().__init__(title=_("Stats"), transient_for=parent, use_header_bar=True)
- # css
- style_context=self.get_style_context()
- provider=Gtk.CssProvider()
- css=b"""* {-GtkDialog-content-area-border: 0px;}"""
- provider.load_from_data(css)
- style_context.add_provider(provider, 800)
- else:
- super().__init__(title=_("Stats"), transient_for=parent)
- self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
- self.set_resizable(False)
-
- # Store
- # (tag, value)
- store=Gtk.ListStore(str, str)
-
- # TreeView
- treeview=Gtk.TreeView(model=store, headers_visible=False, can_focus=False)
- treeview.set_search_column(-1)
-
- # selection
- sel=treeview.get_selection()
- sel.set_mode(Gtk.SelectionMode.NONE)
-
- # Column
- renderer_text=Gtk.CellRendererText()
- renderer_text_ralign=Gtk.CellRendererText(xalign=1.0)
-
- column_tag=Gtk.TreeViewColumn("", renderer_text_ralign, text=0)
- treeview.append_column(column_tag)
-
- column_value=Gtk.TreeViewColumn("", renderer_text, text=1)
- treeview.append_column(column_value)
-
- store.append(["protocol:", str(client.mpd_version)])
-
- stats=client.wrapped_call("stats")
- for key in stats:
- print_key=key+":"
- if key == "uptime" or key == "playtime" or key == "db_playtime":
- store.append([print_key, str(datetime.timedelta(seconds=int(stats[key])))])
- elif key == "db_update":
- store.append([print_key, str(datetime.datetime.fromtimestamp(int(stats[key])))])
- else:
- store.append([print_key, stats[key]])
- frame=Gtk.Frame()
- frame.add(treeview)
- self.vbox.pack_start(frame, True, True, 0)
- self.vbox.set_spacing(6)
- self.show_all()
- self.run()
-
-class AboutDialog(Gtk.AboutDialog):
- def __init__(self, window):
- super().__init__(transient_for=window, modal=True)
- self.set_program_name("mpdevil")
- self.set_version(VERSION)
- self.set_comments(_("A simple music browser for MPD"))
- self.set_authors(["Martin Wagner"])
- self.set_website("https://github.com/SoongNoonien/mpdevil")
- self.set_copyright("\xa9 2020 Martin Wagner")
- self.set_logo_icon_name("mpdevil")
-
###############
# main window #
###############