[gnome-music] playlistview: Use a context menu to edit tracks
- From: Marinus Schraal <mschraal src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-music] playlistview: Use a context menu to edit tracks
- Date: Thu, 18 Jan 2018 09:54:26 +0000 (UTC)
commit 657382d9754eb60345211b998a0ba3b75c2b4a39
Author: Jean Felder <jean felder gmail com>
Date: Fri Jan 12 00:29:06 2018 +0100
playlistview: Use a context menu to edit tracks
Convert self._view to Gtk.TreeView
Remove dependencies to libgd
Closes #48
data/PlaylistContextMenu.ui | 17 +++
data/application.css | 4 +
data/gnome-music.gresource.xml | 1 +
gnomemusic/views/playlistview.py | 241 +++++++++++++++++++++++++++------------
gnomemusic/window.py | 13 ++-
5 files changed, 198 insertions(+), 78 deletions(-)
---
diff --git a/data/PlaylistContextMenu.ui b/data/PlaylistContextMenu.ui
new file mode 100644
index 0000000..e6e7e53
--- /dev/null
+++ b/data/PlaylistContextMenu.ui
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <menu id="song_menu">
+ <item>
+ <attribute name="label" translatable="yes">Play</attribute>
+ <attribute name="action">win.play_song</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">Add to Playlist…</attribute>
+ <attribute name="action">win.add_song_to_playlist</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">Remove From Playlist</attribute>
+ <attribute name="action">win.remove_song</attribute>
+ </item>
+ </menu>
+</interface>
diff --git a/data/application.css b/data/application.css
index 294d8a0..c2cc58b 100644
--- a/data/application.css
+++ b/data/application.css
@@ -49,6 +49,10 @@ flowbox, treeview, widget {
background-color: @theme_bg_color;
}
+.songs-list:selected {
+ color: shade(@theme_fg_color, 0.0);
+}
+
/* We use background-image as a workaround on the StarImage widget to
enable transitions between the non-starred and starred icons. */
.star {
diff --git a/data/gnome-music.gresource.xml b/data/gnome-music.gresource.xml
index 709058b..d98bb47 100644
--- a/data/gnome-music.gresource.xml
+++ b/data/gnome-music.gresource.xml
@@ -15,6 +15,7 @@
<file preprocess="xml-stripblanks">headerbar.ui</file>
<file preprocess="xml-stripblanks">TrackWidget.ui</file>
<file preprocess="xml-stripblanks">NoMusic.ui</file>
+ <file preprocess="xml-stripblanks">PlaylistContextMenu.ui</file>
<file preprocess="xml-stripblanks">PlaylistControls.ui</file>
<file preprocess="xml-stripblanks">PlaylistDialog.ui</file>
</gresource>
diff --git a/gnomemusic/views/playlistview.py b/gnomemusic/views/playlistview.py
index 38e40f8..bfef5e3 100644
--- a/gnomemusic/views/playlistview.py
+++ b/gnomemusic/views/playlistview.py
@@ -23,13 +23,14 @@
# delete this exception statement from your version.
from gettext import gettext as _, ngettext
-from gi.repository import Gd, Gio, GLib, GObject, Gtk, Pango
+from gi.repository import Gio, GLib, GObject, Gtk, Pango
from gnomemusic import log
from gnomemusic.grilo import grilo
from gnomemusic.player import DiscoveryStatus
from gnomemusic.playlists import Playlists, StaticPlaylists
from gnomemusic.views.baseview import BaseView
+from gnomemusic.widgets.playlistdialog import PlaylistDialog
import gnomemusic.utils as utils
playlists = Playlists.get_default()
@@ -58,15 +59,12 @@ class PlaylistView(BaseView):
sidebar_container.add(self._sidebar)
super().__init__(
- 'playlists', _("Playlists"), window, Gd.MainViewType.LIST, True,
- sidebar_container)
+ 'playlists', _("Playlists"), window, None, True, sidebar_container)
self._window = window
self.player = player
- style_context = self._view.get_generic_view().get_style_context()
- style_context.add_class('songs-list')
- style_context.remove_class('content-view')
+ self._view.get_style_context().add_class('songs-list')
self._add_list_renderers()
@@ -82,14 +80,34 @@ class PlaylistView(BaseView):
self._songs_count_label = builder.get_object('songs_count')
self._menubutton = builder.get_object('playlist_menubutton')
+ builder = Gtk.Builder()
+ builder.add_from_resource('/org/gnome/Music/PlaylistContextMenu.ui')
+ self._popover_menu = builder.get_object('song_menu')
+ self._song_popover = Gtk.Popover.new_from_model(
+ self._view, self._popover_menu)
+ self._song_popover.set_position(Gtk.PositionType.BOTTOM)
+
+ play_song = Gio.SimpleAction.new('play_song', None)
+ play_song.connect('activate', self._play_song)
+ self._window.add_action(play_song)
+
+ add_song_to_playlist = Gio.SimpleAction.new(
+ 'add_song_to_playlist', None)
+ add_song_to_playlist.connect('activate', self._add_song_to_playlist)
+ self._window.add_action(add_song_to_playlist)
+
+ remove_song = Gio.SimpleAction.new('remove_song', None)
+ remove_song.connect('activate', self._remove_song)
+ self._window.add_action(remove_song)
+
playlist_play_action = Gio.SimpleAction.new('playlist_play', None)
playlist_play_action.connect('activate', self._on_play_activate)
self._window.add_action(playlist_play_action)
self._playlist_delete_action = Gio.SimpleAction.new('playlist_delete',
None)
- self._playlist_delete_action.connect('activate',
- self._on_delete_activate)
+ self._playlist_delete_action.connect(
+ 'activate', self._on_delete_activated)
self._window.add_action(self._playlist_delete_action)
self._playlist_rename_action = Gio.SimpleAction.new(
'playlist_rename', None)
@@ -137,52 +155,65 @@ class PlaylistView(BaseView):
pass
@log
- def _add_list_renderers(self):
- list_widget = self._view.get_generic_view()
- cols = list_widget.get_columns()
- cells = cols[0].get_cells()
- cells[2].set_visible(False)
- now_playing_symbol_renderer = Gtk.CellRendererPixbuf(xpad=0,
- xalign=0.5,
- yalign=0.5)
+ def _setup_view(self, view_type):
+ view_container = Gtk.ScrolledWindow(hexpand=True, vexpand=True)
+ self._box.pack_start(view_container, True, True, 0)
+
+ self._view = Gtk.TreeView()
+ self._view.set_headers_visible(False)
+ self._view.set_valign(Gtk.Align.START)
+ self._view.set_model(self.model)
+ self._view.set_activate_on_single_click(True)
+ self._view.get_selection().set_mode(Gtk.SelectionMode.SINGLE)
+ self._view.connect('row-activated', self._on_song_activated)
+ self._view.connect('button-press-event', self._on_view_clicked)
+ view_container.add(self._view)
+
+ @log
+ def _add_list_renderers(self):
+ now_playing_symbol_renderer = Gtk.CellRendererPixbuf(
+ xpad=0, xalign=0.5, yalign=0.5)
column_now_playing = Gtk.TreeViewColumn()
column_now_playing.set_fixed_width(48)
column_now_playing.pack_start(now_playing_symbol_renderer, False)
- column_now_playing.set_cell_data_func(now_playing_symbol_renderer,
- self._on_list_widget_icon_render,
- None)
- list_widget.insert_column(column_now_playing, 0)
+ column_now_playing.set_cell_data_func(
+ now_playing_symbol_renderer, self._on_list_widget_icon_render,
+ None)
+ self._view.append_column(column_now_playing)
title_renderer = Gtk.CellRendererText(
xpad=0, xalign=0.0, yalign=0.5, height=48,
ellipsize=Pango.EllipsizeMode.END)
- list_widget.add_renderer(title_renderer,
- self._on_list_widget_title_render, None)
- cols[0].add_attribute(title_renderer, 'text', 2)
-
- self._star_handler.add_star_renderers(list_widget, cols[0])
-
- duration_renderer = Gd.StyledTextRenderer(xpad=32, xalign=1.0)
- duration_renderer.add_class('dim-label')
- list_widget.add_renderer(duration_renderer,
- self._on_list_widget_duration_render, None)
-
- artist_renderer = Gd.StyledTextRenderer(
+ column_title = Gtk.TreeViewColumn("Title", title_renderer, text=2)
+ column_title.set_expand(True)
+ self._view.append_column(column_title)
+
+ column_star = Gtk.TreeViewColumn()
+ self._view.append_column(column_star)
+ self._star_handler.add_star_renderers(self._view, column_star)
+
+ duration_renderer = Gtk.CellRendererText(xpad=32, xalign=1.0)
+ column_duration = Gtk.TreeViewColumn()
+ column_duration.pack_start(duration_renderer, False)
+ column_duration.set_cell_data_func(
+ duration_renderer, self._on_list_widget_duration_render, None)
+ self._view.append_column(column_duration)
+
+ artist_renderer = Gtk.CellRendererText(
xpad=32, ellipsize=Pango.EllipsizeMode.END)
- artist_renderer.add_class('dim-label')
- list_widget.add_renderer(artist_renderer,
- self._on_list_widget_artist_render, None)
- cols[0].add_attribute(artist_renderer, 'text', 3)
+ column_artist = Gtk.TreeViewColumn("Artist", artist_renderer, text=3)
+ column_artist.set_expand(True)
+ self._view.append_column(column_artist)
- type_renderer = Gd.StyledTextRenderer(
+ album_renderer = Gtk.CellRendererText(
xpad=32, ellipsize=Pango.EllipsizeMode.END)
- type_renderer.add_class('dim-label')
- list_widget.add_renderer(type_renderer,
- self._on_list_widget_type_render, None)
-
- def _on_list_widget_title_render(self, col, cell, model, _iter, data):
- pass
+ column_album = Gtk.TreeViewColumn()
+ column_album.set_expand(True)
+ column_album.pack_start(album_renderer, True)
+ column_album.set_cell_data_func(
+ album_renderer, self._on_list_widget_album_render, None)
+ self._view.append_column(column_album)
def _on_list_widget_duration_render(self, col, cell, model, _iter, data):
if not model.iter_is_valid(_iter):
@@ -193,8 +224,13 @@ class PlaylistView(BaseView):
duration = item.get_duration()
cell.set_property('text', utils.seconds_to_string(duration))
- def _on_list_widget_artist_render(self, col, cell, model, _iter, data):
- pass
+ def _on_list_widget_album_render(self, coll, cell, model, _iter, data):
+ if not model.iter_is_valid(_iter):
+ return
+
+ item = model[_iter][5]
+ if item:
+ cell.set_property('text', utils.get_album_title(item))
def _on_list_widget_type_render(self, coll, cell, model, _iter, data):
if not model.iter_is_valid(_iter):
@@ -288,7 +324,16 @@ class PlaylistView(BaseView):
self._sidebar.emit('row-activated', row)
@log
- def _on_item_activated(self, widget, id, path):
+ def _on_song_activated(self, widget, path, column):
+ """Action performed when clicking on a song
+
+ clicking on star column toggles favorite
+ clicking on an other columns launches player
+
+ :param Gtk.Tree treeview: self._view
+ :param Gtk.TreePath path: activated row index
+ :param Gtk.TreeViewColumn column: activated column
+ """
if self._star_handler.star_renderer_click:
self._star_handler.star_renderer_click = False
return
@@ -299,11 +344,56 @@ class PlaylistView(BaseView):
return
if self.model[_iter][8] != self._error_icon_name:
- self.player.set_playlist('Playlist',
- self.current_playlist.get_id(),
- self.model, _iter, 5, 11)
+ self.player.set_playlist(
+ 'Playlist', self.current_playlist.get_id(), self.model, _iter,
+ 5, 11)
self.player.set_playing(True)
+ @log
+ def _on_view_clicked(self, treeview, event):
+ """Right click on self._view displays a context menu
+
+ :param Gtk.TreeView treeview: self._view
+ :param Gdk.EventButton event: clicked event
+ """
+ if event.button != 3:
+ return
+
+ path, col, cell_x, cell_y = treeview.get_path_at_pos(event.x, event.y)
+ self._view.get_selection().select_path(path)
+
+ rect = self._view.get_visible_rect()
+ rect.x = event.x - rect.width / 2.0
+ rect.y = event.y - rect.height + 5
+
+ self._song_popover.set_relative_to(self._view)
+ self._song_popover.set_pointing_to(rect)
+ self._song_popover.popup()
+ return
+
+ @log
+ def _play_song(self, menuitem, data=None):
+ model, _iter = self._view.get_selection().get_selected()
+ path = model.get_path(_iter)
+ cols = self._view.get_columns()
+ self._view.emit('row-activated', path, cols[0])
+
+ @log
+ def _add_song_to_playlist(self, menuitem, data=None):
+ model, _iter = self._view.get_selection().get_selected()
+ song = model[_iter][5]
+
+ playlist_dialog = PlaylistDialog(self._window, self.pl_todelete)
+ if playlist_dialog.run() == Gtk.ResponseType.ACCEPT:
+ playlists.add_to_playlist(playlist_dialog.get_selected(), [song])
+ playlist_dialog.destroy()
+
+ @log
+ def _remove_song(self, menuitem, data=None):
+ model, _iter = self._view.get_selection().get_selected()
+ song = model[_iter][5]
+ playlists.remove_from_playlist(self.current_playlist, [song])
+
@log
def _on_playlist_update(self, playlists, playlist_id):
"""Refresh the displayed playlist if necessary
@@ -367,7 +457,7 @@ class PlaylistView(BaseView):
def remove_playlist(self):
"""Removes the current selected playlist"""
if not self._current_playlist_is_protected():
- self._on_delete_activate(None)
+ self._on_delete_activated(None)
@log
def _on_playlist_activated(self, sidebar, row, data=None):
@@ -387,7 +477,7 @@ class PlaylistView(BaseView):
self._view.set_model(None)
self.model.clear()
self._songs_count = 0
- grilo.populate_playlist_songs(playlist, self._add_item)
+ grilo.populate_playlist_songs(playlist, self._add_song)
if self._current_playlist_is_protected():
self._playlist_delete_action.set_enabled(False)
@@ -397,14 +487,28 @@ class PlaylistView(BaseView):
self._playlist_rename_action.set_enabled(True)
@log
- def _add_item(self, source, param, item, remaining=0, data=None):
- self._add_item_to_model(item, self.model)
+ def _add_song(self, source, param, song, remaining=0, data=None):
+ """Grilo.populate_playlist_songs callback.
+
+ Add all playlists found by Grilo to self._model
+
+ :param GrlTrackerSource source: tracker source
+ :param int param: param
+ :param GrlMedia song: song to add
+ :param int remaining: next playlist_id or zero if None
+ :param data: associated data
+ """
+ self._add_song_to_model(song, self.model)
if remaining == 0:
self._view.set_model(self.model)
@log
- def _add_item_to_model(self, item, model):
- if not item:
+ def _add_song_to_model(self, song, model):
+ """Add song to a playlist
+ :param Grl.Media song: song to add
+ :param Gtk.ListStore model: model
+ """
+ if not song:
self._update_songs_count()
if self.player.playlist:
self.player._validate_next_track()
@@ -412,11 +516,11 @@ class PlaylistView(BaseView):
return
self._offset += 1
- title = utils.get_media_title(item)
- item.set_title(title)
- artist = utils.get_artist_name(item)
+ title = utils.get_media_title(song)
+ song.set_title(title)
+ artist = utils.get_artist_name(song)
model.insert_with_valuesv(-1, [2, 3, 5, 9],
- [title, artist, item, item.get_favourite()])
+ [title, artist, song, song.get_favourite()])
self._songs_count += 1
@@ -426,20 +530,16 @@ class PlaylistView(BaseView):
ngettext("%d Song", "%d Songs", self._songs_count)
% self._songs_count)
- @log
- def _on_selection_mode_changed(self, widget, data=None):
- self._sidebar.set_sensitive(not self._header_bar._selectionMode)
- self._menubutton.set_sensitive(not self._header_bar._selectionMode)
-
@log
def _on_play_activate(self, menuitem, data=None):
_iter = self.model.get_iter_first()
if not _iter:
return
- selection = self._view.get_generic_view().get_selection()
+ selection = self._view.get_selection()
selection.select_path(self.model.get_path(_iter))
- self._view.emit('item-activated', '0', self.model.get_path(_iter))
+ cols = self._view.get_columns()
+ self._view.emit('row-activated', self.model.get_path(_iter), cols[0])
@log
def _current_playlist_is_protected(self):
@@ -472,7 +572,7 @@ class PlaylistView(BaseView):
self.pl_todelete, self._pl_todelete_index)
@log
- def _on_delete_activate(self, menuitem, data=None):
+ def _on_delete_activated(self, menuitem, data=None):
self._window.show_playlist_notification()
self._stage_playlist_for_deletion()
@@ -533,7 +633,7 @@ class PlaylistView(BaseView):
def _on_song_added_to_playlist(self, playlists, playlist, item):
if (self.current_playlist
and playlist.get_id() == self.current_playlist.get_id()):
- self._add_item_to_model(item, self.model)
+ self._add_song_to_model(item, self.model)
@log
def _on_song_removed_from_playlist(self, playlists, playlist, item):
@@ -590,8 +690,3 @@ class PlaylistView(BaseView):
for row in self._sidebar:
self._sidebar.remove(row)
grilo.populate_playlists(self._offset, self._add_playlist_item)
-
- @log
- def get_selected_songs(self, callback):
- callback([self.model[self.model.get_iter(path)][5]
- for path in self._view.get_selection()])
diff --git a/gnomemusic/window.py b/gnomemusic/window.py
index 96f5185..d5074b8 100644
--- a/gnomemusic/window.py
+++ b/gnomemusic/window.py
@@ -510,11 +510,14 @@ class Window(Gtk.ApplicationWindow):
if self.curr_view != self.views[View.SEARCH] and self.curr_view != self.views[View.EMPTY_SEARCH]:
self.toolbar.searchbar.reveal(False)
- # Toggle the selection button for the EmptySearch view
- if self.curr_view == self.views[View.EMPTY_SEARCH] or \
- self.prev_view == self.views[View.EMPTY_SEARCH]:
- self.toolbar._select_button.set_sensitive(
- not self.toolbar._select_button.get_sensitive())
+ # Disable the selection button for the EmptySearch and Playlist
+ # view
+ no_selection_mode = [
+ self.views[View.EMPTY_SEARCH],
+ self.views[View.PLAYLIST]
+ ]
+ self.toolbar._select_button.set_sensitive(
+ self.curr_view not in no_selection_mode)
# Disable renaming playlist if it was active when leaving
# Playlist view
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]