7 Commits

Author SHA1 Message Date
Martin Wagner
237bd3bd92 preparations for 1.4.1 2021-11-13 14:13:02 +01:00
Martin Wagner
84490555b5 added missing coords conversion 2021-11-07 19:54:39 +01:00
Martin Wagner
c986da58f6 removed inelegant try/except 2021-11-07 17:27:58 +01:00
Martin Wagner
74989364fe fixed popover placing 2021-11-07 17:15:45 +01:00
Martin Wagner
e416dbffeb unified signal naming in EventEmitter 2021-10-30 21:58:35 +02:00
Martin Wagner
a6a580a7a6 removed duplicated code in AlbumList 2021-10-30 21:51:37 +02:00
Martin Wagner
ba40cd2ec3 simplified focus dependent keybindings 2021-10-30 21:49:01 +02:00
3 changed files with 184 additions and 204 deletions

View File

@@ -43,7 +43,7 @@ if os.path.isfile("/.flatpak-info"): # test for flatpak environment
else: else:
bindtextdomain("mpdevil", localedir=None) # replace "None" by a static path if needed (e.g when installing on a non-FHS distro) bindtextdomain("mpdevil", localedir=None) # replace "None" by a static path if needed (e.g when installing on a non-FHS distro)
VERSION="1.4.0" # sync with setup.py VERSION="1.4.1" # sync with setup.py
COVER_REGEX=r"^\.?(album|cover|folder|front).*\.(gif|jpeg|jpg|png)$" COVER_REGEX=r"^\.?(album|cover|folder|front).*\.(gif|jpeg|jpg|png)$"
FALLBACK_COVER=Gtk.IconTheme.get_default().lookup_icon("media-optical", 128, Gtk.IconLookupFlags.FORCE_SVG).get_filename() FALLBACK_COVER=Gtk.IconTheme.get_default().lookup_icon("media-optical", 128, Gtk.IconLookupFlags.FORCE_SVG).get_filename()
FALLBACK_SOCKET=os.path.join(GLib.get_user_runtime_dir(), "mpd/socket") FALLBACK_SOCKET=os.path.join(GLib.get_user_runtime_dir(), "mpd/socket")
@@ -207,8 +207,8 @@ class MPRISInterface: # TODO emit Seeked if needed
# connect # connect
self._client.emitter.connect("state", self._on_state_changed) self._client.emitter.connect("state", self._on_state_changed)
self._client.emitter.connect("current_song_changed", self._on_song_changed) self._client.emitter.connect("current_song", self._on_song_changed)
self._client.emitter.connect("volume_changed", self._on_volume_changed) self._client.emitter.connect("volume", self._on_volume_changed)
self._client.emitter.connect("repeat", self._on_loop_changed) self._client.emitter.connect("repeat", self._on_loop_changed)
self._client.emitter.connect("single", self._on_loop_changed) self._client.emitter.connect("single", self._on_loop_changed)
self._client.emitter.connect("random", self._on_random_changed) self._client.emitter.connect("random", self._on_random_changed)
@@ -601,19 +601,17 @@ class EventEmitter(GObject.Object):
"disconnected": (GObject.SignalFlags.RUN_FIRST, None, ()), "disconnected": (GObject.SignalFlags.RUN_FIRST, None, ()),
"reconnected": (GObject.SignalFlags.RUN_FIRST, None, ()), "reconnected": (GObject.SignalFlags.RUN_FIRST, None, ()),
"connection_error": (GObject.SignalFlags.RUN_FIRST, None, ()), "connection_error": (GObject.SignalFlags.RUN_FIRST, None, ()),
"current_song_changed": (GObject.SignalFlags.RUN_FIRST, None, ()), "current_song": (GObject.SignalFlags.RUN_FIRST, None, ()),
"state": (GObject.SignalFlags.RUN_FIRST, None, (str,)), "state": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
"elapsed_changed": (GObject.SignalFlags.RUN_FIRST, None, (float,float,)), "elapsed": (GObject.SignalFlags.RUN_FIRST, None, (float,float,)),
"volume_changed": (GObject.SignalFlags.RUN_FIRST, None, (float,)), "volume": (GObject.SignalFlags.RUN_FIRST, None, (float,)),
"playlist_changed": (GObject.SignalFlags.RUN_FIRST, None, (int,)), "playlist": (GObject.SignalFlags.RUN_FIRST, None, (int,)),
"repeat": (GObject.SignalFlags.RUN_FIRST, None, (bool,)), "repeat": (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
"random": (GObject.SignalFlags.RUN_FIRST, None, (bool,)), "random": (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
"single": (GObject.SignalFlags.RUN_FIRST, None, (str,)), "single": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
"consume": (GObject.SignalFlags.RUN_FIRST, None, (bool,)), "consume": (GObject.SignalFlags.RUN_FIRST, None, (bool,)),
"audio": (GObject.SignalFlags.RUN_FIRST, None, (str,)), "audio": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
"bitrate": (GObject.SignalFlags.RUN_FIRST, None, (str,)), "bitrate": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
"add_to_playlist": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
"show_info": (GObject.SignalFlags.RUN_FIRST, None, ())
} }
class Client(MPDClient): class Client(MPDClient):
@@ -890,22 +888,22 @@ class Client(MPDClient):
for key, val in diff: for key, val in diff:
if key == "elapsed": if key == "elapsed":
if "duration" in status: if "duration" in status:
self.emitter.emit("elapsed_changed", float(val), float(status["duration"])) self.emitter.emit("elapsed", float(val), float(status["duration"]))
else: else:
self.emitter.emit("elapsed_changed", float(val), 0.0) self.emitter.emit("elapsed", float(val), 0.0)
elif key == "bitrate": elif key == "bitrate":
if val == "0": if val == "0":
self.emitter.emit("bitrate", None) self.emitter.emit("bitrate", None)
else: else:
self.emitter.emit("bitrate", val) self.emitter.emit("bitrate", val)
elif key == "songid": elif key == "songid":
self.emitter.emit("current_song_changed") self.emitter.emit("current_song")
elif key in ("state", "single", "audio"): elif key in ("state", "single", "audio"):
self.emitter.emit(key, val) self.emitter.emit(key, val)
elif key == "volume": elif key == "volume":
self.emitter.emit("volume_changed", float(val)) self.emitter.emit("volume", float(val))
elif key == "playlist": elif key == "playlist":
self.emitter.emit("playlist_changed", int(val)) self.emitter.emit("playlist", int(val))
elif key in ("repeat", "random", "consume"): elif key in ("repeat", "random", "consume"):
if val == "1": if val == "1":
self.emitter.emit(key, True) self.emitter.emit(key, True)
@@ -916,9 +914,9 @@ class Client(MPDClient):
diff=set(self._last_status)-set(status) diff=set(self._last_status)-set(status)
for key in diff: for key in diff:
if "songid" == key: if "songid" == key:
self.emitter.emit("current_song_changed") self.emitter.emit("current_song")
elif "volume" == key: elif "volume" == key:
self.emitter.emit("volume_changed", -1) self.emitter.emit("volume", -1)
elif "updating_db" == key: elif "updating_db" == key:
self.emitter.emit("updated_db") self.emitter.emit("updated_db")
elif "bitrate" == key: elif "bitrate" == key:
@@ -1391,6 +1389,17 @@ class AboutDialog(Gtk.AboutDialog):
# general purpose widgets # # general purpose widgets #
########################### ###########################
class TreeView(Gtk.TreeView):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def get_popover_point(self, path):
cell=self.get_cell_area(path, None)
cell.x,cell.y=self.convert_bin_window_to_widget_coords(cell.x,cell.y)
rect=self.get_visible_rect()
rect.x,rect.y=self.convert_tree_to_widget_coords(rect.x,rect.y)
return (rect.x+rect.width//2, max(min(cell.y+cell.height//2, rect.y+rect.height), rect.y))
class AutoSizedIcon(Gtk.Image): class AutoSizedIcon(Gtk.Image):
def __init__(self, icon_name, settings_key, settings): def __init__(self, icon_name, settings_key, settings):
super().__init__(icon_name=icon_name) super().__init__(icon_name=icon_name)
@@ -1458,12 +1467,9 @@ class SongPopover(Gtk.Popover):
self.add(box) self.add(box)
box.show_all() box.show_all()
def open(self, uri, widget, x, y, offset=26): def open(self, uri, widget, x, y):
self._uri=uri self._uri=uri
self._rect.x=x self._rect.x,self._rect.y=x,y
# Gtk places popovers in treeviews 26px above the given position for no obvious reasons, so I move them 26px
# This seems to be related to the width/height of the headers in treeviews
self._rect.y=y+offset
self.set_pointing_to(self._rect) self.set_pointing_to(self._rect)
self.set_relative_to(widget) self.set_relative_to(widget)
window=self.get_toplevel() window=self.get_toplevel()
@@ -1500,7 +1506,7 @@ class SongPopover(Gtk.Popover):
self._client.files_to_playlist([self._uri], mode) self._client.files_to_playlist([self._uri], mode)
self.popdown() self.popdown()
class SongsView(Gtk.TreeView): class SongsView(TreeView):
def __init__(self, client, store, file_column_id): def __init__(self, client, store, file_column_id):
super().__init__(model=store, search_column=-1, activate_on_single_click=True) super().__init__(model=store, search_column=-1, activate_on_single_click=True)
self._client=client self._client=client
@@ -1516,8 +1522,6 @@ class SongsView(Gtk.TreeView):
# connect # connect
self.connect("row-activated", self._on_row_activated) self.connect("row-activated", self._on_row_activated)
self.connect("button-press-event", self._on_button_press_event) self.connect("button-press-event", self._on_button_press_event)
self._client.emitter.connect("show-info", self._on_show_info)
self._client.emitter.connect("add-to-playlist", self._on_add_to_playlist)
def clear(self): def clear(self):
self._song_popover.popdown() self._song_popover.popdown()
@@ -1542,24 +1546,19 @@ class SongsView(Gtk.TreeView):
self._client.files_to_playlist([self._store[path][self._file_column_id]], "append") self._client.files_to_playlist([self._store[path][self._file_column_id]], "append")
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
uri=self._store[path][self._file_column_id] uri=self._store[path][self._file_column_id]
if self.get_property("headers-visible"): point=self.convert_bin_window_to_widget_coords(event.x,event.y)
self._song_popover.open(uri, widget, int(event.x), int(event.y)) self._song_popover.open(uri, widget, *point)
else:
self._song_popover.open(uri, widget, int(event.x), int(event.y), offset=0)
def _on_show_info(self, *args): def show_info(self):
if self.has_focus(): treeview, treeiter=self._selection.get_selected()
treeview, treeiter=self._selection.get_selected() if treeiter is not None:
if treeiter is not None: path=self._store.get_path(treeiter)
path=self._store.get_path(treeiter) self._song_popover.open(self._store[path][self._file_column_id], self, *self.get_popover_point(path))
cell=self.get_cell_area(path, None)
self._song_popover.open(self._store[path][self._file_column_id], self, cell.x, cell.y)
def _on_add_to_playlist(self, emitter, mode): def add_to_playlist(self, mode):
if self.has_focus(): treeview, treeiter=self._selection.get_selected()
treeview, treeiter=self._selection.get_selected() if treeiter is not None:
if treeiter is not None: self._client.files_to_playlist([self._store.get_value(treeiter, self._file_column_id)], mode)
self._client.files_to_playlist([self._store.get_value(treeiter, self._file_column_id)], mode)
class SongsWindow(Gtk.Box): class SongsWindow(Gtk.Box):
__gsignals__={"button-clicked": (GObject.SignalFlags.RUN_FIRST, None, ())} __gsignals__={"button-clicked": (GObject.SignalFlags.RUN_FIRST, None, ())}
@@ -1932,7 +1931,7 @@ class SearchWindow(Gtk.Box):
# browser # # browser #
########### ###########
class SelectionList(Gtk.TreeView): class SelectionList(TreeView):
__gsignals__={"item-selected": (GObject.SignalFlags.RUN_FIRST, None, ()), "clear": (GObject.SignalFlags.RUN_FIRST, None, ())} __gsignals__={"item-selected": (GObject.SignalFlags.RUN_FIRST, None, ()), "clear": (GObject.SignalFlags.RUN_FIRST, None, ())}
def __init__(self, select_all_string): def __init__(self, select_all_string):
super().__init__(activate_on_single_click=True, search_column=0, headers_visible=False, fixed_height_mode=True) super().__init__(activate_on_single_click=True, search_column=0, headers_visible=False, fixed_height_mode=True)
@@ -2067,8 +2066,6 @@ class ArtistList(SelectionList):
self._settings.connect("changed::use-album-artist", self._refresh) self._settings.connect("changed::use-album-artist", self._refresh)
self._client.emitter.connect("disconnected", self._on_disconnected) self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected) self._client.emitter.connect("reconnected", self._on_reconnected)
self._client.emitter.connect("add-to-playlist", self._on_add_to_playlist)
self._client.emitter.connect("show-info", self._on_show_info)
self.genre_list.connect_after("item-selected", self._refresh) self.genre_list.connect_after("item-selected", self._refresh)
def _refresh(self, *args): def _refresh(self, *args):
@@ -2106,24 +2103,21 @@ class ArtistList(SelectionList):
elif event.button == 3: elif event.button == 3:
self._artist_popover.open(artist, genre, self, event.x, event.y) self._artist_popover.open(artist, genre, self, event.x, event.y)
def _on_add_to_playlist(self, emitter, mode): def add_to_playlist(self, mode):
if self.has_focus(): selected_rows=self._selection.get_selected_rows()
selected_rows=self._selection.get_selected_rows() if selected_rows is not None:
if selected_rows is not None: path=selected_rows[1][0]
path=selected_rows[1][0] genre=self.genre_list.get_selected()
genre=self.genre_list.get_selected() artist=self.get_item(path)
artist=self.get_item(path) self._client.artist_to_playlist(artist, genre, mode)
self._client.artist_to_playlist(artist, genre, mode)
def _on_show_info(self, *args): def show_info(self):
if self.has_focus(): treeview, treeiter=self._selection.get_selected()
selected_rows=self._selection.get_selected_rows() if treeiter is not None:
if selected_rows is not None: path=self._store.get_path(treeiter)
path=selected_rows[1][0] genre=self.genre_list.get_selected()
genre=self.genre_list.get_selected() artist=self.get_item(path)
artist=self.get_item(path) self._artist_popover.open(artist, genre, self, *self.get_popover_point(path))
cell=self.get_cell_area(path, None)
self._artist_popover.open(artist, genre, self, cell.x, cell.y)
def _on_disconnected(self, *args): def _on_disconnected(self, *args):
self.set_sensitive(False) self.set_sensitive(False)
@@ -2255,9 +2249,9 @@ class AlbumLoadingThread(threading.Thread):
return False return False
GLib.idle_add(callback) GLib.idle_add(callback)
class AlbumWindow(Gtk.Box): class AlbumList(Gtk.IconView):
def __init__(self, client, settings, artist_list): def __init__(self, client, settings, artist_list):
super().__init__(orientation=Gtk.Orientation.VERTICAL) super().__init__(item_width=0, pixbuf_column=0, markup_column=1, activate_on_single_click=True)
self._settings=settings self._settings=settings
self._client=client self._client=client
self._artist_list=artist_list self._artist_list=artist_list
@@ -2265,46 +2259,33 @@ class AlbumWindow(Gtk.Box):
# cover, display_label, display_label_artist, album, date, artist # cover, display_label, display_label_artist, album, date, artist
self._store=Gtk.ListStore(GdkPixbuf.Pixbuf, str, str, str, str, str) self._store=Gtk.ListStore(GdkPixbuf.Pixbuf, str, str, str, str, str)
self._store.set_default_sort_func(lambda *args: 0) self._store.set_default_sort_func(lambda *args: 0)
self.set_model(self._store)
# iconview
self._iconview=Gtk.IconView(model=self._store, item_width=0, pixbuf_column=0, markup_column=1, activate_on_single_click=True)
# scroll
scroll=Gtk.ScrolledWindow(child=self._iconview)
self._scroll_vadj=scroll.get_vadjustment()
self._scroll_hadj=scroll.get_hadjustment()
# progress bar # progress bar
self._progress_bar=Gtk.ProgressBar(no_show_all=True) self.progress_bar=Gtk.ProgressBar(no_show_all=True)
# popover # popover
self._album_popover=AlbumPopover(self._client, self._settings) self._album_popover=AlbumPopover(self._client, self._settings)
self._artist_popover=ArtistPopover(self._client) self._artist_popover=ArtistPopover(self._client)
# cover thread # cover thread
self._cover_thread=AlbumLoadingThread(self._client, self._settings, self._progress_bar, self._iconview, self._store, None, None) self._cover_thread=AlbumLoadingThread(self._client, self._settings, self.progress_bar, self, self._store, None, None)
# connect # connect
self._iconview.connect("item-activated", self._on_item_activated) self.connect("item-activated", self._on_item_activated)
self._iconview.connect("button-press-event", self._on_button_press_event) self.connect("button-press-event", self._on_button_press_event)
self._client.emitter.connect("disconnected", self._on_disconnected) self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected) self._client.emitter.connect("reconnected", self._on_reconnected)
self._client.emitter.connect("show-info", self._on_show_info)
self._client.emitter.connect("add-to-playlist", self._on_add_to_playlist)
self._settings.connect("changed::sort-albums-by-year", self._sort_settings) self._settings.connect("changed::sort-albums-by-year", self._sort_settings)
self._settings.connect("changed::album-cover", self._on_cover_size_changed) self._settings.connect("changed::album-cover", self._on_cover_size_changed)
self._artist_list.connect("item-selected", self._refresh) self._artist_list.connect("item-selected", self._refresh)
self._artist_list.connect("clear", self._clear) self._artist_list.connect("clear", self._clear)
# packing
self.pack_start(scroll, True, True, 0)
self.pack_start(self._progress_bar, False, False, 0)
def _workaround_clear(self): def _workaround_clear(self):
self._store.clear() self._store.clear()
# workaround (scrollbar still visible after clear) # workaround (scrollbar still visible after clear)
self._iconview.set_model(None) self.set_model(None)
self._iconview.set_model(self._store) self.set_model(self._store)
def _clear(self, *args): def _clear(self, *args):
def callback(): def callback():
@@ -2321,14 +2302,14 @@ class AlbumWindow(Gtk.Box):
def callback(): def callback():
song=self._client.currentsong() song=self._client.currentsong()
album=song["album"][0] album=song["album"][0]
self._iconview.unselect_all() self.unselect_all()
row_num=len(self._store) row_num=len(self._store)
for i in range(0, row_num): for i in range(0, row_num):
path=Gtk.TreePath(i) path=Gtk.TreePath(i)
if self._store[path][3] == album: if self._store[path][3] == album:
self._iconview.set_cursor(path, None, False) self.set_cursor(path, None, False)
self._iconview.select_path(path) self.select_path(path)
self._iconview.scroll_to_path(path, True, 0, 0) self.scroll_to_path(path, True, 0, 0)
break break
if self._cover_thread.is_alive(): if self._cover_thread.is_alive():
self._cover_thread.set_callback(callback) self._cover_thread.set_callback(callback)
@@ -2348,7 +2329,7 @@ class AlbumWindow(Gtk.Box):
return False return False
artist=self._artist_list.get_selected() artist=self._artist_list.get_selected()
genre=self._artist_list.genre_list.get_selected() genre=self._artist_list.genre_list.get_selected()
self._cover_thread=AlbumLoadingThread(self._client,self._settings,self._progress_bar,self._iconview,self._store,artist,genre) self._cover_thread=AlbumLoadingThread(self._client,self._settings,self.progress_bar,self,self._store,artist,genre)
self._cover_thread.start() self._cover_thread.start()
if self._cover_thread.is_alive(): if self._cover_thread.is_alive():
self._cover_thread.set_callback(callback) self._cover_thread.set_callback(callback)
@@ -2372,8 +2353,8 @@ class AlbumWindow(Gtk.Box):
if path is not None: if path is not None:
self._path_to_playlist(path, "append") self._path_to_playlist(path, "append")
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
v=self._scroll_vadj.get_value() v=self.get_vadjustment().get_value()
h=self._scroll_hadj.get_value() h=self.get_hadjustment().get_value()
genre=self._artist_list.genre_list.get_selected() genre=self._artist_list.genre_list.get_selected()
if path is not None: if path is not None:
album=self._store[path][3] album=self._store[path][3]
@@ -2386,35 +2367,29 @@ class AlbumWindow(Gtk.Box):
GLib.idle_add(self._artist_popover.open, artist, genre, widget, event.x-h, event.y-v) GLib.idle_add(self._artist_popover.open, artist, genre, widget, event.x-h, event.y-v)
def _on_item_activated(self, widget, path): def _on_item_activated(self, widget, path):
album=self._store[path][3] self._path_to_playlist(path)
year=self._store[path][4]
artist=self._store[path][5]
genre=self._artist_list.genre_list.get_selected()
self._client.album_to_playlist(album, artist, year, genre)
def _on_disconnected(self, *args): def _on_disconnected(self, *args):
self._iconview.set_sensitive(False) self.set_sensitive(False)
def _on_reconnected(self, *args): def _on_reconnected(self, *args):
self._iconview.set_sensitive(True) self.set_sensitive(True)
def _on_show_info(self, *args): def show_info(self):
if self._iconview.has_focus(): paths=self.get_selected_items()
paths=self._iconview.get_selected_items() if len(paths) > 0:
if len(paths) > 0: path=paths[0]
rect=self._iconview.get_cell_rect(paths[0], None)[1] cell=self.get_cell_rect(path, None)[1]
x=rect.x+rect.width//2 rect=self.get_allocation()
y=rect.y+rect.height//2 x=max(min(rect.x+cell.width//2, rect.x+rect.width), rect.x)
genre=self._artist_list.genre_list.get_selected() y=max(min(cell.y+cell.height//2, rect.y+rect.height), rect.y)
self._album_popover.open( genre=self._artist_list.genre_list.get_selected()
self._store[paths[0]][3], self._store[paths[0]][5], self._store[paths[0]][4], genre, self._iconview, x, y self._album_popover.open(self._store[path][3], self._store[path][5], self._store[path][4], genre, self, x, y)
)
def _on_add_to_playlist(self, emitter, mode): def add_to_playlist(self, mode):
if self._iconview.has_focus(): paths=self.get_selected_items()
paths=self._iconview.get_selected_items() if len(paths) != 0:
if len(paths) != 0: self._path_to_playlist(paths[0], mode)
self._path_to_playlist(paths[0], mode)
def _on_cover_size_changed(self, *args): def _on_cover_size_changed(self, *args):
if self._client.connected(): if self._client.connected():
@@ -2429,9 +2404,10 @@ class Browser(Gtk.Paned):
# widgets # widgets
self._genre_list=GenreList(self._client) self._genre_list=GenreList(self._client)
self._artist_list=ArtistList(self._client, self._settings, self._genre_list) self._artist_list=ArtistList(self._client, self._settings, self._genre_list)
self._album_window=AlbumWindow(self._client, self._settings, self._artist_list) self._album_list=AlbumList(self._client, self._settings, self._artist_list)
genre_window=Gtk.ScrolledWindow(child=self._genre_list) genre_window=Gtk.ScrolledWindow(child=self._genre_list)
artist_window=Gtk.ScrolledWindow(child=self._artist_list) artist_window=Gtk.ScrolledWindow(child=self._artist_list)
album_window=Gtk.ScrolledWindow(child=self._album_list)
# hide/show genre filter # hide/show genre filter
self._genre_list.set_property("visible", True) self._genre_list.set_property("visible", True)
@@ -2440,9 +2416,12 @@ class Browser(Gtk.Paned):
self._settings.connect("changed::genre-filter", self._on_genre_filter_changed) self._settings.connect("changed::genre-filter", self._on_genre_filter_changed)
# packing # packing
album_box=Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
album_box.pack_start(album_window, True, True, 0)
album_box.pack_start(self._album_list.progress_bar, False, False, 0)
self.paned1=Gtk.Paned() self.paned1=Gtk.Paned()
self.paned1.pack1(artist_window, False, False) self.paned1.pack1(artist_window, False, False)
self.paned1.pack2(self._album_window, True, False) self.paned1.pack2(album_box, True, False)
self.pack1(genre_window, False, False) self.pack1(genre_window, False, False)
self.pack2(self.paned1, True, False) self.pack2(self.paned1, True, False)
@@ -2459,7 +2438,7 @@ class Browser(Gtk.Paned):
self._artist_list.highlight_selected() self._artist_list.highlight_selected()
else: # one artist selected else: # one artist selected
self._artist_list.select(artist) self._artist_list.select(artist)
self._album_window.scroll_to_current_album() self._album_list.scroll_to_current_album()
else: else:
self._genre_list.deactivate() self._genre_list.deactivate()
@@ -2472,31 +2451,20 @@ class Browser(Gtk.Paned):
# playlist # # playlist #
############ ############
class PlaylistWindow(Gtk.Overlay): class PlaylistView(TreeView):
selected_path=GObject.Property(type=Gtk.TreePath, default=None) # currently marked song (bold text) selected_path=GObject.Property(type=Gtk.TreePath, default=None) # currently marked song (bold text)
def __init__(self, client, settings): def __init__(self, client, settings):
super().__init__() super().__init__(activate_on_single_click=True, reorderable=True, search_column=2, fixed_height_mode=True)
self._client=client self._client=client
self._settings=settings self._settings=settings
self._playlist_version=None self._playlist_version=None
self._inserted_path=None # needed for drag and drop self._inserted_path=None # needed for drag and drop
self._selection=self.get_selection()
# back button # store
self._back_to_current_song_button=Gtk.Button(
image=Gtk.Image.new_from_icon_name("go-previous-symbolic", Gtk.IconSize.BUTTON), tooltip_text=_("Scroll to current song"),
can_focus=False
)
self._back_to_current_song_button.get_style_context().add_class("osd")
self._back_button_revealer=Gtk.Revealer(
child=self._back_to_current_song_button, transition_duration=0,
margin_bottom=6, margin_start=6, halign=Gtk.Align.START, valign=Gtk.Align.END
)
# treeview
# (track, disc, title, artist, album, human duration, date, genre, file, weight, duration) # (track, disc, title, artist, album, human duration, date, genre, file, weight, duration)
self._store=Gtk.ListStore(str, str, str, str, str, str, str, str, str, Pango.Weight, float) self._store=Gtk.ListStore(str, str, str, str, str, str, str, str, str, Pango.Weight, float)
self._treeview=Gtk.TreeView(model=self._store,activate_on_single_click=True,reorderable=True,search_column=2,fixed_height_mode=True) self.set_model(self._store)
self._selection=self._treeview.get_selection()
# columns # columns
renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True) renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
@@ -2518,51 +2486,40 @@ class PlaylistWindow(Gtk.Overlay):
column.connect("notify::fixed-width", self._on_column_width, i) column.connect("notify::fixed-width", self._on_column_width, i)
self._load_settings() self._load_settings()
# scroll
scroll=Gtk.ScrolledWindow(child=self._treeview)
# song popover # song popover
self._song_popover=SongPopover(self._client, show_buttons=False) self._song_popover=SongPopover(self._client, show_buttons=False)
# connect # connect
self._treeview.connect("row-activated", self._on_row_activated) self.connect("row-activated", self._on_row_activated)
self._treeview.connect("button-press-event", self._on_button_press_event) self.connect("button-press-event", self._on_button_press_event)
self._treeview.connect("key-release-event", self._on_key_release_event) self.connect("key-release-event", self._on_key_release_event)
self._row_deleted=self._store.connect("row-deleted", self._on_row_deleted) 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._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)
scroll.get_vadjustment().connect("value-changed", self._on_show_hide_back_button)
self.connect("notify::selected-path", self._on_show_hide_back_button)
self._client.emitter.connect("playlist_changed", self._on_playlist_changed) self._client.emitter.connect("playlist", self._on_playlist_changed)
self._client.emitter.connect("current_song_changed", self._on_song_changed) self._client.emitter.connect("current_song", self._on_song_changed)
self._client.emitter.connect("disconnected", self._on_disconnected) self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected) self._client.emitter.connect("reconnected", self._on_reconnected)
self._client.emitter.connect("show-info", self._on_show_info)
self._settings.connect("changed::column-visibilities", self._load_settings) self._settings.connect("changed::column-visibilities", self._load_settings)
self._settings.connect("changed::column-permutation", self._load_settings) self._settings.connect("changed::column-permutation", self._load_settings)
self._settings.bind("mini-player", self, "no-show-all", Gio.SettingsBindFlags.GET) self._settings.bind("mini-player", self, "no-show-all", Gio.SettingsBindFlags.GET)
self._settings.bind("mini-player", self, "visible", Gio.SettingsBindFlags.INVERT_BOOLEAN|Gio.SettingsBindFlags.GET) self._settings.bind("mini-player", self, "visible", Gio.SettingsBindFlags.INVERT_BOOLEAN|Gio.SettingsBindFlags.GET)
# packing
self.add(scroll)
self.add_overlay(self._back_button_revealer)
def _on_column_width(self, obj, typestring, pos): def _on_column_width(self, obj, typestring, pos):
self._settings.array_modify("ai", "column-sizes", pos, obj.get_property("fixed-width")) self._settings.array_modify("ai", "column-sizes", pos, obj.get_property("fixed-width"))
def _load_settings(self, *args): def _load_settings(self, *args):
columns=self._treeview.get_columns() columns=self.get_columns()
for column in columns: for column in columns:
self._treeview.remove_column(column) self.remove_column(column)
sizes=self._settings.get_value("column-sizes").unpack() sizes=self._settings.get_value("column-sizes").unpack()
visibilities=self._settings.get_value("column-visibilities").unpack() visibilities=self._settings.get_value("column-visibilities").unpack()
for i in self._settings.get_value("column-permutation"): for i in self._settings.get_value("column-permutation"):
if sizes[i] > 0: if sizes[i] > 0:
self._columns[i].set_fixed_width(sizes[i]) self._columns[i].set_fixed_width(sizes[i])
self._columns[i].set_visible(visibilities[i]) self._columns[i].set_visible(visibilities[i])
self._treeview.append_column(self._columns[i]) self.append_column(self._columns[i])
def _clear(self, *args): def _clear(self, *args):
self._song_popover.popdown() self._song_popover.popdown()
@@ -2591,14 +2548,14 @@ class PlaylistWindow(Gtk.Overlay):
except IndexError: # invalid path except IndexError: # invalid path
self.set_property("selected-path", None) self.set_property("selected-path", None)
def _scroll_to_selected_title(self, *args): def scroll_to_selected_title(self):
treeview, treeiter=self._selection.get_selected() treeview, treeiter=self._selection.get_selected()
if treeiter is not None: if treeiter is not None:
path=treeview.get_path(treeiter) path=treeview.get_path(treeiter)
self._treeview.scroll_to_cell(path, None, True, 0.25) self.scroll_to_cell(path, None, True, 0.25)
def _refresh_selection(self): # Gtk.TreePath(len(self._store) is used to generate an invalid TreePath (needed to unset cursor) def _refresh_selection(self): # Gtk.TreePath(len(self._store) is used to generate an invalid TreePath (needed to unset cursor)
self._treeview.set_cursor(Gtk.TreePath(len(self._store)), None, False) self.set_cursor(Gtk.TreePath(len(self._store)), None, False)
song=self._client.status().get("song") song=self._client.status().get("song")
if song is None: if song is None:
self._selection.unselect_all() self._selection.unselect_all()
@@ -2621,15 +2578,8 @@ class PlaylistWindow(Gtk.Overlay):
if event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS: if event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS:
self._store.remove(self._store.get_iter(path)) self._store.remove(self._store.get_iter(path))
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS: elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
self._song_popover.open(self._store[path][8], widget, int(event.x), int(event.y)) point=self.convert_bin_window_to_widget_coords(event.x,event.y)
self._song_popover.open(self._store[path][8], widget, *point)
def _on_show_hide_back_button(self, *args):
visible_range=self._treeview.get_visible_range()
if visible_range is None or self.get_property("selected-path") is None:
self._back_button_revealer.set_reveal_child(False)
else:
current_song_visible=(visible_range[0] <= self.get_property("selected-path") <= visible_range[1])
self._back_button_revealer.set_reveal_child(not(current_song_visible))
def _on_key_release_event(self, widget, event): def _on_key_release_event(self, widget, event):
if event.keyval == Gdk.keyval_from_name("Delete"): if event.keyval == Gdk.keyval_from_name("Delete"):
@@ -2655,7 +2605,7 @@ class PlaylistWindow(Gtk.Overlay):
self._playlist_version=int(self._client.status()["playlist"]) self._playlist_version=int(self._client.status()["playlist"])
except MPDBase.CommandError as e: except MPDBase.CommandError as e:
self._playlist_version=None self._playlist_version=None
self._client.emitter.emit("playlist_changed", int(self._client.status()["playlist"])) self._client.emitter.emit("playlist", int(self._client.status()["playlist"]))
raise e # propagate exception raise e # propagate exception
def _on_row_inserted(self, model, path, treeiter): def _on_row_inserted(self, model, path, treeiter):
@@ -2677,7 +2627,7 @@ class PlaylistWindow(Gtk.Overlay):
songs=self._client.playlistinfo() songs=self._client.playlistinfo()
self._client.tagtypes("all") self._client.tagtypes("all")
if songs: if songs:
self._treeview.freeze_child_notify() self.freeze_child_notify()
self._set_playlist_info("") self._set_playlist_info("")
for song in songs: for song in songs:
try: try:
@@ -2704,7 +2654,7 @@ class PlaylistWindow(Gtk.Overlay):
song["file"], Pango.Weight.BOOK, song["file"], Pango.Weight.BOOK,
float(song["duration"]) float(song["duration"])
]) ])
self._treeview.thaw_child_notify() self.thaw_child_notify()
for i in reversed(range(int(self._client.status()["playlistlength"]), len(self._store))): for i in reversed(range(int(self._client.status()["playlistlength"]), len(self._store))):
treeiter=self._store.get_iter(i) treeiter=self._store.get_iter(i)
self._store.remove(treeiter) self._store.remove(treeiter)
@@ -2717,7 +2667,7 @@ class PlaylistWindow(Gtk.Overlay):
self._set_playlist_info(translated_string.format(number=playlist_length, duration=duration)) self._set_playlist_info(translated_string.format(number=playlist_length, duration=duration))
self._refresh_selection() self._refresh_selection()
if self._playlist_version != version: if self._playlist_version != version:
self._scroll_to_selected_title() self.scroll_to_selected_title()
self._playlist_version=version self._playlist_version=version
self._store.handler_unblock(self._row_inserted) self._store.handler_unblock(self._row_inserted)
self._store.handler_unblock(self._row_deleted) self._store.handler_unblock(self._row_deleted)
@@ -2725,30 +2675,58 @@ class PlaylistWindow(Gtk.Overlay):
def _on_song_changed(self, *args): def _on_song_changed(self, *args):
self._refresh_selection() self._refresh_selection()
if self._client.status()["state"] == "play": if self._client.status()["state"] == "play":
self._scroll_to_selected_title() self.scroll_to_selected_title()
def _on_back_to_current_song_button_clicked(self, *args):
self._treeview.set_cursor(Gtk.TreePath(len(self._store)), None, False) # set to invalid TreePath (needed to unset cursor)
if self.get_property("selected-path") is not None:
self._selection.select_path(self.get_property("selected-path"))
self._scroll_to_selected_title()
def _on_disconnected(self, *args): def _on_disconnected(self, *args):
self._treeview.set_sensitive(False) self.set_sensitive(False)
self._back_to_current_song_button.set_sensitive(False)
self._clear() self._clear()
def _on_reconnected(self, *args): def _on_reconnected(self, *args):
self._back_to_current_song_button.set_sensitive(True) self.set_sensitive(True)
self._treeview.set_sensitive(True)
def _on_show_info(self, *args): def show_info(self):
if self._treeview.has_focus(): treeview, treeiter=self._selection.get_selected()
treeview, treeiter=self._selection.get_selected() if treeiter is not None:
if treeiter is not None: path=self._store.get_path(treeiter)
path=self._store.get_path(treeiter) self._song_popover.open(self._store[path][8], self, *self.get_popover_point(path))
cell=self._treeview.get_cell_area(path, None)
self._song_popover.open(self._store[path][8], self._treeview, int(cell.x), int(cell.y)) class PlaylistWindow(Gtk.Overlay):
def __init__(self, client, settings):
super().__init__()
self._back_to_current_song_button=Gtk.Button(
image=Gtk.Image.new_from_icon_name("go-previous-symbolic", Gtk.IconSize.BUTTON), tooltip_text=_("Scroll to current song"),
can_focus=False
)
self._back_to_current_song_button.get_style_context().add_class("osd")
self._back_button_revealer=Gtk.Revealer(
child=self._back_to_current_song_button, transition_duration=0,
margin_bottom=6, margin_start=6, halign=Gtk.Align.START, valign=Gtk.Align.END
)
self._treeview=PlaylistView(client, settings)
scroll=Gtk.ScrolledWindow(child=self._treeview)
# connect
self._back_to_current_song_button.connect("clicked", self._on_back_to_current_song_button_clicked)
scroll.get_vadjustment().connect("value-changed", self._on_show_hide_back_button)
self._treeview.connect("notify::selected-path", self._on_show_hide_back_button)
# packing
self.add(scroll)
self.add_overlay(self._back_button_revealer)
def _on_show_hide_back_button(self, *args):
visible_range=self._treeview.get_visible_range()
if visible_range is None or self._treeview.get_property("selected-path") is None:
self._back_button_revealer.set_reveal_child(False)
else:
current_song_visible=(visible_range[0] <= self._treeview.get_property("selected-path") <= visible_range[1])
self._back_button_revealer.set_reveal_child(not(current_song_visible))
def _on_back_to_current_song_button_clicked(self, *args):
self._treeview.set_cursor(Gtk.TreePath(len(self._treeview.get_model())), None, False) # unset cursor
if self._treeview.get_property("selected-path") is not None:
self._treeview.get_selection().select_path(self._treeview.get_property("selected-path"))
self._treeview.scroll_to_selected_title()
#################### ####################
# cover and lyrics # # cover and lyrics #
@@ -2773,7 +2751,7 @@ class LyricsWindow(Gtk.ScrolledWindow):
# connect # connect
self._client.emitter.connect("disconnected", self._on_disconnected) self._client.emitter.connect("disconnected", self._on_disconnected)
self._song_changed=self._client.emitter.connect("current_song_changed", self._refresh) self._song_changed=self._client.emitter.connect("current_song", self._refresh)
self._client.emitter.handler_block(self._song_changed) self._client.emitter.handler_block(self._song_changed)
# packing # packing
@@ -2891,7 +2869,7 @@ class MainCover(Gtk.Image):
self.set_size_request(size, size) self.set_size_request(size, size)
# connect # connect
self._client.emitter.connect("current_song_changed", self._refresh) self._client.emitter.connect("current_song", self._refresh)
self._client.emitter.connect("disconnected", self._on_disconnected) self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected) self._client.emitter.connect("reconnected", self._on_reconnected)
self._settings.connect("changed::track-cover", self._on_settings_changed) self._settings.connect("changed::track-cover", self._on_settings_changed)
@@ -3001,8 +2979,8 @@ class PlaybackControl(Gtk.ButtonBox):
self._settings.connect("changed::mini-player", self._mini_player) self._settings.connect("changed::mini-player", self._mini_player)
self._settings.connect("changed::show-stop", self._mini_player) self._settings.connect("changed::show-stop", self._mini_player)
self._client.emitter.connect("state", self._on_state) self._client.emitter.connect("state", self._on_state)
self._client.emitter.connect("playlist_changed", self._refresh_tooltips) self._client.emitter.connect("playlist", self._refresh_tooltips)
self._client.emitter.connect("current_song_changed", self._refresh_tooltips) self._client.emitter.connect("current_song", self._refresh_tooltips)
self._client.emitter.connect("disconnected", self._on_disconnected) self._client.emitter.connect("disconnected", self._on_disconnected)
# packing # packing
@@ -3071,7 +3049,7 @@ class SeekBar(Gtk.Box):
self._scale.connect("button-release-event", self._on_scale_button_release_event) self._scale.connect("button-release-event", self._on_scale_button_release_event)
self._client.emitter.connect("disconnected", self._disable) self._client.emitter.connect("disconnected", self._disable)
self._client.emitter.connect("state", self._on_state) self._client.emitter.connect("state", self._on_state)
self._client.emitter.connect("elapsed_changed", self._refresh) self._client.emitter.connect("elapsed", self._refresh)
# packing # packing
self.pack_start(elapsed_event_box, False, False, 0) self.pack_start(elapsed_event_box, False, False, 0)
@@ -3163,7 +3141,7 @@ class AudioFormat(Gtk.Box):
self._settings.connect("changed::show-audio-format", self._mini_player) self._settings.connect("changed::show-audio-format", self._mini_player)
self._client.emitter.connect("audio", self._on_audio) self._client.emitter.connect("audio", self._on_audio)
self._client.emitter.connect("bitrate", self._on_bitrate) self._client.emitter.connect("bitrate", self._on_bitrate)
self._client.emitter.connect("current_song_changed", self._on_song_changed) self._client.emitter.connect("current_song", self._on_song_changed)
self._client.emitter.connect("disconnected", self._on_disconnected) self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected) self._client.emitter.connect("reconnected", self._on_reconnected)
@@ -3335,7 +3313,7 @@ class VolumeButton(Gtk.VolumeButton):
# connect # connect
self._changed=self.connect("value-changed", self._set_volume) self._changed=self.connect("value-changed", self._set_volume)
self._client.emitter.connect("volume_changed", self._refresh) self._client.emitter.connect("volume", self._refresh)
self._client.emitter.connect("disconnected", self._on_disconnected) self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected) self._client.emitter.connect("reconnected", self._on_reconnected)
self.connect("button-press-event", self._on_button_press_event) self.connect("button-press-event", self._on_button_press_event)
@@ -3594,12 +3572,16 @@ class MainWindow(Gtk.ApplicationWindow):
simple_actions_data=( simple_actions_data=(
"settings","profile-settings","stats","help","menu", "settings","profile-settings","stats","help","menu",
"toggle-lyrics","back-to-current-album","toggle-search", "toggle-lyrics","back-to-current-album","toggle-search",
"profile-next","profile-prev","show-info","append","play","enqueue" "profile-next","profile-prev","show-info"
) )
for name in simple_actions_data: for name in simple_actions_data:
action=Gio.SimpleAction.new(name, None) action=Gio.SimpleAction.new(name, None)
action.connect("activate", getattr(self, ("_on_"+name.replace("-","_")))) action.connect("activate", getattr(self, ("_on_"+name.replace("-","_"))))
self.add_action(action) self.add_action(action)
for name in ("append","play","enqueue"):
action=Gio.SimpleAction.new(name, None)
action.connect("activate", self._on_add_to_playlist, name)
self.add_action(action)
self.add_action(self._settings.create_action("mini-player")) self.add_action(self._settings.create_action("mini-player"))
self.add_action(self._settings.create_action("genre-filter")) self.add_action(self._settings.create_action("genre-filter"))
self.add_action(self._settings.create_action("active-profile")) self.add_action(self._settings.create_action("active-profile"))
@@ -3680,7 +3662,7 @@ class MainWindow(Gtk.ApplicationWindow):
self._settings.connect_after("changed::mini-player", self._mini_player) self._settings.connect_after("changed::mini-player", self._mini_player)
self._settings.connect_after("notify::cursor-watch", self._on_cursor_watch) self._settings.connect_after("notify::cursor-watch", self._on_cursor_watch)
self._settings.connect("changed::playlist-right", self._on_playlist_pos_changed) self._settings.connect("changed::playlist-right", self._on_playlist_pos_changed)
self._client.emitter.connect("current_song_changed", self._on_song_changed) self._client.emitter.connect("current_song", self._on_song_changed)
self._client.emitter.connect("disconnected", self._on_disconnected) self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected) self._client.emitter.connect("reconnected", self._on_reconnected)
# auto save window state and size # auto save window state and size
@@ -3786,16 +3768,14 @@ class MainWindow(Gtk.ApplicationWindow):
self._settings.set_int("active-profile", ((current_profile-1)%3)) self._settings.set_int("active-profile", ((current_profile-1)%3))
def _on_show_info(self, action, param): def _on_show_info(self, action, param):
self._client.emitter.emit("show-info") widget=self.get_focus()
if hasattr(widget, "show_info") and callable(widget.show_info):
widget.show_info()
def _on_append(self, action, param): def _on_add_to_playlist(self, action, param, mode):
self._client.emitter.emit("add-to-playlist", "append") widget=self.get_focus()
if hasattr(widget, "add_to_playlist") and callable(widget.add_to_playlist):
def _on_play(self, action, param): widget.add_to_playlist(mode)
self._client.emitter.emit("add-to-playlist", "play")
def _on_enqueue(self, action, param):
self._client.emitter.emit("add-to-playlist", "enqueue")
def _on_search_button_toggled(self, button): def _on_search_button_toggled(self, button):
if button.get_active(): if button.get_active():

View File

@@ -23,7 +23,7 @@
</ul> </ul>
</description> </description>
<releases> <releases>
<release version="1.4.0" date="2021-10-24"/> <release version="1.4.1" date="2021-11-13"/>
</releases> </releases>
<launchable type="desktop-id">org.mpdevil.mpdevil.desktop</launchable> <launchable type="desktop-id">org.mpdevil.mpdevil.desktop</launchable>
<screenshots> <screenshots>

View File

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