[gnome-music] playlistview: Reorder a playlist



commit 094f1f5cbdde73c5d8625721360d2dff4d87bc9d
Author: Jean Felder <jean felder gmail com>
Date:   Sun Jan 28 21:54:10 2018 +0100

    playlistview: Reorder a playlist
    
    Add drag and drop support
    Static playlists can not be reordered
    
    Closes: #108

 gnomemusic/playlists.py          |  21 +++++++
 gnomemusic/query.py              |  23 +++++++
 gnomemusic/views/playlistview.py | 132 ++++++++++++++++++++++++++++++++-------
 3 files changed, 153 insertions(+), 23 deletions(-)
---
diff --git a/gnomemusic/playlists.py b/gnomemusic/playlists.py
index ed32717..6d202b4 100644
--- a/gnomemusic/playlists.py
+++ b/gnomemusic/playlists.py
@@ -126,6 +126,9 @@ class Playlists(GObject.GObject):
         'song-removed-from-playlist': (
             GObject.SignalFlags.RUN_FIRST, None, (Grl.Media, Grl.Media)
         ),
+        'song-position-changed': (
+            GObject.SignalFlags.RUN_FIRST, None, (Grl.Media, Grl.Media)
+        ),
     }
 
     instance = None
@@ -428,6 +431,24 @@ class Playlists(GObject.GObject):
                 None, update_callback, item
             )
 
+    @log
+    def reorder_playlist(self, playlist, items, new_positions):
+        """Change the order of songs on a playlist.
+
+        :param GlrMedia playlist: playlist to reorder
+        :param list items: songs to reorder
+        :param list new_positions: new songs positions
+        """
+        def update_callback(conn, res, data):
+            conn.update_finish(res)
+            self.emit('song-position-changed', playlist, data)
+
+        for item, new_position in zip(items, new_positions):
+            self.tracker.update_async(
+                Query.change_song_position(
+                    playlist.get_id(), item.get_id(), new_position),
+                GLib.PRIORITY_LOW, None, update_callback, item)
+
     @log
     def is_static_playlist(self, playlist):
         """Checks whether the given playlist is static or not
diff --git a/gnomemusic/query.py b/gnomemusic/query.py
index c4ff40c..5632868 100644
--- a/gnomemusic/query.py
+++ b/gnomemusic/query.py
@@ -606,6 +606,29 @@ class Query():
         }
         return query
 
+    @staticmethod
+    def change_song_position(playlist_id, song_id, new_position):
+        query = """
+    INSERT OR REPLACE {
+        ?entry
+            nfo:listPosition %(position)s
+    }
+    WHERE {
+        ?playlist a nmm:Playlist ;
+            a nfo:MediaList ;
+            nfo:hasMediaFileListEntry ?entry .
+        FILTER (
+            tracker:id(?playlist) = %(playlist_id)s &&
+            tracker:id(?entry) = %(song_id)s
+        )
+    }
+    """.replace("\n", " ").strip() % {
+            'playlist_id': playlist_id,
+            'song_id': song_id,
+            'position': float(new_position)
+        }
+        return query
+
     @staticmethod
     def get_playlist_with_id(playlist_id):
         query = """
diff --git a/gnomemusic/views/playlistview.py b/gnomemusic/views/playlistview.py
index af57942..bf3176f 100644
--- a/gnomemusic/views/playlistview.py
+++ b/gnomemusic/views/playlistview.py
@@ -140,6 +140,9 @@ class PlaylistView(BaseView):
 
         self._update_songs_count()
 
+        self.model.connect('row-inserted', self._on_song_inserted)
+        self.model.connect('row-deleted', self._on_song_deleted)
+
         self.player.connect('playlist-item-changed', self._update_model)
         playlists.connect('playlist-created', self._on_playlist_created)
         playlists.connect('playlist-updated', self._on_playlist_update)
@@ -147,6 +150,8 @@ class PlaylistView(BaseView):
             'song-added-to-playlist', self._on_song_added_to_playlist)
         playlists.connect(
             'song-removed-from-playlist', self._on_song_removed_from_playlist)
+        playlists.connect(
+            'song-position-changed', self._on_song_position_changed)
 
         self.show_all()
 
@@ -167,6 +172,9 @@ class PlaylistView(BaseView):
         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)
+        self._view.connect('drag-begin', self._drag_begin)
+        self._view.connect('drag-end', self._drag_end)
+        self._song_drag = {'active': False}
 
         view_container.add(self._view)
 
@@ -330,25 +338,30 @@ class PlaylistView(BaseView):
 
         clicking on star column toggles favorite
         clicking on an other columns launches player
+        Action is not performed if drag and drop is active
 
         :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
+        def activate_song():
+            if self._song_drag['active']:
+                return
+
+            if self._star_handler.star_renderer_click:
+                self._star_handler.star_renderer_click = False
+                return
 
-        try:
             _iter = self.model.get_iter(path)
-        except TypeError:
-            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_playing(True)
 
-        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_playing(True)
+        # 'row-activated' signal is emitted before 'drag-begin' signal.
+        # Need to wait to check if drag and drop operation is active.
+        GLib.idle_add(activate_song)
 
     @log
     def _on_view_clicked(self, treeview, event):
@@ -372,6 +385,59 @@ class PlaylistView(BaseView):
         self._song_popover.popup()
         return
 
+    @log
+    def _drag_begin(self, widget_, drag_context):
+        self._song_drag['active'] = True
+
+    @log
+    def _drag_end(self, widget_, drag_context):
+        self._song_drag['active'] = False
+
+    @log
+    def _on_song_inserted(self, model, path, iter_):
+        if not self._song_drag['active']:
+            return
+
+        self._song_drag['new_pos'] = int(path.to_string())
+
+    @log
+    def _on_song_deleted(self, model, path):
+        """Save new playlist order after drag and drop operation.
+
+        Update player's playlist if necessary.
+        """
+        if not self._song_drag['active']:
+            return
+
+        new_pos = self._song_drag['new_pos']
+        prev_pos = int(path.to_string())
+
+        if abs(new_pos - prev_pos) == 1:
+            return
+
+        # If playing song position has changed, update player's playlist.
+        if self.player.playing and not self.player.currentTrack.valid():
+            pos = new_pos
+            if new_pos > prev_pos:
+                pos -= 1
+            new_iter = self.model.get_iter_from_string(str(pos))
+            self._iter_to_clean = new_iter
+            self.player.set_playlist(
+                'Playlist', self.current_playlist.get_id(), self.model,
+                new_iter, 5, 11)
+
+        first_pos = min(new_pos, prev_pos)
+        last_pos = max(new_pos, prev_pos)
+
+        positions = []
+        songs = []
+        for pos in range(first_pos, last_pos):
+            _iter = model.get_iter_from_string(str(pos))
+            songs.append(model[_iter][5])
+            positions.append(pos + 1)
+
+        playlists.reorder_playlist(self.current_playlist, songs, positions)
+
     @log
     def _play_song(self, menuitem, data=None):
         model, _iter = self._view.get_selection().get_selected()
@@ -486,10 +552,12 @@ class PlaylistView(BaseView):
             self._playlist_delete_action.set_enabled(False)
             self._playlist_rename_action.set_enabled(False)
             self._remove_song_action.set_enabled(False)
+            self._view.set_reorderable(False)
         else:
             self._playlist_delete_action.set_enabled(True)
             self._playlist_rename_action.set_enabled(True)
             self._remove_song_action.set_enabled(True)
+            self._view.set_reorderable(True)
 
     @log
     def _add_song(self, source, param, song, remaining=0, data=None):
@@ -634,6 +702,19 @@ class PlaylistView(BaseView):
         """Add new playlist to sidebar"""
         self._add_playlist_to_sidebar(playlist)
 
+    @log
+    def _row_is_playing(self, playlist, row):
+        """Check if row is being played"""
+        if (self._is_current_playlist(playlist)
+                and self.player.currentTrack is not None
+                and self.player.currentTrack.valid()):
+                track_path = self.player.currentTrack.get_path()
+                track_path_str = track_path.to_string()
+                if (row.path is not None
+                        and row.path.to_string() == track_path_str):
+                    return True
+        return False
+
     @log
     def _on_song_added_to_playlist(self, playlists, playlist, item):
         if self._is_current_playlist(playlist):
@@ -647,21 +728,10 @@ class PlaylistView(BaseView):
             return
 
         # checks if the to be removed track is now being played
-        def is_playing(row):
-            if self._is_current_playlist(playlist):
-                if (self.player.currentTrack is not None
-                        and self.player.currentTrack.valid()):
-                    track_path = self.player.currentTrack.get_path()
-                    track_path_str = track_path.to_string()
-                    if (row.path is not None
-                            and row.path.to_string() == track_path_str):
-                        return True
-            return False
-
         for row in model:
             if row[5].get_id() == item.get_id():
 
-                is_being_played = is_playing(row)
+                is_being_played = self._row_is_playing(playlist, row)
 
                 next_iter = model.iter_next(row.iter)
                 model.remove(row.iter)
@@ -686,6 +756,22 @@ class PlaylistView(BaseView):
                 self._update_songs_count()
                 return
 
+    @log
+    def _on_song_position_changed(self, playlists, playlist, item):
+        """ If song is currently played, update next track"""
+        if not self._is_current_playlist(playlist):
+            return
+
+        if self.player.playing:
+            for row in self.model:
+                if (row[5].get_id() == item.get_id()
+                        and self._row_is_playing(playlist, row)):
+                    self._iter_to_clean = row.iter
+                    self.player.set_playlist(
+                        'Playlist', playlist.get_id(), self.model, row.iter, 5,
+                        11)
+                    return
+
     @log
     def populate(self):
         """Populate sidebar.


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]